Welcome to QRules!#
QRules is a Python package for validating and generating particle reactions using quantum number conservation rules. The user only has to provide a certain set of boundary conditions (initial and final state, allowed interaction types, expected decay topologies, etc.). QRules will then span the space of allowed quantum numbers over all allowed decay topologies and particle instances that correspond with the sets of allowed quantum numbers it has found.
The resulting state transition objects are particularly useful for amplitude analysis / Partial Wave Analysis as they contain all information (such as expected masses, widths, and spin projections) that is needed to formulate an amplitude model.
The Usage pages illustrate several features of qrules
. You can run each
of them as Jupyter notebooks with the launch button in the top-right
corner. Enjoy!
Internal design
QRules consists of three major components:
State transition graphs
A
MutableTransition
is a directed graph that consists of nodes and edges. In a directed graph, each edge must be connected to at least one node (in correspondence to Feynman graphs). This way, a graph describes the transition from one state to another.Edges correspond to states (particles with spin). In other words, edges are a collection of properties such as the quantum numbers that characterize a state that the particle is in.
Nodes represents interactions and contain all information for the transition of this specific step. Most importantly, a node contains a collection of conservation rules that have to be satisfied. An interaction node has \(M\) ingoing lines and \(N\) outgoing lines, where \(M,N \in \mathbb{Z}\), \(M > 0, N > 0\).
Conservation rules
The central component are the
conservation_rules
. They belong to individual nodes and receive properties about the node itself, as well as properties of the ingoing and outgoing edges of that node. Based on those properties the conservation rules determine whether edges pass or not.Solvers
The determination of the correct state properties in the graph is done by solvers. New properties are set for intermediate edges and interaction nodes and their validity is checked with the conservation rules.
QRules workflow
Preparation
1.1. Build all possible topologies. A topology is represented by a
MutableTransition
, in which the edges and nodes are empty (no particle information).1.2. Fill the topology graphs with the user provided information. Typically these are the graph’s ingoing edges (initial state) and outgoing edges (final state).
Solving
2.1. Propagate quantum number information through the complete graph while respecting the specified conservation laws. Information like mass is not used in this first solving step.
2.2. Clone graphs while inserting concrete matching particles for the intermediate edges (mainly adds the mass variable).
2.3. Validate the complete graphs, so run all conservation law check that were postponed from the first step.
Table of Contents
Installation#
Quick installation#
The fastest way of installing this package is through PyPI or Conda:
python3 -m pip install qrules
conda install -c conda-forge qrules
This installs the latest release that you
can find on the stable
branch.
Optionally, you can install the dependencies required for visualizing topologies with the following optional dependency syntax:
pip install qrules[viz] # installs qrules with graphviz
The latest version on the main
branch
can be installed as follows:
python3 -m pip install git+https://github.com/ComPWA/qrules@main
Editable installation#
It is highly recommend to use the more dynamic ‘editable installation’. This allows you to:
exactly pin all dependencies to a specific version, so that your work is reproducible.
edit the source code of the framework and help improving it.
For this, you first need to get the source code with Git:
git clone https://github.com/ComPWA/qrules.git
cd qrules
Next, you install the project in editable mode with either
Conda or pip
. It’s
recommended to use Conda, because this also pins the version of Python.
conda env create
This installs the project in a Conda environment following the definitions in
environment.yml
.
[Recommended] Create a virtual environment with
venv
(see here).Install the project as an ‘editable installation’ with additional packages for the developer and all dependencies pinned through constraints files:
python3 -m pip install -c .constraints/py3.x.txt -e .[dev]
See Updating for how to update the dependencies when new commits come in.
That’s all! Have a look at Usage to try out the package. You can also have a look at Help developing for tips on how to work with this ‘editable’ developer setup!
Usage#
Main interface#
Here are some quick examples of how to use qrules
. For more fine-grained control, have a look at Advanced.
Investigate intermediate resonances#
import qrules
reaction = qrules.generate_transitions(
initial_state="J/psi(1S)",
final_state=["K0", "Sigma+", "p~"],
allowed_interaction_types="strong",
formalism="canonical-helicity",
)
import graphviz
dot = qrules.io.asdot(reaction, collapse_graphs=True)
graphviz.Source(dot)
Next, you use the ampform
package to convert these transitions into a mathematical description that you can use to fit your data and perform Partial Wave Analysis!
See also
Quantum number search#
The load_pdg()
function creates a ParticleCollection
containing the latest PDG info. Its find()
and filter()
methods allows you to quickly look up the quantum numbers of a particle and, vice versa, look up particle candidates based on a set of quantum numbers.
import qrules
pdg = qrules.load_pdg()
pdg.find(22) # by pid
pdg.find("Delta(1920)++")
Particle(
name='Delta(1920)++',
pid=22224,
latex='\\Delta(1920)^{++}',
spin=1.5,
mass=1.92,
width=0.3,
charge=2,
isospin=Spin(3/2, +3/2),
baryon_number=1,
parity=+1,
)
subset = pdg.filter(lambda p: p.spin in {2.5, 3.5, 4.5} and p.name.startswith("N"))
subset.names
['N(1675)~-',
'N(1675)~0',
'N(1675)0',
'N(1675)+',
'N(1680)~-',
'N(1680)~0',
'N(1680)0',
'N(1680)+',
'N(2190)~-',
'N(2190)~0',
'N(2190)0',
'N(2190)+']
Tip
Check allowed reactions#
qrules
can be used to check
whether a transition between an initial and final state is violated by any conservation rules:
qrules.check_reaction_violations(
initial_state="pi0",
final_state=["gamma", "gamma", "gamma"],
)
{frozenset({'c_parity_conservation'})}
Advanced#
Each of the qrules
’s sub-modules offer functionality to handle more advanced reaction types. The following notebooks illustrate how use them.
Generate transitions#
A Partial Wave Analysis starts by defining an amplitude model that describes the reaction process that is to be analyzed. Such a model is generally very complex and requires a fair amount of effort by the analyst (you). This gives a lot of room for mistakes.
QRules is responsible to give you advice on the form of an amplitude model, based on the problem set you define (initial state, final state, allowed interactions, intermediate states, etc.). Internally, the system propagates the quantum numbers through the reaction graph while satisfying the specified conservation rules. How to control this procedure is explained in more detail below.
Afterwards, the amplitude model produced by AmpForm can be exported into TensorWaves. The model can for instance be used to generate a data set (toy Monte Carlo) for this reaction and to optimize its parameters to resemble an actual data set as good as possible. For more info on that see Formulate amplitude model.
Note
Simple channels can be treated with the generate_transitions()
façade function. This notebook shows how to treat more complicated cases with the StateTransitionManager
.
1. Define the problem set#
We first define the boundary conditions of our physics problem, such as initial state, final state, formalism type, etc. and pass all of that information to the StateTransitionManager
. This is the main user interface class of qrules
.
By default, the StateTransitionManager
loads all particles from the PDG. The qrules
would take a long time to check the quantum numbers of all these particles, so in this notebook, we use a smaller subset of relatively common particles.
from qrules import InteractionType, StateTransitionManager
stm = StateTransitionManager(
initial_state=["J/psi(1S)"],
final_state=["gamma", "pi0", "pi0"],
formalism="helicity",
max_angular_momentum=2,
)
State Transition Manager
The StateTransitionManager
(STM) is the main user interface class of qrules
. The boundary conditions of your physics problem, such as the initial state, final state, formalism type, etc., are defined through this interface.
create_problem_sets()
of the STM creates all problem sets ― using the boundary conditions of theStateTransitionManager
instance. In total 3 steps are performed. The creation of reaction topologies. The creation ofInitialFacts
, based on a topology and the initial and final state information. And finally the solving settings such as the conservation laws and quantum number domains to use at which point of the topology.By default, all three interaction types (
EM
,STRONG
, andWEAK
) are used in the preparation stage. However, it is also possible to choose the allowed interaction types globally viaset_allowed_interaction_types()
.After the preparation step, you can modify the problem sets returned by
create_problem_sets()
to your liking. Since the output of this function contains quite a lot of information,qrules
aids in the configuration (especially the STM).A subset of particles that are allowed as intermediate states can also be specified: either through the
STM's constructor
or by setting the instanceallowed_intermediate_particles
.
Tip
Custom topologies shows how to provide custom Topology
instances to the STM, so that you generate more than just isobar decays.
2. Prepare Problem Sets#
Create all ProblemSet
’s using the boundary conditions of the StateTransitionManager
instance. By default it uses the isobar model (tree of two-body decays) to build Topology
’s. Various InitialFacts
are created for each topology based on the initial and final state. Lastly some reasonable default settings for the solving process are chosen. Remember that each interaction node defines its own set of conservation laws.
The StateTransitionManager
(STM) defines three interaction types:
Interaction |
Strength |
---|---|
strong |
\(60\) |
electromagnetic (EM) |
\(1\) |
weak |
\(10^{-4}\) |
By default, all three are used in the preparation stage. The create_problem_sets()
method of the STM generates graphs with all possible combinations of interaction nodes. An overall interaction strength is assigned to each graph and they are grouped according to this strength.
problem_sets = stm.create_problem_sets()
sorted(problem_sets, reverse=True)
[60.0, 1.0, 0.0001]
To get an idea of what these ProblemSet
s represent, you can use asdot()
and Graphviz to visualize one of them (see Visualize solutions):
import graphviz
from qrules import io
some_problem_set = problem_sets[60.0][0]
dot = io.asdot(some_problem_set, render_node=True)
graphviz.Source(dot)
Each ProblemSet
provides a mapping of initial_facts
that represent the initial and final states with spin projections. The nodes and edges in between these initial_facts
are still to be generated. This will be done from the provided solving_settings
. There are two mechanisms there:
One the one hand, the
EdgeSettings.qn_domains
andNodeSettings.qn_domains
contained in thesolving_settings
define the domain over which quantum number sets can be generated.On the other, the
EdgeSettings.rule_priorities
andNodeSettings.rule_priorities
insolving_settings
define whichconservation_rules
are used to determine which of the sets of generated quantum numbers are valid.
Together, these two constraints allow the StateTransitionManager
to generate a number of MutableTransition
s that comply with the selected conservation_rules
.
3. Find solutions#
If you are happy with the default settings generated by the StateTransitionManager
, just start with solving directly!
This step takes about 23 sec on an Intel(R) Core(TM) i7-6820HQ CPU of 2.70GHz running, multi-threaded.
reaction = stm.find_solutions(problem_sets)
Tip
See Quantum number solutions for a visualization of the intermediate steps.
The find_solutions()
method returns a ReactionInfo
object from which you can extract the transitions
. Now, you can use get_intermediate_particles()
to print the names of the intermediate states that the StateTransitionManager
found:
print("found", len(reaction.transitions), "solutions!")
reaction.get_intermediate_particles().names
found 420 solutions!
['a(0)(980)0',
'a(1)(1260)0',
'a(2)(1320)0',
'a(0)(1450)0',
'a(1)(1640)0',
'a(2)(1700)0',
'b(1)(1235)0',
'f(0)(500)',
'f(0)(980)',
'f(2)(1270)',
'f(1)(1285)',
'f(0)(1370)',
'f(1)(1420)',
"f(2)'(1525)",
'f(0)(1500)',
'f(0)(1710)',
'f(2)(1950)',
'f(0)(2020)',
'f(2)(2010)',
'f(2)(2300)',
'f(2)(2340)',
'h(1)(1170)',
'omega(782)',
'omega(1420)',
'omega(1650)',
'phi(1020)',
'phi(1680)',
'rho(770)0',
'rho(1450)0',
'rho(1700)0']
About the number of solutions
The “number of transitions
” is the total number of allowed MutableTransition
instances that the StateTransitionManager
has found. This also includes all allowed spin projection combinations. In this channel, we for example consider a \(J/\psi\) with spin projection \(\pm1\) that decays into a \(\gamma\) with spin projection \(\pm1\), which already gives us four possibilities.
On the other hand, the intermediate state names that was extracted with ReactionInfo.get_intermediate_particles()
, is just a set
of the state names on the intermediate edges of the list of transitions
, regardless of spin projection.
Now we have a lot of solutions that are actually heavily suppressed (they involve two weak decays).
Select interaction types#
In general, you can modify the ProblemSet
s returned by create_problem_sets()
directly, but the STM also comes with functionality to globally choose the allowed interaction types. So, go ahead and disable the EM
and InteractionType.WEAK
interactions:
stm.set_allowed_interaction_types([InteractionType.STRONG])
problem_sets = stm.create_problem_sets()
reaction = stm.find_solutions(problem_sets)
print("found", len(reaction.transitions), "solutions!")
reaction.get_intermediate_particles().names
found 198 solutions!
['b(1)(1235)0',
'f(0)(500)',
'f(0)(980)',
'f(2)(1270)',
'f(0)(1370)',
"f(2)'(1525)",
'f(0)(1500)',
'f(0)(1710)',
'f(2)(1950)',
'f(0)(2020)',
'f(2)(2010)',
'f(2)(2300)',
'f(2)(2340)',
'rho(770)0',
'rho(1450)0',
'rho(1700)0']
Now note that, since a \(\gamma\) particle appears in one of the interaction nodes, qrules
knows that this node must involve EM interactions! Because the node can be an effective interaction, the weak interaction cannot be excluded, as it contains only a subset of conservation laws. Since only the strong interaction was supposed to be used, this results in a warning and the STM automatically corrects the mistake. Once the EM interaction is included, this warning disappears.
stm.set_allowed_interaction_types([InteractionType.STRONG, InteractionType.EM])
problem_sets = stm.create_problem_sets()
reaction = stm.find_solutions(problem_sets)
print("found", len(reaction.transitions), "solutions!")
reaction.get_intermediate_particles().names
found 324 solutions!
['a(0)(980)0',
'a(2)(1320)0',
'a(0)(1450)0',
'a(2)(1700)0',
'b(1)(1235)0',
'f(0)(500)',
'f(0)(980)',
'f(2)(1270)',
'f(0)(1370)',
"f(2)'(1525)",
'f(0)(1500)',
'f(0)(1710)',
'f(2)(1950)',
'f(0)(2020)',
'f(2)(2010)',
'f(2)(2300)',
'f(2)(2340)',
'h(1)(1170)',
'omega(782)',
'omega(1420)',
'omega(1650)',
'phi(1020)',
'phi(1680)',
'rho(770)0',
'rho(1450)0',
'rho(1700)0']
This automatic selection of conservation rules can be switched of the StateTransitionManager.interaction_determinators
. Here’s an example where we deselect the check that causes makes detects the existence of a photon in the decay chain. Note, however, that for \(J/\psi \to \gamma\pi^0\pi^0\), this results in non-executed node rules:
from qrules.system_control import GammaCheck
stm_no_check = StateTransitionManager(
initial_state=["J/psi(1S)"],
final_state=["gamma", "pi0", "pi0"],
)
stm_no_check.interaction_determinators = [
check
for check in stm_no_check.interaction_determinators
if not isinstance(check, GammaCheck)
]
stm_no_check.set_allowed_interaction_types([InteractionType.STRONG])
problem_sets_no_check = stm_no_check.create_problem_sets()
try:
reaction_no_check = stm_no_check.find_solutions(problem_sets_no_check)
except RuntimeError as e:
msg, execution_info = e.args
execution_info
ExecutionInfo(
not_executed_node_rules=defaultdict(set,
{0: {'g_parity_conservation', 'isospin_conservation'},
1: {'g_parity_conservation', 'isospin_conservation'}}),
violated_node_rules=defaultdict(set, {0: set(), 1: set()}),
not_executed_edge_rules=defaultdict(set, {0: {'isospin_validity'}}),
violated_edge_rules=defaultdict(set, {}),
)
Be aware that after calling set_allowed_interaction_types()
, the EM
interaction is now selected for all nodes, for each node in the decay topology. Hence, there now might be solutions in which both nodes are electromagnetic. This is fine for the decay \(J/\psi \to \gamma \pi^0 \pi^0\), but for decays that require the WEAK
interaction type, you want to set the interaction type per specific nodes. Take for example the decay \(\Lambda_c^+ \to p K^- \pi^+\), which has a production node that is mediated by the weak force and a decay node that goes via the strong force. In this case, only limit the decay node to the STRONG
force:
lc2pkpi_stm = StateTransitionManager(
initial_state=["Lambda(c)+"],
final_state=["p", "K-", "pi+"],
mass_conservation_factor=0.6,
)
lc2pkpi_stm.set_allowed_interaction_types([InteractionType.STRONG], node_id=1)
lc2pkpi_problem_sets = lc2pkpi_stm.create_problem_sets()
lc2pkpi_reaction = lc2pkpi_stm.find_solutions(lc2pkpi_problem_sets)
Show code cell source
dot = io.asdot(lc2pkpi_reaction, collapse_graphs=True)
graphviz.Source(dot)
Select intermediate particles#
Great! Now we selected only the strongest contributions. Be aware, though, that there are more effects that can suppress certain decays, like small branching ratios. In this example, the initial state \(J/\Psi\) can decay into \(\pi^0 + \rho^0\) or \(\pi^0 + \omega\).
decay |
branching ratio |
---|---|
\(\omega\to\gamma+\pi^0\) |
0.0828 |
\(\rho^0\to\gamma+\pi^0\) |
0.0006 |
Unfortunately, the \(\rho^0\) mainly decays into \(\pi^0+\pi^0\), not \(\gamma+\pi^0\) and is therefore suppressed. This information is currently not known to qrules
, but it is possible to hand qrules
a list of allowed intermediate states,
stm.set_allowed_intermediate_particles(["f(0)", "f(2)"])
reaction = stm.find_solutions(problem_sets)
reaction.get_intermediate_particles().names
Show code cell output
['f(0)(500)',
'f(0)(980)',
'f(2)(1270)',
'f(0)(1370)',
"f(2)'(1525)",
'f(0)(1500)',
'f(0)(1710)',
'f(2)(1950)',
'f(0)(2020)',
'f(2)(2010)',
'f(2)(2300)',
'f(2)(2340)']
or, using regular expressions,
stm.set_allowed_intermediate_particles(r"f\([02]\)", regex=True)
reaction = stm.find_solutions(problem_sets)
assert len(reaction.get_intermediate_particles().names) == 12
Now we have selected all amplitudes that involve f states:
Show code cell source
dot = io.asdot(reaction, collapse_graphs=True, render_node=False)
graphviz.Source(dot)
See also
4. Export generated transitions#
The ReactionInfo
, MutableTransition
, and Topology
can be serialized to and from a dict
with io.asdict()
and io.fromdict()
:
io.asdict(reaction.transitions[0].topology)
{'nodes': frozenset({0, 1}),
'edges': {-1: {'ending_node_id': 0},
3: {'originating_node_id': 0, 'ending_node_id': 1},
0: {'originating_node_id': 0},
1: {'originating_node_id': 1},
2: {'originating_node_id': 1}}}
This also means that the ReactionInfo
can be written to JSON or YAML format with io.write()
and loaded again with io.load()
:
io.write(reaction, "transitions.json")
imported_reaction = io.load("transitions.json")
assert imported_reaction == reaction
Handy if it takes a lot of computation time to re-generate the transitions!
Tip
The ampform
package can formulate amplitude models based on the state transitions created by qrules
. See Formulate amplitude model.
Particle database#
In PWA, you usually want to search for special resonances, possibly even some not listed in the PDG. In this notebook, we go through a few ways to add or overwrite Particle
instances in the database with your own particle definitions.
Loading the default database#
In Generate transitions, we made use of the StateTransitionManager
. By default, if you do not specify the particle_db
argument, the StateTransitionManager
calls the function load_default_particles()
. This functions returns a ParticleCollection
instance with Particle
definitions from the PDG, along with additional definitions that are provided in the file additional_definitions.yml
.
Here, we call this method directly to illustrate what happens (we use load_pdg()
, which loads a subset):
from qrules.particle import load_pdg
particle_db = load_pdg()
print("Number of loaded particles:", len(particle_db))
Number of loaded particles: 537
In the following, we illustrate how to use the methods of the ParticleCollection
class to find and ‘modify’ Particle
s and add()
them back to the ParticleCollection
.
Finding particles#
The ParticleCollection
class offers some methods to search for particles by name or by PID (see find()
):
particle_db.find(333)
Particle(
name='phi(1020)',
pid=333,
latex='\\phi(1020)',
spin=1.0,
mass=1.019461,
width=0.004248999999999999,
isospin=Spin(0, 0),
parity=-1,
c_parity=-1,
g_parity=-1,
)
With filter()
, you can perform more sophisticated searches. This is done by either passing a function or lambda.
subset = particle_db.filter(lambda p: p.name.startswith("f(2)"))
subset.names
['f(2)(1270)',
"f(2)'(1525)",
'f(2)(1950)',
'f(2)(2010)',
'f(2)(2300)',
'f(2)(2340)']
subset = particle_db.filter(
lambda p: p.strangeness == 1 and p.spin >= 1 and p.mass > 1.8 and p.mass < 1.9
)
subset.names
['K(2)(1820)0',
'K(2)(1820)+',
'Lambda(1820)~',
'Lambda(1830)~',
'Lambda(1890)~']
subset = particle_db.filter(lambda p: p.is_lepton())
subset.names
['e-',
'e+',
'mu-',
'mu+',
'nu(e)',
'nu(tau)~',
'nu(tau)',
'nu(e)~',
'nu(mu)~',
'nu(mu)',
'tau-',
'tau+']
Note that in each of these examples, we call the names
property. This is just to only display the names, sorted alphabetically, otherwise the output becomes a bit of a mess:
particle_db.filter(lambda p: p.name.startswith("pi") and len(p.name) == 3)
ParticleCollection({
Particle(
name='pi-',
pid=-211,
latex='\\pi^{-}',
spin=0.0,
mass=0.13957039000000002,
width=2.5284e-17,
charge=-1,
isospin=Spin(1, -1),
parity=-1,
g_parity=-1,
),
Particle(
name='pi+',
pid=211,
latex='\\pi^{+}',
spin=0.0,
mass=0.13957039000000002,
width=2.5284e-17,
charge=1,
isospin=Spin(1, +1),
parity=-1,
g_parity=-1,
),
Particle(
name='pi0',
pid=111,
latex='\\pi^{0}',
spin=0.0,
mass=0.1349768,
width=7.81e-09,
isospin=Spin(1, 0),
parity=-1,
c_parity=+1,
g_parity=-1,
),
})
LaTeX representation#
Particle
s also contain a latex
tag. Here, we use ipython to render them nicely as mathematical symbols:
from IPython.display import Math
sigmas = particle_db.filter(lambda p: p.name.startswith("Sigma") and p.charmness == 1)
Math(", ".join(p.latex for p in sigmas))
Adding custom particle definitions through Python#
A quick way to modify or overwrite particles, is through your Python script or notebook. Notice that the instances in the database are Particle
instances:
N1650_plus = particle_db["N(1650)+"]
N1650_plus
Particle(
name='N(1650)+',
pid=32212,
latex='N(1650)^{+}',
spin=0.5,
mass=1.65,
width=0.125,
charge=1,
isospin=Spin(1/2, +1/2),
baryon_number=1,
parity=-1,
)
The instances in the database are immutable. Therefore, if you want to modify, say, the width, you have to create a new Particle
instance from the particle you want to modify and add()
it back to the database. You can do this with create_particle()
:
from qrules.particle import create_particle
new_N1650_plus = create_particle(
template_particle=N1650_plus, name="Modified N(1650)+", width=0.2
)
particle_db.add(new_N1650_plus)
particle_db["Modified N(1650)+"].width
0.2
You often also want to add the antiparticle of the particle you modified to the database. Using create_antiparticle()
, it is easy to create the corresponding antiparticle object.
from qrules.particle import create_antiparticle
new_N1650_minus = create_antiparticle(new_N1650_plus, new_name="Modified N(1650)-")
particle_db.add(new_N1650_minus)
particle_db["Modified N(1650)-"]
Particle(
name='Modified N(1650)-',
pid=-32212,
latex='\\overline{N(1650)^{+}}',
spin=0.5,
mass=1.65,
width=0.2,
charge=-1,
isospin=Spin(1/2, -1/2),
baryon_number=-1,
parity=+1,
)
When adding additional particles you may need for your research, it is easiest to work with an existing particle as template. Let’s say we want to study \(e^+e^-\) collisions of several energies:
energies_mev = {4180, 4220, 4420, 4600}
template_particle = particle_db["J/psi(1S)"]
for energy_mev in energies_mev:
energy_gev = energy_mev / 1e3
new_particle = create_particle(
template_particle,
name=f"EpEm ({energy_mev} MeV)",
mass=energy_gev,
)
particle_db.add(new_particle)
len(particle_db)
543
particle_db.filter(lambda p: "EpEm" in p.name).names
['EpEm (4180 MeV)', 'EpEm (4220 MeV)', 'EpEm (4420 MeV)', 'EpEm (4600 MeV)']
Of course, it’s also possible to add any kind of custom Particle
, as long as its quantum numbers comply with the gellmann_nishijima()
rule:
from qrules.particle import Particle
custom = Particle(
name="custom",
pid=99999,
latex=R"p_\mathrm{custom}",
spin=1.0,
mass=1,
charge=1,
isospin=(1.5, 0.5),
charmness=1,
)
custom
Particle(
name='custom',
pid=99999,
latex='p_\\mathrm{custom}',
spin=1.0,
mass=1.0,
charge=1,
isospin=Spin(3/2, +1/2),
charmness=1,
)
particle_db += custom
len(particle_db)
544
Loading custom definitions from a YAML file#
It’s also possible to add particles from a config file, with io.load()
. Existing entries remain and if the imported file of particle definitions contains a particle with the same name, it is overwritten in the database.
It’s easiest to work with YAML. Here, we use the provided additional_particles.yml
example file:
from qrules import io
particle_db += io.load("additional_particles.yml")
Writing to YAML#
You can also dump the existing particle lists to YAML. You do this with the io.write()
function.
io.write(instance=particle_db, filename="dumped_particle_list.yaml")
Note that the function write
can dump any ParticleCollection
to an output file, also a specific subset.
from qrules.particle import ParticleCollection
output = ParticleCollection()
output += particle_db["J/psi(1S)"]
output += particle_db.find(22) # gamma
output += particle_db.filter(lambda p: p.name.startswith("f(0)"))
output += particle_db["pi0"]
output += particle_db["pi+"]
output += particle_db["pi-"]
output += particle_db["custom"]
io.write(output, "particle_list_selection.yml")
output.names
['custom',
'f(0)(500)',
'f(0)(980)',
'f(0)(1370)',
'f(0)(1500)',
'f(0)(1710)',
'f(0)(2020)',
'gamma',
'J/psi(1S)',
'pi0',
'pi-',
'pi+']
As a side note, qrules
provides JSON schemas (reaction/particle-validation.json
) to validate your particle list files (see also jsonschema.validators.validate()
). If you have installed qrules
as an Editable installation and use VSCode, your YAML particle list are checked automatically in the GUI.
Visualize solutions#
The io
module allows you to convert MutableTransition
, Topology
instances, and ProblemSet
s to DOT language with asdot()
. You can visualize its output with third-party libraries, such as Graphviz. This is particularly useful after running find_solutions()
, which produces a ReactionInfo
object with a list
of MutableTransition
instances (see Generate transitions).
Topologies#
First of all, here are is an example of how to visualize a group of Topology
instances. We use create_isobar_topologies()
and create_n_body_topology()
to create a few standard topologies.
Show code cell content
import graphviz
from IPython.display import display
import qrules
from qrules.particle import Spin
from qrules.topology import create_isobar_topologies, create_n_body_topology
from qrules.transition import State
topology = create_n_body_topology(2, 4)
graphviz.Source(qrules.io.asdot(topology, render_initial_state_id=True))
Note the IDs of the nodes
is also rendered if there is more than node:
topologies = create_isobar_topologies(4)
graphviz.Source(qrules.io.asdot(topologies))
This can be turned on or off with the arguments of asdot()
:
topologies = create_isobar_topologies(3)
graphviz.Source(qrules.io.asdot(topologies, render_node=False))
asdot()
provides other options as well:
topologies = create_isobar_topologies(5)
dot = qrules.io.asdot(
topologies[0],
render_final_state_id=False,
render_resonance_id=True,
render_node=False,
)
display(graphviz.Source(dot))
ProblemSet
s#
As noted in Generate transitions, the StateTransitionManager
provides more control than the façade function generate_transitions()
. One advantages, is that the StateTransitionManager
first generates a set of ProblemSet
s with create_problem_sets()
that you can further configure if you wish.
from qrules.settings import InteractionType
stm = qrules.StateTransitionManager(
initial_state=["J/psi(1S)"],
final_state=["K0", "Sigma+", "p~"],
formalism="canonical-helicity",
)
stm.set_allowed_interaction_types([InteractionType.STRONG, InteractionType.EM])
problem_sets = stm.create_problem_sets()
Note that the output of create_problem_sets()
is a dict
with float
values as keys (representing the interaction strength) and list
s of ProblemSet
s as values.
sorted(problem_sets, reverse=True)
[3600.0, 60.0, 1.0]
problem_set = problem_sets[60.0][0]
dot = qrules.io.asdot(problem_set, render_node=True)
graphviz.Source(dot)
Quantum number solutions#
As noted in 3. Find solutions, a ProblemSet
can be fed to StateTransitionManager.find_solutions()
directly to get a ReactionInfo
object. ReactionInfo
is a final result that consists of Particle
s, but in the intermediate steps, QRules works with sets of quantum numbers. One can inspect these intermediate generated quantum numbers by using find_quantum_number_transitions()
and inspecting is output. Note that the resulting object is again a dict
with strengths as keys and a list of solution as values.
qn_solutions = stm.find_quantum_number_transitions(problem_sets)
{strength: len(values) for strength, values in qn_solutions.items()}
{3600.0: 36, 60.0: 72, 1.0: 36}
The list of solutions consist of a tuple
of a QNProblemSet
(compare ProblemSets) and a QNResult
:
strong_qn_solutions = qn_solutions[3600.0]
qn_problem_set, qn_result = strong_qn_solutions[0]
Show code cell source
dot = qrules.io.asdot(qn_problem_set, render_node=True)
graphviz.Source(dot)
Show code cell source
dot = qrules.io.asdot(qn_result, render_node=True)
graphviz.Source(dot)
StateTransition
s#
After finding the Quantum number solutions, QRules finds Particle
definitions that match these quantum numbers. All these steps are hidden in the convenience functions StateTransitionManager.find_solutions()
and generate_transitions()
. In the following, we’ll visualize the allowed transitions for the decay \(\psi' \to \gamma\eta\eta\) as an example.
import qrules
reaction = qrules.generate_transitions(
initial_state="psi(2S)",
final_state=["gamma", "eta", "eta"],
allowed_interaction_types="EM",
)
As noted in 3. Find solutions, the transitions
contain all spin projection combinations (which is necessary for the ampform
package). It is possible to convert all these solutions to DOT language with asdot()
. To avoid visualizing all solutions, we just take a subset of the transitions
:
dot = qrules.io.asdot(reaction.transitions[::50][:3]) # just some selection
This str
of DOT language for the list of MutableTransition
instances can then be visualized with a third-party library, for instance, with graphviz.Source
:
import graphviz
dot = qrules.io.asdot(
reaction.transitions[::50][:3], render_node=False
) # just some selection
graphviz.Source(dot)
You can also serialize the DOT string to file with io.write()
. The file extension for a DOT file is .gv
:
qrules.io.write(reaction, "decay_topologies_with_spin.gv")
Collapse graphs#
Since this list of all possible spin projections transitions
is rather long, it is often useful to use strip_spin=True
or collapse_graphs=True
to bundle comparable graphs. First, strip_spin=True
allows one collapse (ignore) the spin projections (we again show a selection only):
dot = qrules.io.asdot(reaction.transitions[:3], strip_spin=True)
graphviz.Source(dot)
or, with stripped node properties:
dot = qrules.io.asdot(reaction.transitions[:3], strip_spin=True, render_node=True)
graphviz.Source(dot)
Note
By default, asdot()
renders edge IDs, because they represent the (final) state IDs as well. In the example above, we switched this off.
If that list is still too much, there is collapse_graphs=True
, which bundles all graphs with the same final state groupings:
dot = qrules.io.asdot(reaction, collapse_graphs=True, render_node=False)
graphviz.Source(dot)
Other state renderings#
The convert()
method makes it possible to convert the types of its states
. This for instance allows us to only render the spin states on in a Transition
:
spin_transitions = sorted({
t.convert(lambda s: Spin(s.particle.spin, s.spin_projection))
for t in reaction.transitions
})
some_selection = spin_transitions[::67][:3]
dot = qrules.io.asdot(some_selection, render_node=True)
graphviz.Source(dot)
Or any other properties of a State
:
Show code cell source
def render_mass(state: State, digits: int = 3) -> str:
mass = round(state.particle.mass, digits)
width = round(state.particle.width, digits)
if width == 0:
return str(mass)
return f"{mass}±{width}"
mass_transitions = sorted({
t.convert(
state_converter=render_mass,
interaction_converter=lambda _: None,
)
for t in reaction.transitions
})
dot = qrules.io.asdot(mass_transitions[::10])
graphviz.Source(dot)
Styling#
The asdot()
function also takes Graphviz attributes. These can be used to modify the layout of the whole figure. Examples are the size
, color
, and fontcolor
. Edges and nodes can be styled with edge_style
and node_style
respectively:
dot = qrules.io.asdot(
reaction.transitions[0],
render_node=True,
size=12,
bgcolor="white",
edge_style={
"color": "red",
"arrowhead": "open",
"fontcolor": "blue",
"fontsize": 25,
},
node_style={
"color": "gray",
"penwidth": 2,
"shape": "ellipse",
"style": "dashed",
},
)
display(graphviz.Source(dot))
Conservation rules#
Show code cell content
import attrs
import graphviz
from IPython.display import display
import qrules
from qrules.conservation_rules import (
SpinEdgeInput,
SpinNodeInput,
parity_conservation,
spin_conservation,
spin_magnitude_conservation,
)
from qrules.quantum_numbers import Parity
QRules generates MutableTransition
s, populates them with quantum numbers (edge properties representing states and nodes properties representing interactions), then checks whether the generated MutableTransition
s comply with the rules formulated in the conservation_rules
module.
The conservation_rules
module can also be used separately. In this notebook, we will illustrate this by checking spin and parity conservation.
Parity conservation#
parity_conservation(
ingoing_edge_qns=[Parity(-1)],
outgoing_edge_qns=[Parity(+1), Parity(+1)],
l_magnitude=1,
)
True
Spin conservation#
See also
spin_conservation()
, tests/unit/conservation_rules/test_spin.py
, PDG2020, §Quark Model, and these lecture notes by Curtis Meyer.
spin_conservation()
checks whether spin magnitude and spin projections are conserved. In addition, it checks whether the Clebsch-Gordan coefficients are non-zero, meaning that the coupled spins on the interaction nodes are valid as well.
No spin and angular momentum#
spin_conservation(
ingoing_spins=[
SpinEdgeInput(0, 0),
],
outgoing_spins=[
SpinEdgeInput(0, 0),
SpinEdgeInput(0, 0),
],
interaction_qns=SpinNodeInput(
l_magnitude=0, # false if 1
l_projection=0,
s_magnitude=0,
s_projection=0,
),
)
True
Non-zero example#
spin_conservation(
ingoing_spins=[
SpinEdgeInput(1, 0),
],
outgoing_spins=[
SpinEdgeInput(1, +1),
SpinEdgeInput(1, -1),
],
interaction_qns=SpinNodeInput(
l_magnitude=1,
l_projection=0,
s_magnitude=2,
s_projection=0,
),
)
True
Example with a StateTransition
#
First, generate some StateTransition
s with generate_transitions()
, then select one of them:
reaction = qrules.generate_transitions(
initial_state="J/psi(1S)",
final_state=["K0", "Sigma+", "p~"],
allowed_interaction_types="strong",
formalism="canonical",
)
transition = reaction.transitions[0]
Next, have a look at the edge and node properties, and use the underlying Topology
to extract one of the node InteractionProperties
with the surrounding states (these are tuple
s of a Particle
and a float
spin projection).
Show code cell source
dot = qrules.io.asdot(transition, render_node=True)
display(graphviz.Source(dot))
dot = qrules.io.asdot(
transition.topology,
render_node=True,
render_resonance_id=True,
render_initial_state_id=True,
)
display(graphviz.Source(dot))
We select node \((0)\), which has incoming state ID \(-1\) and outgoing state IDs \(0\) and \(3\):
topology = transition.topology
node_id = 0
in_id, *_ = topology.get_edge_ids_ingoing_to_node(node_id)
out_id1, out_id2, *_ = topology.get_edge_ids_outgoing_from_node(node_id)
incoming_state = transition.states[in_id]
outgoing_state1 = transition.states[out_id1]
outgoing_state2 = transition.states[out_id2]
interaction = transition.interactions[node_id]
spin_magnitude_conservation(
ingoing_spins=[
SpinEdgeInput(
spin_magnitude=incoming_state.particle.spin,
spin_projection=incoming_state.spin_projection,
)
],
outgoing_spins=[
SpinEdgeInput(
spin_magnitude=outgoing_state1.particle.spin,
spin_projection=outgoing_state1.spin_projection,
),
SpinEdgeInput(
spin_magnitude=outgoing_state2.particle.spin,
spin_projection=outgoing_state2.spin_projection,
),
],
interaction_qns=interaction,
)
True
Contrary to expectations, this transition does not conserve spin projection and therefore spin_conservation()
returns False
:
spin_conservation(
ingoing_spins=[
SpinEdgeInput(
spin_magnitude=incoming_state.particle.spin,
spin_projection=incoming_state.spin_projection,
)
],
outgoing_spins=[
SpinEdgeInput(
spin_magnitude=outgoing_state1.particle.spin,
spin_projection=outgoing_state1.spin_projection,
),
SpinEdgeInput(
spin_magnitude=outgoing_state2.particle.spin,
spin_projection=outgoing_state2.spin_projection,
),
],
interaction_qns=interaction,
)
False
The reason is that AmpForm formulates the HelicityModel
with the helicity formalism first and then uses a transformation to get the model in the canonical basis (see formulate_clebsch_gordan_coefficients()
). The canonical basis does not conserve helicity (taken to be State.spin_projection
).
Modifying StateTransition
s#
When checking conservation rules, you may want to modify the properties on the StateTransition
s. However, a StateTransition
is a FrozenTransition
, so it is not possible to modify its interactions
and states
. The only way around this is to create a new instance with attrs.evolve()
.
First, we get the instance (in this case one of the InteractionProperties
) and substitute its InteractionProperties.l_magnitude
:
new_interaction = attrs.evolve(transition.interactions[node_id], l_magnitude=2)
new_interaction
InteractionProperties(
l_magnitude=2,
l_projection=None,
s_magnitude=1.0,
s_projection=None,
parity_prefactor=None,
)
We then again use attrs.evolve()
to substitute the Transition.interactions
of the original StateTransition
:
new_interaction_dict = dict(transition.interactions) # make mutable
new_interaction_dict[node_id] = new_interaction
new_transition = attrs.evolve(transition, interactions=new_interaction_dict)
Show code cell source
dot = qrules.io.asdot(new_transition, render_node=True)
graphviz.Source(dot)
Custom topologies#
As illustrated in Generate transitions, the StateTransitionManager
offers you a bit more flexibility than the façade function generate_transitions()
used in the main Usage page. In this notebook, we go one step further, by specifying a custom Topology
via StateTransitionManager.topologies
.
Show code cell content
import graphviz
import qrules
from qrules import InteractionType, StateTransitionManager
from qrules.topology import Edge, Topology
2-to-2 topology#
As a simple example, we start with a 2-to-2 scattering topology. We define it as follows:
topology = Topology(
nodes=range(2),
edges=enumerate(
[
Edge(None, 0),
Edge(None, 0),
Edge(1, None),
Edge(1, None),
Edge(0, 1),
],
-2,
),
)
Show code cell source
dot = qrules.io.asdot(
topology,
render_resonance_id=True,
render_node=True,
render_initial_state_id=True,
)
graphviz.Source(dot)
First, we construct a StateTransitionManager
for the transition \(K^-K^+ \to \pi^+\pi^-\). The constructed Topology
can then be inserted via its topologies
attribute:
stm = StateTransitionManager(
initial_state=["K-", "K+"],
final_state=["pi-", "pi+"],
formalism="canonical",
)
stm.set_allowed_interaction_types([InteractionType.STRONG, InteractionType.EM])
stm.topologies = (topology,) # tuple is immutable
For the rest, the process is just the same as in Generate transitions:
problem_sets = stm.create_problem_sets()
reaction_kk = stm.find_solutions(problem_sets)
Show code cell source
dot = qrules.io.asdot(reaction_kk, collapse_graphs=True)
graphviz.Source(dot)
Warning
It is not yet possible to give the initial state a certain energy. So some collider process like \(e^-e^+\to\pi^+\pi\) does not result in a large number of resonances.
stm.initial_state = ["e-", "e+"]
problem_sets = stm.create_problem_sets()
reaction_ep = stm.find_solutions(problem_sets)
Show code cell source
dot = qrules.io.asdot(reaction_ep, collapse_graphs=True)
graphviz.Source(dot)
What can do at most, is switch off MassConservation
, either through the constructor of the StateTransitionManager
, or by modifying ProblemSet
.
stm = StateTransitionManager(
initial_state=["e-", "e+"],
final_state=["pi-", "pi+"],
formalism="canonical",
mass_conservation_factor=None,
)
stm.set_allowed_interaction_types([InteractionType.STRONG, InteractionType.EM])
stm.topologies = [topology]
problem_sets = stm.create_problem_sets()
reaction_ep_no_mass = stm.find_solutions(problem_sets)
Show code cell source
dot = qrules.io.asdot(reaction_ep_no_mass, collapse_graphs=True)
graphviz.Source(dot)
LS-couplings#
The spin_conservation()
rule is one of the more complicated checks in the conservation_rules
module. It provides an implementation of \(LS\)-couplings, which is a procedure to determine which values for total angular momentum \(L\) and coupled spin \(S\) are allowed in an interaction node. In this notebook, we illustrate this procedure with the following decay chain as an example:
In this decay chain, there are two decay nodes that we investigate separately. In addition, both decays are mediated interactions by the strong force, which means there is also parity conservation.
In the following derivations, the Particle.spin
and Particle.parity
values are of importance:
Show code cell source
from IPython.display import Math
import qrules
PDG = qrules.load_pdg()
particle_names = [
"J/psi(1S)",
"Sigma+",
"Sigma(1670)~-",
"p~",
"K0",
]
latex_expressions = []
for name in particle_names:
particle = PDG[name]
parity = "+" if particle.parity > 0 else "-"
if particle.spin.is_integer():
spin = int(particle.spin)
else:
nominator = int(particle.spin * 2)
spin = Rf"\tfrac{{{nominator}}}{2}"
latex_expressions.append(f"{particle.latex}={spin}^{parity}")
Math(R"\qquad ".join(latex_expressions))
Procedure#
Imagine we have a two-body decay of \(p_0\rightarrow p_1p_2\). We denote the Spin.magnitude
of each particle \(p_i\) as \(s_i\) and their parity
as \(\eta_i\). The values for \(L\) and \(S\) can now be determined as follows:
Determine all values for \(S\) that satisfy \(\left| s_1-s_2 \right| \le S \le s_1+s_2\). The difference between each value for \(S\) has to integer, so \(S = \left| s_1-s_2 \right|, \left| s_1-s_2 \right|+1, \dots, s_1+s_2\).
Determine all values for \(L\) that satisfy \(\left| L-S \right| \le s_0 \le L+S\), with \(L\) being a non-negative integer.
If there is parity conservation, \(L\) has to satisfy an additional constraint: \(\eta_0 = \eta_1\cdot\eta_2\cdot(-1)^L\).
\(J/\psi \to \Sigma^+\bar\Sigma(1670)^-\)#
The spin and parity of each particle in the first transition can be summarized as \(1^-\to\frac{1}{2}^+\frac{3}{2}^+\). Following step 1 in the procedure, we get:
Next, we determine the allowed total angular momentum values \(L\) with step 2:
So in total, we have 6 \(LS\)-combinations:
This decay however goes via the strong force. This means that parity has to be conserved and we have to follow step 3:
From this, we can easily see that only odd \(L\) values are possible, which leaves us with 3 \(LS\)-combinations:
\(\bar \Sigma(1670)^-\to \bar pK^0\)#
The second part of the decay chain can be expressed as \(\frac{3}{2}^+ \to \frac{1}{2}^- 0^-\). Following step 1, we see:
This time, only one coupled spin value is allowed. That allows for the following values of \(L\):
By now, only two \(LS\)-combinations are possible:
This again is a strong interaction, which means we have to check for parity conservation.
Again, it is clear that only even \(L\)’s are allowed. This means that only one \(LS\)-combination is possible:
Check with QRules#
Finally, let’s use generate_transitions()
to check whether the allowed \(LS\)-couplings are found by qrules
as well. Note that we have to increase the maximum angular momentum to find the \((L,S)=(3,2)\) combination as well.
Show code cell source
import logging
import graphviz
LOGGER = logging.getLogger()
LOGGER.setLevel(logging.ERROR)
reaction = qrules.generate_transitions(
initial_state="J/psi(1S)",
final_state=["K0", "Sigma+", "p~"],
allowed_intermediate_particles=["Sigma(1670)"],
allowed_interaction_types="strong",
max_angular_momentum=3,
)
dot = qrules.io.asdot(reaction, render_node=True, strip_spin=True)
graphviz.Source(dot)
Bibliography#
Tip
Download this bibliography as BibTeX here
.
D. M. Asner. Dalitz Plot Analysis Formalism. In Review of Particle Physics: Volume I Reviews. January 2006. pdg.lbl.gov/2010/reviews/rpp2010-rev-dalitz-analysis-formalism.pdf.
S.-U. Chung et al. Partial wave analysis in 𝐾-matrix formalism. Annalen der Physik, 507(5):404–430, May 1995. doi:10.1002/andp.19955070504.
qrules#
import qrules
A rule based system that facilitates particle reaction analysis.
QRules generates allowed particle transitions from a set of conservation rules and boundary conditions as specified by the user. The user boundary conditions for a particle reaction problem are for example the initial state, final state, and allowed interactions.
The core of qrules
computes which transitions (represented by a MutableTransition
)
are allowed between a certain initial and final state. Internally, the system propagates
the quantum numbers defined by the particle
module through the MutableTransition
,
while satisfying the rules define by the conservation_rules
module. See
Generate transitions and Particle database.
Finally, the io
module provides tools that can read and write the objects of this
framework.
- check_reaction_violations(initial_state: str | Tuple[str, Sequence[float]] | Sequence[str | Tuple[str, Sequence[float]]], final_state: Sequence[str | Tuple[str, Sequence[float]]], mass_conservation_factor: float | None = 3.0, particle_db: ParticleCollection | None = None, max_angular_momentum: int = 1, max_spin_magnitude: float = 2.0) Set[FrozenSet[str]] [source]#
Determine violated interaction rules for a given particle reaction.
Warning
This function only guarantees to find P, C and G parity violations, if it’s a two body decay. If all initial and final states have the C/G parity defined, then these violations are also determined correctly.
- Parameters:
initial_state – Shortform description of the initial state w/o spin projections.
final_state – Shortform description of the final state w/o spin projections.
mass_conservation_factor – Factor with which the width is multiplied when checking for
MassConservation
. Set toNone
in order to deactivate mass conservation.particle_db (Optional) – Custom
ParticleCollection
object. Defaults to theParticleCollection
returned byload_pdg
.max_angular_momentum – Maximum angular momentum over which to generate \(LS\)-couplings.
max_spin_magnitude – Maximum spin magnitude over which to generate \(LS\)-couplings.
- Returns:
Set of least violating rules. The set can have multiple entries, as several quantum numbers can be violated. Each entry in the frozenset represents a group of rules that together violate all possible quantum number configurations.
Example
>>> import qrules >>> qrules.check_reaction_violations( ... initial_state="pi0", ... final_state=["gamma", "gamma", "gamma"], ... ) {frozenset({'c_parity_conservation'})}
See also
- generate_transitions(initial_state: str | Tuple[str, Sequence[float]] | Sequence[str | Tuple[str, Sequence[float]]], final_state: Sequence[str | Tuple[str, Sequence[float]]], allowed_intermediate_particles: List[str] | None = None, allowed_interaction_types: str | Iterable[str] | None = None, formalism: str = 'canonical-helicity', particle_db: ParticleCollection | None = None, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 2, max_spin_magnitude: float = 2.0, topology_building: str = 'isobar', number_of_threads: int | None = None) ReactionInfo [source]#
Generate allowed transitions between an initial and final state.
Serves as a facade to the
StateTransitionManager
(see Generate transitions).- Parameters:
initial_state (list) – A list of particle names in the initial state. You can specify spin projections for these particles with a
tuple
, e.g.("J/psi(1S)", [-1, 0, +1])
. If spin projections are not specified, all projections are taken, so the example here would be equivalent to"J/psi(1S)"
.final_state (list) – Same as
initial_state
, but for final state particles.allowed_intermediate_particles (
list
, optional) – A list of particle states that you want to allow as intermediate states. This helps (1) filter out resonances and (2) speed up computation time.allowed_interaction_types – Interaction types you want to consider. For instance,
["s", "em"]
results inEM
andSTRONG
and["strong"]
results inSTRONG
.formalism (
str
, optional) – Formalism that you intend to use in the eventual amplitude model.particle_db (
ParticleCollection
, optional) – The particles that you want to be involved in the reaction. Usesload_pdg
by default. It’s better to use a subset for larger reactions, because of the computation times. This argument is especially useful when you want to use your own particle definitions (see Particle database).mass_conservation_factor – Width factor that is taken into account for for the
MassConservation
rule.max_angular_momentum – Maximum angular momentum over which to generate angular momenta.
max_spin_magnitude – Maximum spin magnitude over which to generate spins.
topology_building (str) –
Technique with which to build the
Topology
instances. Allowed values are:"isobar"
: Isobar model (each state decays into two states)"nbody"
: Use one central node and connect initial and final states to it
number_of_threads – Number of cores with which to compute the allowed transitions. Defaults to the current value returned by
settings.NumberOfThreads.get()
.
An example (where, for illustrative purposes only, we specify all arguments) would be:
>>> import qrules >>> reaction = qrules.generate_transitions( ... initial_state="D0", ... final_state=["K~0", "K+", "K-"], ... allowed_intermediate_particles=["a(0)(980)", "a(2)(1320)-"], ... allowed_interaction_types=["e", "w"], ... formalism="helicity", ... particle_db=qrules.load_pdg(), ... topology_building="isobar", ... ) >>> len(reaction.transitions) 4 >>> len(reaction.group_by_topology()) 3
- load_default_particles() ParticleCollection [source]#
Load the default particle list that comes with
qrules
.Runs
load_pdg
and supplements its output definitions from the fileadditional_definitions.yml
.
Submodules and Subpackages
io#
import qrules.io
Serialization module for the qrules
.
The io
module provides tools to export or import objects from qrules
to and from
disk, so that they can be used by external packages, or just to store (cache) the state
of the system.
- asdot(instance: object, *, render_node: bool | None = None, render_final_state_id: bool = True, render_resonance_id: bool = False, render_initial_state_id: bool = False, strip_spin: bool = False, collapse_graphs: bool = False, edge_style: Dict[str, Any] | None = None, node_style: Dict[str, Any] | None = None, **figure_style: Any) str [source]#
Convert a
object
to a DOT languagestr
.Only works for objects that can be represented as a graph, particularly a
MutableTransition
or alist
ofMutableTransition
instances.- Parameters:
instance – the input
object
that is to be rendered as DOT (graphviz) language.strip_spin – Normally, each
MutableTransition
has aParticle
with a spin projection on its edges. This option hides the projections, leaving onlyParticle
names on edges.collapse_graphs – Group all transitions by equivalent kinematic topology and combine all allowed particles on each edge.
render_node –
Whether or not to render node ID (in the case of a
Topology
) and/or node properties (in the case of aMutableTransition
). Meaning of the labels:\(P\): parity prefactor
\(s\): tuple of coupled spin magnitude and its projection
\(l\): tuple of angular momentum and its projection
See
InteractionProperties
for more info.render_final_state_id – Add edge IDs for the final state edges.
render_resonance_id – Add edge IDs for the intermediate state edges.
render_initial_state_id – Add edge IDs for the initial state edges.
edge_style – Styling of a Graphviz edge.
node_style – Styling of a Graphviz node.
figure_style – Styling of the whole figure.
See also
See Graphviz attributes for the available styling arguments.
See also
- class JSONSetEncoder(*, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None)[source]#
Bases:
JSONEncoder
JSONEncoder
that supportsset
andfrozenset
.>>> import json >>> instance = {"val1": {1, 2, 3}, "val2": frozenset({2, 3, 4, 5})} >>> json.dumps(instance, cls=JSONSetEncoder) '{"val1": [1, 2, 3], "val2": [2, 3, 4, 5]}'
- default(o: Any) Any [source]#
Implement this method in a subclass such that it returns a serializable object for
o
, or calls the base implementation (to raise aTypeError
).For example, to support arbitrary iterators, you could implement default like this:
def default(self, o): try: iterable = iter(o) except TypeError: pass else: return list(iterable) # Let the base class default method raise the TypeError return JSONEncoder.default(self, o)
argument_handling#
import qrules.argument_handling
Handles argument handling for rules.
Responsibilities are the check of requirements for rules and the creation of the arguments from general graph property maps. The information is extracted from the type annotations of the rules.
- class RuleArgumentHandler[source]#
Bases:
object
- register_rule(rule: GraphElementRule | EdgeQNConservationRule | ConservationRule) Tuple[Callable, Callable] [source]#
- get_required_qns(rule: GraphElementRule | EdgeQNConservationRule | ConservationRule) Tuple[Set[Type[pid | mass | width | spin_magnitude | spin_projection | charge | isospin_magnitude | isospin_projection | strangeness | charmness | bottomness | topness | baryon_number | electron_lepton_number | muon_lepton_number | tau_lepton_number | parity | c_parity | g_parity]], Set[Type[l_magnitude | l_projection | s_magnitude | s_projection | parity_prefactor]]] [source]#
combinatorics#
import qrules.combinatorics
Perform permutations on the edges of a MutableTransition
.
In a MutableTransition
, the edges represent quantum states, while the nodes represent
interactions. This module provides tools to permutate, modify or extract these edge and
node properties.
- InitialFacts: TypeAlias = 'MutableTransition[ParticleWithSpin, InteractionProperties]'#
A
Transition
with only initial and final state information.
- create_initial_facts(topology: Topology, initial_state: Sequence[str | Tuple[str, Sequence[float]]], final_state: Sequence[str | Tuple[str, Sequence[float]]], particle_db: ParticleCollection) List[MutableTransition[Tuple[Particle, float], InteractionProperties]] [source]#
- permutate_topology_kinematically(topology: Topology, initial_state: List[str | Tuple[str, Sequence[float]]], final_state: List[str | Tuple[str, Sequence[float]]], final_state_groupings: List[List[List[str]]] | List[List[str]] | List[str] | None = None) List[Topology] [source]#
- match_external_edges(graphs: List[MutableTransition[Tuple[Particle, float], InteractionProperties]]) None [source]#
- perform_external_edge_identical_particle_combinatorics(graph: MutableTransition) List[MutableTransition] [source]#
Create combinatorics clones of the
MutableTransition
.In case of identical particles in the initial or final state. Only identical particles, which do not enter or exit the same node allow for combinatorics!
conservation_rules#
import qrules.conservation_rules
Collection of quantum number conservation rules for particle reactions.
This module is the place where the ‘expert’ defines the rules that verify quantum numbers of the reaction.
A rule is a function that takes quantum numbers as input and outputs a boolean. There are three different types of rules:
GraphElementRule
that work on individual graph edges or nodes.EdgeQNConservationRule
that work on the interaction level, which use ingoing edges, outgoing edges as arguments. E.g.:ChargeConservation
.ConservationRule
that work on the interaction level, which use ingoing edges, outgoing edges and a interaction node as arguments. E.g:parity_conservation
.
The arguments can be any type of quantum number. However a rule argument resembling
edges only accepts EdgeQuantumNumbers
. Similarly arguments that
resemble a node only accept NodeQuantumNumbers
. The argument types
do not have to be limited to a single quantum number, but can be a composite (see
CParityEdgeInput
).
Warning
Besides the rule logic itself, a rule also has the responsibility of
stating its run conditions. These run conditions must be stated by
the type annotations of its __call__
method. The type annotations
therefore are not just there for static type checking: they also
carry more information about the rule that is extracted dynamically
by the solving
module.
Generally, the conditions can be separated into two categories:
variable conditions
toplogical conditions
Currently, only variable conditions are being used. Topological conditions could be
created in the form of Tuple
instead of List
.
For additive quantum numbers, the decorator additive_quantum_number_rule
can be used
to automatically generate the appropriate behavior.
The module is therefore strongly typed (both for the reader of the code and for type
checking with mypy). An example is HelicityParityEdgeInput
, which
has been defined to provide type checks on parity_conservation_helicity
.
See also
- additive_quantum_number_rule(quantum_number: type) Callable[[Any], EdgeQNConservationRule] [source]#
Class decorator for creating an additive conservation rule.
Use this decorator to create a
EdgeQNConservationRule
for a quantum number to which an additive conservation rule applies:\[\sum q_{in} = \sum q_{out}\]- Parameters:
quantum_number – Quantum number to which you want to apply the additive conservation check. An example would be
EdgeQuantumNumbers.charge
.
- class ChargeConservation(*args, **kwargs)[source]#
Bases:
EdgeQNConservationRule
Decorated via
additive_quantum_number_rule
.Check for
charge
conservation.
- class BaryonNumberConservation(*args, **kwargs)[source]#
Bases:
EdgeQNConservationRule
Decorated via
additive_quantum_number_rule
.Check for
baryon_number
conservation.- __call__(ingoing_edge_qns: List[baryon_number], outgoing_edge_qns: List[baryon_number]) bool [source]#
Call self as a function.
- class ElectronLNConservation(*args, **kwargs)[source]#
Bases:
EdgeQNConservationRule
Decorated via
additive_quantum_number_rule
.Check for
electron_lepton_number
conservation.- __call__(ingoing_edge_qns: List[electron_lepton_number], outgoing_edge_qns: List[electron_lepton_number]) bool [source]#
Call self as a function.
- class MuonLNConservation(*args, **kwargs)[source]#
Bases:
EdgeQNConservationRule
Decorated via
additive_quantum_number_rule
.Check for
muon_lepton_number
conservation.- __call__(ingoing_edge_qns: List[muon_lepton_number], outgoing_edge_qns: List[muon_lepton_number]) bool [source]#
Call self as a function.
- class TauLNConservation(*args, **kwargs)[source]#
Bases:
EdgeQNConservationRule
Decorated via
additive_quantum_number_rule
.Check for
tau_lepton_number
conservation.- __call__(ingoing_edge_qns: List[tau_lepton_number], outgoing_edge_qns: List[tau_lepton_number]) bool [source]#
Call self as a function.
- class StrangenessConservation(*args, **kwargs)[source]#
Bases:
EdgeQNConservationRule
Decorated via
additive_quantum_number_rule
.Check for
strangeness
conservation.- __call__(ingoing_edge_qns: List[strangeness], outgoing_edge_qns: List[strangeness]) bool [source]#
Call self as a function.
- class CharmConservation(*args, **kwargs)[source]#
Bases:
EdgeQNConservationRule
Decorated via
additive_quantum_number_rule
.Check for
charmness
conservation.
- class BottomnessConservation(*args, **kwargs)[source]#
Bases:
EdgeQNConservationRule
Decorated via
additive_quantum_number_rule
.Check for
bottomness
conservation.- __call__(ingoing_edge_qns: List[bottomness], outgoing_edge_qns: List[bottomness]) bool [source]#
Call self as a function.
- parity_conservation(ingoing_edge_qns: List[parity], outgoing_edge_qns: List[parity], l_magnitude: l_magnitude) bool [source]#
Implement \(P_{in} = P_{out} \cdot (-1)^L\).
- class HelicityParityEdgeInput(parity, spin_magnitude, spin_projection)[source]#
Bases:
object
- spin_magnitude: spin_magnitude[source]#
- spin_projection: spin_projection[source]#
- parity_conservation_helicity(ingoing_edge_qns: List[HelicityParityEdgeInput], outgoing_edge_qns: List[HelicityParityEdgeInput], parity_prefactor: parity_prefactor) bool [source]#
Implements parity conservation for helicity formalism.
Check the following:
\[A_{-\lambda_1-\lambda_2} = P_1 P_2 P_3 (-1)^{S_2+S_3-S_1} A_{\lambda_1\lambda_2}\]\[\mathrm{parity\,prefactor} = P_1 P_2 P_3 (-1)^{S_2+S_3-S_1}\]Note
Only the special case \(\lambda_1=\lambda_2=0\) may return
False
independent on the parity prefactor.
- class CParityEdgeInput(spin_magnitude, pid, c_parity=None)[source]#
Bases:
object
- spin_magnitude: spin_magnitude[source]#
- class CParityNodeInput(l_magnitude, s_magnitude)[source]#
Bases:
object
- l_magnitude: l_magnitude[source]#
- s_magnitude: s_magnitude[source]#
- c_parity_conservation(ingoing_edge_qns: List[CParityEdgeInput], outgoing_edge_qns: List[CParityEdgeInput], interaction_node_qns: CParityNodeInput) bool [source]#
Check for \(C\)-parity conservation.
Implements \(C_{in} = C_{out}\).
- class GParityEdgeInput(isospin_magnitude, spin_magnitude, pid, g_parity=None)[source]#
Bases:
object
- isospin_magnitude: isospin_magnitude[source]#
- spin_magnitude: spin_magnitude[source]#
- class GParityNodeInput(l_magnitude, s_magnitude)[source]#
Bases:
object
- l_magnitude: l_magnitude[source]#
- s_magnitude: s_magnitude[source]#
- g_parity_conservation(ingoing_edge_qns: List[GParityEdgeInput], outgoing_edge_qns: List[GParityEdgeInput], interaction_qns: GParityNodeInput) bool [source]#
Check for \(G\)-parity conservation.
Implements for \(G_{in} = G_{out}\).
- class IdenticalParticleSymmetryOutEdgeInput(spin_magnitude, spin_projection, pid)[source]#
Bases:
object
- spin_magnitude: spin_magnitude[source]#
- spin_projection: spin_projection[source]#
- identical_particle_symmetrization(ingoing_parities: List[parity], outgoing_edge_qns: List[IdenticalParticleSymmetryOutEdgeInput]) bool [source]#
Verifies multi particle state symmetrization for identical particles.
In case of a multi particle state with identical particles, their exchange symmetry has to follow the spin statistic theorem.
For bosonic systems the total exchange symmetry (parity) has to be even (+1). For fermionic systems the total exchange symmetry (parity) has to be odd (-1).
In case of a particle decaying into N identical particles (N>1), the decaying particle has to have the same parity as required by the spin statistic theorem of the multi body state.
- class SpinNodeInput(l_magnitude, l_projection, s_magnitude, s_projection)[source]#
Bases:
object
- l_magnitude: l_magnitude[source]#
- l_projection: l_projection[source]#
- s_magnitude: s_magnitude[source]#
- s_projection: s_projection[source]#
- class SpinMagnitudeNodeInput(l_magnitude, s_magnitude)[source]#
Bases:
object
- l_magnitude: l_magnitude[source]#
- s_magnitude: s_magnitude[source]#
- ls_spin_validity(spin_input: SpinNodeInput) bool [source]#
Check for valid isospin magnitude and projection.
- class IsoSpinEdgeInput(isospin_magnitude, isospin_projection)[source]#
Bases:
object
- isospin_magnitude: isospin_magnitude[source]#
- isospin_projection: isospin_projection[source]#
- isospin_validity(isospin: IsoSpinEdgeInput) bool [source]#
Check for valid isospin magnitude and projection.
- isospin_conservation(ingoing_isospins: List[IsoSpinEdgeInput], outgoing_isospins: List[IsoSpinEdgeInput]) bool [source]#
Check for isospin conservation.
Implements
\[|I_1 - I_2| \leq I \leq |I_1 + I_2|\]Also checks \(I_{1,z} + I_{2,z} = I_z\) and if Clebsch-Gordan coefficients are all 0.
- class SpinEdgeInput(spin_magnitude, spin_projection)[source]#
Bases:
object
- spin_magnitude: spin_magnitude[source]#
- spin_projection: spin_projection[source]#
- spin_validity(spin: SpinEdgeInput) bool [source]#
Check for valid spin magnitude and projection.
- spin_conservation(ingoing_spins: List[SpinEdgeInput], outgoing_spins: List[SpinEdgeInput], interaction_qns: SpinNodeInput) bool [source]#
Check for spin conservation.
Implements
\[|S_1 - S_2| \leq S \leq |S_1 + S_2|\]and
\[|L - S| \leq J \leq |L + S|\]Also checks \(M_1 + M_2 = M\) and if Clebsch-Gordan coefficients are all 0.
See also
/docs/usage/ls-coupling
- spin_magnitude_conservation(ingoing_spins: List[SpinEdgeInput], outgoing_spins: List[SpinEdgeInput], interaction_qns: SpinMagnitudeNodeInput) bool [source]#
Check for spin conservation.
Implements
\[|S_1 - S_2| \leq S \leq |S_1 + S_2|\]and
\[|L - S| \leq J \leq |L + S|\]
- clebsch_gordan_helicity_to_canonical(ingoing_spins: List[SpinEdgeInput], outgoing_spins: List[SpinEdgeInput], interaction_qns: SpinNodeInput) bool [source]#
Implement Clebsch-Gordan checks.
For \(S_1, S_2\) to \(S\) and the \(L,S\) to \(J\) coupling based on the conversion of helicity to canonical amplitude sums.
Note
This rule does not check that the spin magnitudes couple correctly to \(L\) and \(S\), as this is already performed by
spin_magnitude_conservation
.
- helicity_conservation(ingoing_spin_mags: List[spin_magnitude], outgoing_helicities: List[spin_projection]) bool [source]#
Implementation of helicity conservation.
Check for \(|\lambda_2-\lambda_3| \leq S_1\).
- class GellMannNishijimaInput(charge, isospin_projection=None, strangeness=None, charmness=None, bottomness=None, topness=None, baryon_number=None, electron_lepton_number=None, muon_lepton_number=None, tau_lepton_number=None)[source]#
Bases:
object
- isospin_projection: isospin_projection | None[source]#
- strangeness: strangeness | None[source]#
- bottomness: bottomness | None[source]#
- baryon_number: baryon_number | None[source]#
- electron_lepton_number: electron_lepton_number | None[source]#
- muon_lepton_number: muon_lepton_number | None[source]#
- tau_lepton_number: tau_lepton_number | None[source]#
- gellmann_nishijima(edge_qns: GellMannNishijimaInput) bool [source]#
Check the Gell-Mann-Nishijima formula.
\[Q = I_3 + \frac{1}{2}(B+S+C+B'+T)\]where \(Q\) is charge (computed), \(I_3\) is
Spin.projection
ofisospin
, \(B\) isbaryon_number
, \(S\) isstrangeness
, \(C\) ischarmness
, \(B'\) isbottomness
, and \(T\) istopness
.
- class MassConservation(width_factor: float)[source]#
Bases:
object
Mass conservation rule.
- __call__(ingoing_edge_qns: List[MassEdgeInput], outgoing_edge_qns: List[MassEdgeInput]) bool [source]#
Implements mass conservation.
\(M_{out} - N \cdot W_{out} < M_{in} + N \cdot W_{in}\)
It makes sure that the net mass outgoing state \(M_{out}\) is smaller than the net mass of the ingoing state \(M_{in}\). Also the width \(W\) of the states is taken into account.
particle#
import qrules.particle
A collection of particle info containers.
The particle
module is the starting point of qrules
. Its main interface is the
ParticleCollection
, which is a collection of immutable Particle
instances that are
uniquely defined by their properties. As such, it can be used stand-alone as a database
of quantum numbers (see Particle database).
The transition
module uses the properties of Particle
instances when it computes
which MutableTransition
s are allowed between an initial state and final state.
- class Spin(magnitude: SupportsFloat, projection: SupportsFloat)[source]#
Bases:
object
Safe, immutable data container for spin with projection.
- class Particle(*, name: str, pid: int, latex: str | None = None, spin, mass, width=0.0, charge: int = 0, isospin: Spin | Tuple[float, float] | None = None, strangeness: int = 0, charmness: int = 0, bottomness: int = 0, topness: int = 0, baryon_number: int = 0, electron_lepton_number: int = 0, muon_lepton_number: int = 0, tau_lepton_number: int = 0, parity: Parity | int | None = None, c_parity: Parity | int | None = None, g_parity: Parity | int | None = None)[source]#
Bases:
object
Immutable container of data defining a physical particle.
A
Particle
is defined by the minimum set of the quantum numbers that every possible instances of that particle have in common (the “static” quantum numbers of the particle). A “non-static” quantum number is the spin projection. HenceParticle
instances do not contain spin projection information.Particle
instances are uniquely defined by their quantum numbers and properties likemass
. Thename
andpid
are therefore just labels that are not taken into account when checking if twoParticle
instances are equal.Note
As opposed to classes such as
EdgeQuantumNumbers
andNodeQuantumNumbers
, theParticle
class serves as an interface to the user (see Particle database).
- class ParticleCollection(particles: Iterable[Particle] | None = None)[source]#
Bases:
MutableSet
Searchable collection of immutable
Particle
instances.- discard(value: Particle | str) None [source]#
Remove an element. Do not raise an exception if absent.
- filter(function: Callable[[Particle], bool]) ParticleCollection [source]#
Search by
Particle
properties using alambda
function.For example:
>>> from qrules.particle import load_pdg >>> pdg = load_pdg() >>> subset = pdg.filter( ... lambda p: p.mass > 1.8 ... and p.mass < 2.0 ... and p.spin == 2 ... and p.strangeness == 1 ... ) >>> sorted(subset.names) ['K(2)(1820)+', 'K(2)(1820)0', 'K(2)*(1980)+', 'K(2)*(1980)0']
- create_particle(template_particle: Particle, name: str | None = None, latex: str | None = None, pid: int | None = None, mass: float | None = None, width: float | None = None, charge: int | None = None, spin: float | None = None, isospin: Spin | None = None, strangeness: int | None = None, charmness: int | None = None, bottomness: int | None = None, topness: int | None = None, baryon_number: int | None = None, electron_lepton_number: int | None = None, muon_lepton_number: int | None = None, tau_lepton_number: int | None = None, parity: int | None = None, c_parity: int | None = None, g_parity: int | None = None) Particle [source]#
- create_antiparticle(template_particle: Particle, new_name: str | None = None, new_latex: str | None = None) Particle [source]#
- load_pdg() ParticleCollection [source]#
Create a
ParticleCollection
with all entries from the PDG.PDG info is imported from the scikit-hep/particle package.
quantum_numbers#
import qrules.quantum_numbers
Definitions used internally for type hints and signatures.
qrules
is strictly typed (enforced through mypy). This module
bundles structures and definitions that don’t serve as data containers but only as type
hints. EdgeQuantumNumbers
and NodeQuantumNumbers
are the main structures and serve
as a bridge between the particle
and the conservation_rules
module.
- class EdgeQuantumNumbers[source]#
Bases:
object
Definition of quantum numbers for edges.
This class defines the types that are used in the
conservation_rules
, for instance inadditive_quantum_number_rule
. You can also create data classes (seeattrs.define()
) with data members that are typed as the data members ofEdgeQuantumNumbers
(see for exampleHelicityParityEdgeInput
) and use them in conservation rules that satisfy the appropriate rule protocol (seeConservationRule
,EdgeQNConservationRule
).
- class NodeQuantumNumbers[source]#
Bases:
object
Definition of quantum numbers for interaction nodes.
- class InteractionProperties(l_magnitude: int | None = None, l_projection: int | None = None, s_magnitude: float | None = None, s_projection: float | None = None, parity_prefactor: float | None = None)[source]#
Bases:
object
Immutable data structure containing interaction properties.
Interactions are represented by a node on a
MutableTransition
. This class represents the properties that are carried collectively by the edges that this node connects.Interaction properties are in particular important in the canonical basis of the helicity formalism. There, the coupled spin and angular momentum of each interaction are used for the Clebsch-Gordan coefficients for each term in a sequential amplitude.
Note
As opposed to
NodeQuantumNumbers
, theInteractionProperties
class serves as an interface to the user.
settings#
import qrules.settings
Default configuration for qrules
.
It is possible to change some settings from the outside, for instance:
>>> import qrules
>>> qrules.settings.MAX_ANGULAR_MOMENTUM = 4
>>> qrules.settings.MAX_SPIN_MAGNITUDE = 3
- CONSERVATION_LAW_PRIORITIES: Dict[GraphElementRule | EdgeQNConservationRule | ConservationRule, int] = {<class 'qrules.conservation_rules.BaryonNumberConservation'>: 90, <class 'qrules.conservation_rules.BottomnessConservation'>: 68, <class 'qrules.conservation_rules.ChargeConservation'>: 100, <class 'qrules.conservation_rules.CharmConservation'>: 70, <class 'qrules.conservation_rules.ElectronLNConservation'>: 45, <class 'qrules.conservation_rules.MassConservation'>: 10, <class 'qrules.conservation_rules.MuonLNConservation'>: 44, <class 'qrules.conservation_rules.StrangenessConservation'>: 69, <class 'qrules.conservation_rules.TauLNConservation'>: 43, <function c_parity_conservation>: 5, <function g_parity_conservation>: 3, <function helicity_conservation>: 7, <function identical_particle_symmetrization>: 2, <function isospin_conservation>: 60, <function ls_spin_validity>: 89, <function parity_conservation>: 6, <function parity_conservation_helicity>: 4, <function spin_conservation>: 8, <function spin_magnitude_conservation>: 8}#
Determines the order with which to verify conservation rules.
- EDGE_RULE_PRIORITIES: Dict[GraphElementRule, int] = {<function gellmann_nishijima>: 50, <function isospin_validity>: 61, <function spin_validity>: 62}#
Determines the order with which to verify
Edge
conservation rules.
- class InteractionType(value)[source]#
Bases:
Enum
Types of interactions in the form of an enumerate.
- static from_str(description: str) InteractionType [source]#
- create_interaction_settings(formalism: str, particle_db: ParticleCollection, nbody_topology: bool = False, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 2, max_spin_magnitude: float = 2.0) Dict[InteractionType, Tuple[EdgeSettings, NodeSettings]] [source]#
Create a container that holds the settings for
InteractionType
.
solving#
import qrules.solving
Functions to solve a particle reaction problem.
This module is responsible for solving a particle reaction problem stated by a
QNProblemSet
. The Solver
classes (e.g. CSPSolver
) generate new quantum
numbers (for example belonging to an intermediate state) and validate the decay
processes with the rules formulated by the conservation_rules
module.
- class EdgeSettings(conservation_rules: Set[GraphElementRule] = _Nothing.NOTHING, rule_priorities: Dict[GraphElementRule, int] = _Nothing.NOTHING, qn_domains: Dict[Any, list] = _Nothing.NOTHING)[source]#
Bases:
object
Solver settings for a specific edge of a graph.
- conservation_rules: Set[GraphElementRule][source]#
- rule_priorities: Dict[GraphElementRule, int][source]#
- class NodeSettings(conservation_rules: Set[GraphElementRule | EdgeQNConservationRule | ConservationRule] = _Nothing.NOTHING, rule_priorities: Dict[GraphElementRule | EdgeQNConservationRule | ConservationRule, int] = _Nothing.NOTHING, qn_domains: Dict[Any, list] = _Nothing.NOTHING, interaction_strength: float = 1.0)[source]#
Bases:
object
Container class for the interaction settings.
This class can be assigned to each node of a state transition graph. Hence, these settings contain the complete configuration information which is required for the solution finding, e.g:
set of conservation rules
mapping of rules to priorities (optional)
mapping of quantum numbers to their domains
strength scale parameter (higher value means stronger force)
- conservation_rules: Set[GraphElementRule | EdgeQNConservationRule | ConservationRule][source]#
- rule_priorities: Dict[GraphElementRule | EdgeQNConservationRule | ConservationRule, int][source]#
- GraphSettings: TypeAlias = 'MutableTransition[EdgeSettings, NodeSettings]'#
(Mutable) mapping of settings on a
Topology
.
- GraphElementProperties: TypeAlias = 'MutableTransition[GraphEdgePropertyMap, GraphNodePropertyMap]'#
(Mutable) mapping of edge and node properties on a
Topology
.
- class QNProblemSet(initial_facts: GraphElementProperties, solving_settings: GraphSettings)[source]#
Bases:
object
Particle reaction problem set, defined as a graph like data structure.
- Parameters:
initial_facts – all of the known facts quantum numbers of the problem.
solving_settings – solving specific settings, such as the specific rules and variable domains for nodes and edges of the
topology
.
- initial_facts: GraphElementProperties[source]#
- solving_settings: GraphSettings[source]#
- class QNResult(solutions: List[MutableTransition[Dict[Type[pid | mass | width | spin_magnitude | spin_projection | charge | isospin_magnitude | isospin_projection | strangeness | charmness | bottomness | topness | baryon_number | electron_lepton_number | muon_lepton_number | tau_lepton_number | parity | c_parity | g_parity], int | float], Dict[Type[l_magnitude | l_projection | s_magnitude | s_projection | parity_prefactor], int | float]]] = _Nothing.NOTHING, not_executed_node_rules: Dict[int, Set[str]] = _Nothing.NOTHING, violated_node_rules: Dict[int, Set[str]] = _Nothing.NOTHING, not_executed_edge_rules: Dict[int, Set[str]] = _Nothing.NOTHING, violated_edge_rules: Dict[int, Set[str]] = _Nothing.NOTHING)[source]#
Bases:
object
Defines a result to a problem set processed by the solving code.
- solutions: List[MutableTransition[Dict[Type[pid | mass | width | spin_magnitude | spin_projection | charge | isospin_magnitude | isospin_projection | strangeness | charmness | bottomness | topness | baryon_number | electron_lepton_number | muon_lepton_number | tau_lepton_number | parity | c_parity | g_parity], int | float], Dict[Type[l_magnitude | l_projection | s_magnitude | s_projection | parity_prefactor], int | float]]][source]#
- class Solver[source]#
Bases:
ABC
Interface of a Solver.
- abstract find_solutions(problem_set: QNProblemSet) QNResult [source]#
Find solutions for the given input.
It is expected that this function determines and returns all of the found solutions. In case no solutions are found a partial list of violated rules has to be given. This list of violated rules does not have to be complete.
- Parameters:
problem_set (
QNProblemSet
) – states a problem set- Returns:
- contains possible solutions, violated rules and not executed
rules due to requirement issues.
- Return type:
- validate_full_solution(problem_set: QNProblemSet) QNResult [source]#
- class CSPSolver(allowed_intermediate_states: Iterable[Dict[Type[pid | mass | width | spin_magnitude | spin_projection | charge | isospin_magnitude | isospin_projection | strangeness | charmness | bottomness | topness | baryon_number | electron_lepton_number | muon_lepton_number | tau_lepton_number | parity | c_parity | g_parity], int | float]])[source]#
Bases:
Solver
Solver reducing the task to a Constraint Satisfaction Problem.
Solving this done with the python-constraint module.
The variables are the quantum numbers of particles/edges, but also some composite quantum numbers which are attributed to the interaction nodes (such as angular momentum \(L\)). The conservation rules serve as the constraints and a special wrapper class serves as an adapter.
- find_solutions(problem_set: QNProblemSet) QNResult [source]#
Find solutions for the given input.
It is expected that this function determines and returns all of the found solutions. In case no solutions are found a partial list of violated rules has to be given. This list of violated rules does not have to be complete.
- Parameters:
problem_set (
QNProblemSet
) – states a problem set- Returns:
- contains possible solutions, violated rules and not executed
rules due to requirement issues.
- Return type:
- class Scoresheet[source]#
Bases:
object
- register_rule(graph_element_id: int, rule: GraphElementRule | EdgeQNConservationRule | ConservationRule) Callable[[bool], None] [source]#
- property rule_calls: Dict[Tuple[int, GraphElementRule | EdgeQNConservationRule | ConservationRule], int][source]#
- property rule_passes: Dict[Tuple[int, GraphElementRule | EdgeQNConservationRule | ConservationRule], int][source]#
system_control#
import qrules.system_control
Functions that steer operations of qrules
.
- create_edge_properties(particle: Particle, spin_projection: float | None = None) Dict[Type[pid | mass | width | spin_magnitude | spin_projection | charge | isospin_magnitude | isospin_projection | strangeness | charmness | bottomness | topness | baryon_number | electron_lepton_number | muon_lepton_number | tau_lepton_number | parity | c_parity | g_parity], int | float] [source]#
- create_node_properties(interactions: InteractionProperties) Dict[Type[l_magnitude | l_projection | s_magnitude | s_projection | parity_prefactor], int | float] [source]#
- find_particle(state: Dict[Type[pid | mass | width | spin_magnitude | spin_projection | charge | isospin_magnitude | isospin_projection | strangeness | charmness | bottomness | topness | baryon_number | electron_lepton_number | muon_lepton_number | tau_lepton_number | parity | c_parity | g_parity], int | float], particle_db: ParticleCollection) Tuple[Particle, float] [source]#
Create a Particle with spin projection from a qn dictionary.
The implementation assumes the edge properties match the attributes of a particle inside the
ParticleCollection
.- Parameters:
states – The quantum number dictionary. particle_db: A
ParticleCollection
which is used to retrieve a referencestate
to lower the memory footprint.- Raises:
KeyError – If the edge properties do not contain the pid information or no particle with the same pid is found in the
ParticleCollection
.ValueError – If the edge properties do not contain spin projection info.
- create_interaction_properties(qn_solution: Dict[Type[l_magnitude | l_projection | s_magnitude | s_projection | parity_prefactor], int | float]) InteractionProperties [source]#
- filter_interaction_types(valid_determined_interaction_types: List[InteractionType], allowed_interaction_types: List[InteractionType]) List[InteractionType] [source]#
- class GammaCheck[source]#
Bases:
InteractionDeterminator
Conservation check for photons.
- class LeptonCheck[source]#
Bases:
InteractionDeterminator
Conservation check lepton numbers.
- remove_duplicate_solutions(solutions: List[MutableTransition[Tuple[Particle, float], InteractionProperties]], remove_qns_list: Set[Type[l_magnitude | l_projection | s_magnitude | s_projection | parity_prefactor]] | None = None, ignore_qns_list: Set[Type[l_magnitude | l_projection | s_magnitude | s_projection | parity_prefactor]] | None = None) List[MutableTransition[Tuple[Particle, float], InteractionProperties]] [source]#
- class NodePropertyComparator(ignored_qn_list: Set[Type[l_magnitude | l_projection | s_magnitude | s_projection | parity_prefactor]] | None = None)[source]#
Bases:
object
Functor for comparing node properties in two graphs.
- __call__(interactions1: InteractionProperties, interactions2: InteractionProperties) bool [source]#
Call self as a function.
- filter_graphs(graphs: List[MutableTransition], filters: Iterable[Callable[[MutableTransition], bool]]) List[MutableTransition] [source]#
Implement filtering of a list of
MutableTransition
‘s.This function can be used to select a subset of
MutableTransition
‘s from a list. Only the graphs passing all supplied filters will be returned.Note
For the more advanced user, lambda functions can be used as filters.
Example
Selecting only the solutions, in which the \(\rho\) decays via p-wave:
my_filter = require_interaction_property( "rho", InteractionQuantumNumberNames.L, create_spin_domain([1], True), ) filtered_solutions = filter_graphs(solutions, [my_filter])
- require_interaction_property(ingoing_particle_name: str, interaction_qn: Type[l_magnitude | l_projection | s_magnitude | s_projection | parity_prefactor], allowed_values: List) Callable[[MutableTransition[Tuple[Particle, float], InteractionProperties]], bool] [source]#
Filter function.
Closure, which can be used as a filter function in
filter_graphs()
.It selects graphs based on a requirement on the property of specific interaction nodes.
- Parameters:
ingoing_particle_name – name of particle, used to find nodes which have a particle with this name as “ingoing”
interaction_qn – interaction quantum number
allowed_values – list of allowed values, that the interaction quantum number may take
- Returns:
True if the graph has nodes with an ingoing particle of the given name, and the graph fullfills the quantum number requirement
False otherwise
- Return type:
Callable[Any, bool]
topology#
import qrules.topology
Functionality for Topology
and Transition
instances.
Main interfaces
Topology
and its builder functionscreate_isobar_topologies()
andcreate_n_body_topology()
.Transition
and its two implementationsMutableTransition
andFrozenTransition
.
- class FrozenDict(mapping: Mapping | None = None)[source]#
Bases:
Hashable
,Mapping
,Generic
[KT
,VT
]An immutable and hashable version of a
dict
.FrozenDict
makes it possible to make classes hashable if they are decorated withattr.frozen()
and containMapping
-like attributes. If these attributes were to be implemented with a normaldict
, the instance is strictly speaking still mutable (even if those attributes are aproperty
) and the class is therefore not safely hashable.Warning
The keys have to be comparable, that is, they need to have a
__lt__()
method.
- class Edge(originating_node_id: int | None = None, ending_node_id: int | None = None)[source]#
Bases:
object
Struct-like definition of an edge, used in
Topology.edges
.- originating_node_id: int | None[source]#
Node ID where the
Edge
starts.An
Edge
is incoming to aTopology
if itsoriginating_node_id
isNone
(seeincoming_edge_ids
).
- ending_node_id: int | None[source]#
Node ID where the
Edge
ends.An
Edge
is outgoing from aTopology
if itsending_node_id
isNone
(seeoutgoing_edge_ids
).
- class Topology(nodes: Iterable[int], edges: Mapping[int, Edge])[source]#
Bases:
object
Directed Feynman-like graph without edge or node properties.
A
Topology
is directed in the sense that its edges are ingoing and outgoing to specific nodes. This is to mimic Feynman graphs, which have a time axis. Note that aTopology
is not strictly speaking a graph from graph theory, because it allows open edges, like a Feynman-diagram.The edges and nodes can be provided with properties with a
Transition
, which contains atopology
.As opposed to a
MutableTopology
, aTopology
is frozen, hashable, and ordered, so that it can be used as a kind of fingerprint for aTransition
. In addition, the IDs ofedges
are guaranteed to be sequential integers and follow a specific pattern:incoming_edge_ids
(initial_states
) are always negative.outgoing_edge_ids
(final_states
) lie in the range0...n-1
withn
the number of final states.intermediate_edge_ids
continue counting fromn
.
See also
MutableTopology.organize_edge_ids()
.Example
Isobar decay topologies can best be created as follows:
>>> topologies = create_isobar_topologies(number_of_final_states=3) >>> len(topologies) 1 >>> topologies[0] Topology(nodes=..., edges=...)
- incoming_edge_ids: FrozenSet[int][source]#
Edge IDs of edges that have no
originating_node_id
.Transition.initial_states
provide properties for these edges.
- outgoing_edge_ids: FrozenSet[int][source]#
Edge IDs of edges that have no
ending_node_id
.Transition.final_states
provide properties for these edges.
- is_isomorphic(other: Topology) bool [source]#
Check if two graphs are isomorphic.
Returns
True
if the two graphs have a one-to-one mapping of the node IDs and edge IDs.Warning
Not yet implemented.
- relabel_edges(old_to_new: Mapping[int, int]) Topology [source]#
Create a new
Topology
with new edge IDs.This method is particularly useful when creating permutations of a
Topology
, e.g.:>>> topologies = create_isobar_topologies(3) >>> len(topologies) 1 >>> topology = topologies[0] >>> final_state_ids = topology.outgoing_edge_ids >>> permuted_topologies = { ... topology.relabel_edges(dict(zip(final_state_ids, permutation))) ... for permutation in itertools.permutations(final_state_ids) ... } >>> len(permuted_topologies) 3
- get_originating_node_list(topology: Topology, edge_ids: Iterable[int]) List[int] [source]#
Get list of node ids from which the supplied edges originate from.
- class MutableTopology(nodes: Iterable[int] = _Nothing.NOTHING, edges: Mapping[int, Edge] = _Nothing.NOTHING)[source]#
Bases:
object
Mutable version of a
Topology
.A
MutableTopology
can be used to conveniently build up aTopology
(see e.g.SimpleStateTransitionTopologyBuilder
). It does not have restrictions on the numbering of edge and node IDs.- nodes: Set[int][source]#
See
Topology.nodes
.
- add_node(node_id: int) None [source]#
Adds a node with number
node_id
.- Raises:
ValueError – if
node_id
already exists innodes
.
- add_edges(edge_ids: Iterable[int]) None [source]#
Add edges with the ids in the
edge_ids
list.- Raises:
ValueError – if
edge_ids
already exist inedges
.
- attach_edges_to_node_ingoing(ingoing_edge_ids: Iterable[int], node_id: int) None [source]#
Attach existing edges to nodes.
So that the are ingoing to these nodes.
- Parameters:
- Raises:
ValueError – if an edge not doesn’t exist.
ValueError – if an edge ID is already an ingoing node.
- organize_edge_ids() MutableTopology [source]#
Organize edge IDS so that they lie in range
[-m, n+i]
.Here,
m
is the number ofincoming_edge_ids
,n
is the number ofoutgoing_edge_ids
, andi
is the number ofintermediate_edge_ids
.In other words, relabel the edges so that:
incoming edge IDs lie in the range
[-1, -2, ...]
,outgoing edge IDs lie in the range
[0, 1, ..., n]
,intermediate edge IDs lie in the range
[n+1, n+2, ...]
.
- freeze() Topology [source]#
Create an immutable
Topology
from thisMutableTopology
.You may need to call
organize_edge_ids()
first.
- class InteractionNode(number_of_ingoing_edges: int, number_of_outgoing_edges: int)[source]#
Bases:
object
Helper class for the
SimpleStateTransitionTopologyBuilder
.
- class SimpleStateTransitionTopologyBuilder(interaction_node_set: Iterable[InteractionNode])[source]#
Bases:
object
Simple topology builder.
Recursively tries to add the interaction nodes to available open end edges/lines in all combinations until the number of open end lines matches the final state lines.
- create_isobar_topologies(number_of_final_states: int) Tuple[Topology, ...] [source]#
Builder function to create a set of unique isobar decay topologies.
- Parameters:
number_of_final_states – The number of
outgoing_edge_ids
(final_states
).- Returns:
A sorted
tuple
of non-isomorphicTopology
instances, all with the same number of final states.
Example
>>> topologies = create_isobar_topologies(number_of_final_states=4) >>> len(topologies) 2 >>> len(topologies[0].outgoing_edge_ids) 4 >>> len(set(topologies)) # hashable 2 >>> list(topologies) == sorted(topologies) # ordered True
- create_n_body_topology(number_of_initial_states: int, number_of_final_states: int) Topology [source]#
Create a
Topology
that connects all edges through a single node.These types of “\(n\)-body topologies” are particularly important for
check_reaction_violations()
andconservation_rules
.- Parameters:
number_of_initial_states – The number of
incoming_edge_ids
(initial_states
).number_of_final_states – The number of
outgoing_edge_ids
(final_states
).
Example
>>> topology = create_n_body_topology( ... number_of_initial_states=2, ... number_of_final_states=5, ... ) >>> topology Topology(nodes=..., edges...) >>> len(topology.nodes) 1 >>> len(topology.incoming_edge_ids) 2 >>> len(topology.outgoing_edge_ids) 5
- class Transition[source]#
Bases:
ABC
,Generic
[EdgeType
,NodeType
]Mapping of edge and node properties over a
Topology
.This interface class describes a transition from an initial state to a final state by providing a mapping of properties over the
edges
andnodes
of itstopology
. Since aTopology
behaves like a Feynman graph, edges are considered as “states
” and nodes are considered asinteractions
between those states.There are two implementation classes:
FrozenTransition
: a complete, hashable and ordered mapping of properties over theedges
andnodes
in itstopology
.MutableTransition
: comparable toMutableTopology
in that it is used internally when finding solutions through theStateTransitionManager
etc.
These classes are also provided with mixin attributes
initial_states
,final_states
,intermediate_states
, andfilter_states()
.- abstract property topology: Topology[source]#
Topology
over whichstates
andinteractions
are defined.
- class FrozenTransition(topology: Topology, states: Mapping | None, interactions: Mapping | None)[source]#
Bases:
Transition
,Generic
[EdgeType
,NodeType
]Defines a frozen mapping of edge and node properties on a
Topology
.- states: FrozenDict[int, EdgeType][source]#
- interactions: FrozenDict[int, NodeType][source]#
- unfreeze() MutableTransition[EdgeType, NodeType] [source]#
Convert into a
MutableTransition
.
- convert() FrozenTransition[EdgeType, NodeType] [source]#
- convert(state_converter: Callable[[EdgeType], NewEdgeType]) FrozenTransition[NewEdgeType, NodeType]
- convert(*, interaction_converter: Callable[[NodeType], NewNodeType]) FrozenTransition[EdgeType, NewNodeType]
- convert(state_converter: Callable[[EdgeType], NewEdgeType], interaction_converter: Callable[[NodeType], NewNodeType]) FrozenTransition[NewEdgeType, NewNodeType]
Cast the edge and/or node properties to another type.
- class MutableTransition(topology: Topology, states: Mapping[int, EdgeType] = _Nothing.NOTHING, interactions: Mapping[int, NodeType] = _Nothing.NOTHING)[source]#
Bases:
Transition
,Generic
[EdgeType
,NodeType
]Mutable implementation of a
Transition
.Mainly used internally by the
StateTransitionManager
to build solutions.- compare(other: MutableTransition, state_comparator: Callable[[EdgeType, EdgeType], bool] | None = None, interaction_comparator: Callable[[NodeType, NodeType], bool] | None = None) bool [source]#
- freeze() FrozenTransition[EdgeType, NodeType] [source]#
Convert into a
FrozenTransition
.
transition#
import qrules.transition
Find allowed transitions between an initial and final state.
- class ExecutionInfo(not_executed_node_rules: Dict[int, Set[str]] = _Nothing.NOTHING, violated_node_rules: Dict[int, Set[str]] = _Nothing.NOTHING, not_executed_edge_rules: Dict[int, Set[str]] = _Nothing.NOTHING, violated_edge_rules: Dict[int, Set[str]] = _Nothing.NOTHING)[source]#
Bases:
object
- extend(other_result: ExecutionInfo, intersect_violations: bool = False) None [source]#
- class ProblemSet(topology: Topology, initial_facts: InitialFacts, solving_settings: GraphSettings)[source]#
Bases:
object
Particle reaction problem set as a graph-like data structure.
- initial_facts: InitialFacts[source]#
Information about the initial and final state.
- solving_settings: GraphSettings[source]#
Solving settings, such as conservation rules and QN-domains.
- to_qn_problem_set() QNProblemSet [source]#
- class StateTransitionManager(initial_state: Sequence[str | Tuple[str, Sequence[float]]], final_state: Sequence[str | Tuple[str, Sequence[float]]], particle_db: ParticleCollection | None = None, allowed_intermediate_particles: List[str] | None = None, interaction_type_settings: Dict[InteractionType, Tuple[EdgeSettings, NodeSettings]] | None = None, formalism: str = 'helicity', topology_building: str = 'isobar', solving_mode: SolvingMode = SolvingMode.FAST, reload_pdg: bool = False, mass_conservation_factor: float | None = 3.0, max_angular_momentum: int = 1, max_spin_magnitude: float = 2.0, number_of_threads: int | None = None)[source]#
Bases:
object
Main handler for decay topologies.
See also
- interaction_determinators: List[InteractionDeterminator][source]#
Checks that are executed over selected conservation rules.
See also
{ref}`usage/reaction:Select interaction types`
- topologies: Tuple[Topology, ...][source]#
Topology
instances over which the STM propagates quantum numbers.
- set_allowed_intermediate_particles(name_patterns: Iterable[str] | str, regex: bool = False) None [source]#
- get_allowed_interaction_types() List[InteractionType] | Dict[int, List[InteractionType]] [source]#
- get_allowed_interaction_types(node_id: int) List[InteractionType]
- set_allowed_interaction_types(allowed_interaction_types: Iterable[InteractionType], node_id: int | None = None) None [source]#
- find_solutions(problem_sets: Dict[float, List[ProblemSet]]) ReactionInfo [source]#
Check for solutions for a specific set of interaction settings.
- class State(particle: Particle, spin_projection: SupportsFloat)[source]#
Bases:
object
- class ReactionInfo(transitions: Iterable[FrozenTransition[State, InteractionProperties]], formalism: str)[source]#
Bases:
object
Ordered collection of
StateTransition
instances.- transitions: Tuple[FrozenTransition[State, InteractionProperties], ...][source]#
- initial_state: FrozenDict[int, Particle][source]#
- final_state: FrozenDict[int, Particle][source]#
- get_intermediate_particles() ParticleCollection [source]#
Extract the names of the intermediate state particles.
- group_by_topology() Dict[Topology, List[FrozenTransition[State, InteractionProperties]]] [source]#