Welcome to QRules!#

10.5281/zenodo.5526360 Supported Python versions Google Colab Binder

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:

  1. 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\).

  2. 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.

  3. 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

  1. 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).

  2. 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#

PyPI package Conda package Supported Python versions

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:

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.

  1. [Recommended] Create a virtual environment with venv (see here).

  2. 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)
_images/755de53f4668ca9454b15cbe2773b3e45a31d8eb7242e21298655b3b151fae1b.svg

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!

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,
)

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 ProblemSets 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)
_images/8acddb893fa4caa81c2139ede95355daa4fb98651900e9944b911317b9cdb8e0.svg

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:

  1. One the one hand, the EdgeSettings.qn_domains and NodeSettings.qn_domains contained in the solving_settings define the domain over which quantum number sets can be generated.

  2. On the other, the EdgeSettings.rule_priorities and NodeSettings.rule_priorities in solving_settings define which conservation_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 MutableTransitions 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']

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 ProblemSets 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)
Hide code cell source
dot = io.asdot(lc2pkpi_reaction, collapse_graphs=True)
graphviz.Source(dot)
_images/0383732071c513e119b53fb43f5199908633bcaa982e6a0534702e6942996ed0.svg
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
Hide 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:

Hide code cell source
dot = io.asdot(reaction, collapse_graphs=True, render_node=False)
graphviz.Source(dot)
_images/6b9ae7a4351c8ac36390a9becc4fc83a5e8f82b88e9c9142331447954f74a929.svg
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’ Particles 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(mu)~',
 'nu(tau)',
 'nu(e)',
 'nu(tau)~',
 'nu(e)~',
 '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='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,
  ),
  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,
  ),
})
LaTeX representation#

Particles 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))
\[\displaystyle \Sigma_{c}(2520)^{++}, \Sigma_{c}(2520)^{0}, \Sigma_{c}^{0}, \Sigma_{c}(2455)^{+}, \Sigma_{c}(2520)^{+}, \Sigma_{c}(2455)^{++}\]
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 ProblemSets 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.

Hide 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))
_images/fde1d867e8ea71d86f44f28aa57dd3f21a0163db17447e6ed67d29fcb8a3d27e.svg

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))
_images/61a2589985401a6892b71ecc3a98085016762a2206bb4c5848453fa854cb2b59.svg

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))
_images/3412df546f05b64af047fa81bbc9c540ebcacf00d855bd9c807ca11feb7cf405.svg

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))
_images/c2108fa9a892b20b51867851cf6c74e9ed763d6e68624700a14cbd6c41b20a93.svg
ProblemSets#

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 ProblemSets 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 lists of ProblemSets 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)
_images/42ca155ee9c9b8f5f90c26a62f21ecd1dc0873ba4fb04f723103c6b5634713f8.svg
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 Particles, 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]
Hide code cell source
dot = qrules.io.asdot(qn_problem_set, render_node=True)
graphviz.Source(dot)
_images/ed156af976cbb0f98e902cfef17adda2b7c4592af8a8bea292cb128595496141.svg
Hide code cell source
dot = qrules.io.asdot(qn_result, render_node=True)
graphviz.Source(dot)
_images/78dc5f11685c561833724b20c22a7f9bb21f1825bb3c9a3827683b54d190b7ef.svg
StateTransitions#

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)
_images/941cd153e9f2b5b927090abf20054cbb188fe8f7534a3530c5d91dfa7789efc8.svg

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)
_images/f7dc60d4308b06a24eb27cde4aa9d67716fa33fdae420a3f8281ef534a2d019a.svg

or, with stripped node properties:

dot = qrules.io.asdot(reaction.transitions[:3], strip_spin=True, render_node=True)
graphviz.Source(dot)
_images/e38278546a79a8018b3efeccb393aa2bb0538842965f9dc997aaedb63ce4b7c1.svg

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)
_images/494c311ce26954c002e36b42f9b58391c947502268d44fe1d6992e1ccdb0fae2.svg
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)
_images/82c05c92eac40c1f52802e4a85930038fa97bc8dd690ce9ae0a4bdea03cc6483.svg

Or any other properties of a State:

Hide 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)
_images/7067b5603d1054cca9b4548965d1a81330d1868572872e3f6c5893325f29a4f9.svg
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))
_images/e8df3d8eb37c61d4af3f94fc013cc410e79c2f764e293b1c4b2e171bdff93fae.svg

Conservation rules#

Hide 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 MutableTransitions, populates them with quantum numbers (edge properties representing states and nodes properties representing interactions), then checks whether the generated MutableTransitions 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#

See parity_conservation():

parity_conservation(
    ingoing_edge_qns=[Parity(-1)],
    outgoing_edge_qns=[Parity(+1), Parity(+1)],
    l_magnitude=1,
)
True
Spin conservation#

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 StateTransitions 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 tuples of a Particle and a float spin projection).

Hide 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))
_images/c9ed721c4d0c45c809aa88e07291e812583b658d08bf9451882c03bfbf5c9d69.svg_images/ee9b5ff812c4113f2d7d0b64be322f07c7df633d9241b3e72345756c3add590c.svg

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 StateTransitions#

When checking conservation rules, you may want to modify the properties on the StateTransitions. 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)
Hide code cell source
dot = qrules.io.asdot(new_transition, render_node=True)
graphviz.Source(dot)
_images/c6d95b5fab1fb8e66b47d2e41de99cbe025d0134a330fb1b30322f5559897bfe.svg

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.

Hide 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,
    ),
)
Hide code cell source
dot = qrules.io.asdot(
    topology,
    render_resonance_id=True,
    render_node=True,
    render_initial_state_id=True,
)
graphviz.Source(dot)
_images/9f7ecac02305547942d78e88c13de9e5738933ad23db910c3e1023ddc940fee6.svg

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)
Hide code cell source
dot = qrules.io.asdot(reaction_kk, collapse_graphs=True)
graphviz.Source(dot)
_images/851c89360bd013775626e62862f0321363f6b979f771f003d6b79ef6fbfa74d3.svg

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)
Hide code cell source
dot = qrules.io.asdot(reaction_ep, collapse_graphs=True)
graphviz.Source(dot)
_images/0c0607fb9926da357c27d7fb258825a056a7822da7436e4750e48b61752f21bc.svg

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)
Hide code cell source
dot = qrules.io.asdot(reaction_ep_no_mass, collapse_graphs=True)
graphviz.Source(dot)
_images/26b56046d8bb2b2b88af4d0573e5101dcedfff18f8ff17876ac5ff42b69b46c1.svg

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:

\[ J/\psi \to \Sigma^+ \bar\Sigma(1670)^-, \quad \bar\Sigma(1670)^- \rightarrow \bar p K^0. \]

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:

Hide 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))
\[\displaystyle J/\psi(1S)=1^-\qquad \Sigma^{+}=\tfrac{1}2^+\qquad \overline{\Sigma}(1670)^{-}=\tfrac{3}2^+\qquad \overline{p}=\tfrac{1}2^-\qquad K^{0}=0^-\]
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:

  1. 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\).

  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.

  3. 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:

\[\begin{split} \begin{eqnarray} \left|s_{\Sigma^+} - s_{\bar\Sigma(1670)^-}\right| & \le S & \le s_{\Sigma^+} + s_{\bar\Sigma(1670)^-} \\ \left|\tfrac{1}{2}-\tfrac{3}{2}\right| & \le S & \le \tfrac{1}{2} + \tfrac{3}{2} \\ 1 & \le S & \le 2 \end{eqnarray} \end{split}\]
\[ \Rightarrow S=1 \quad \text{or} \quad S=2 \]

Next, we determine the allowed total angular momentum values \(L\) with step 2:

\[\begin{split} \begin{eqnarray} |L-S| & \le s_{J/\psi} & \le L+S \\ |L-S| & \le 1 & \le L+S \end{eqnarray} \end{split}\]
\[\begin{split} \Rightarrow \begin{cases} L=0,1,2 & \text{if} & S=1\\ L=1,2,3 & \text{if} & S=2. \end{cases} \end{split}\]

So in total, we have 6 \(LS\)-combinations:

\[ (L,S) = (0,1), (1,1), (2,1), (1,2), (2,2), (3,2). \]

This decay however goes via the strong force. This means that parity has to be conserved and we have to follow step 3:

\[\begin{split} \begin{eqnarray} \eta_{J/\psi} & = & \eta_{\Sigma^+} \cdot \eta_{\bar\Sigma(1670)^-} \cdot(-1)^L \\ (-1) & = & (+1)\cdot(+1)\cdot(-1)^L \\ (-1) & = & (-1)^{L}. \end{eqnarray} \end{split}\]

From this, we can easily see that only odd \(L\) values are possible, which leaves us with 3 \(LS\)-combinations:

\[ (L,S) = (1,1), (1,2), (3,2). \]
\(\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:

\[\begin{split} \begin{eqnarray} |s_{\bar p} - s_{K^0}| & \le S & \le s_{\bar p} + s_{K^0} \\ \left|\tfrac{1}{2}-0 \right| & \le S & \le \tfrac{1}{2} + 0 \end{eqnarray} \end{split}\]
\[ \Rightarrow S = \tfrac{1}{2}. \]

This time, only one coupled spin value is allowed. That allows for the following values of \(L\):

\[\begin{split} \begin{eqnarray} |L-S| & \le s_0 & \le L+S \\ \left|L-\tfrac{1}{2}\right| & \le \tfrac{3}{2} & \le L+\tfrac{1}{2}. \end{eqnarray} \end{split}\]
\[ \Rightarrow L = 1,2 \]

By now, only two \(LS\)-combinations are possible:

\[ (L,S)=\left(1,\tfrac{1}{2}\right), \left(2,\tfrac{1}{2}\right). \]

This again is a strong interaction, which means we have to check for parity conservation.

\[\begin{split} \begin{eqnarray} \eta_{\bar \Sigma(1670)^-} & = & \eta_{\bar p}\cdot\eta_{K^0}\cdot(-1)^L\\ (+1) & = & (-1)\cdot(-1)\cdot(-1)^L\\ (+1) & = & (-1)^L. \end{eqnarray} \end{split}\]

Again, it is clear that only even \(L\)’s are allowed. This means that only one \(LS\)-combination is possible:

\[ (L,S)=\left(2,\tfrac{1}{2}\right) \]
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.

Hide 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)
_images/fe42e3b12985d50dcaf2c014275fad29cc881c4a977b95efbf3ed26ff4d76a16.svg

Bibliography#

Tip

Download this bibliography as BibTeX here.

[1]

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.

[2]

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 to None in order to deactivate mass conservation.

  • particle_db (Optional) – Custom ParticleCollection object. Defaults to the ParticleCollection returned by load_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'})}
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 in EM and STRONG and ["strong"] results in STRONG.

  • 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. Uses load_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 file additional_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.

asdict(instance: object) dict[source]#
fromdict(definition: dict) object[source]#
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 language str.

Only works for objects that can be represented as a graph, particularly a MutableTransition or a list of MutableTransition instances.

Parameters:
  • instance – the input object that is to be rendered as DOT (graphviz) language.

  • strip_spin – Normally, each MutableTransition has a Particle with a spin projection on its edges. This option hides the projections, leaving only Particle 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 a MutableTransition). 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.

load(filename: str) object[source]#
write(instance: object, filename: str) None[source]#
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 supports set and frozenset.

>>> 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 a TypeError).

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.

ensure_nested_list(nested_list: List[str] | List[List[str]]) List[List[str]][source]#
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:

  1. GraphElementRule that work on individual graph edges or nodes.

  2. EdgeQNConservationRule that work on the interaction level, which use ingoing edges, outgoing edges as arguments. E.g.: ChargeConservation.

  3. 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.

class GraphElementRule(*args, **kwargs)[source]#

Bases: Protocol

__call__(_GraphElementRule__qns: Any) bool[source]#

Call self as a function.

class EdgeQNConservationRule(*args, **kwargs)[source]#

Bases: Protocol

__call__(_EdgeQNConservationRule__ingoing_edge_qns: List[Any], _EdgeQNConservationRule__outgoing_edge_qns: List[Any]) bool[source]#

Call self as a function.

class ConservationRule(*args, **kwargs)[source]#

Bases: Protocol

__call__(_ConservationRule__ingoing_edge_qns: List[Any], _ConservationRule__outgoing_edge_qns: List[Any], _ConservationRule__node_qns: Any) bool[source]#

Call self as a function.

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.

__call__(ingoing_edge_qns: List[charge], outgoing_edge_qns: List[charge]) bool[source]#

Call self as a function.

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.

__call__(ingoing_edge_qns: List[charmness], outgoing_edge_qns: List[charmness]) bool[source]#

Call self as a function.

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

parity: parity[source]#
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]#
pid: pid[source]#
c_parity: c_parity | None[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]#
pid: pid[source]#
g_parity: g_parity | None[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]#
pid: pid[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

charge: charge[source]#
isospin_projection: isospin_projection | None[source]#
strangeness: strangeness | None[source]#
charmness: charmness | None[source]#
bottomness: bottomness | None[source]#
topness: topness | 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.

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 of isospin, \(B\) is baryon_number, \(S\) is strangeness, \(C\) is charmness, \(B'\) is bottomness, and \(T\) is topness.

class MassEdgeInput(mass, width=None)[source]#

Bases: object

mass: mass[source]#
width: width | None[source]#
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.

magnitude: float[source]#
projection: float[source]#
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. Hence Particle instances do not contain spin projection information.

Particle instances are uniquely defined by their quantum numbers and properties like mass. The name and pid are therefore just labels that are not taken into account when checking if two Particle instances are equal.

Note

As opposed to classes such as EdgeQuantumNumbers and NodeQuantumNumbers, the Particle class serves as an interface to the user (see Particle database).

name: str[source]#
pid: int[source]#
latex: str | None[source]#
spin: float[source]#
mass: float[source]#
width: float[source]#
charge: int[source]#
isospin: Spin | None[source]#
strangeness: int[source]#
charmness: int[source]#
bottomness: int[source]#
topness: int[source]#
baryon_number: int[source]#
electron_lepton_number: int[source]#
muon_lepton_number: int[source]#
tau_lepton_number: int[source]#
parity: Parity | None[source]#
c_parity: Parity | None[source]#
g_parity: Parity | None[source]#
is_lepton() bool[source]#
class ParticleCollection(particles: Iterable[Particle] | None = None)[source]#

Bases: MutableSet

Searchable collection of immutable Particle instances.

add(value: Particle) None[source]#

Add an element.

discard(value: Particle | str) None[source]#

Remove an element. Do not raise an exception if absent.

find(search_term: int | str) Particle[source]#

Search for a particle by either name (str) or PID (int).

filter(function: Callable[[Particle], bool]) ParticleCollection[source]#

Search by Particle properties using a lambda 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']
update(other: Iterable[Particle]) None[source]#
property names: List[str][source]#
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 Parity(value: int)[source]#

Bases: object

value: int[source]#
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 in additive_quantum_number_rule. You can also create data classes (see attrs.define()) with data members that are typed as the data members of EdgeQuantumNumbers (see for example HelicityParityEdgeInput) and use them in conservation rules that satisfy the appropriate rule protocol (see ConservationRule, EdgeQNConservationRule).

pid()[source]#
mass()[source]#
width()[source]#
spin_magnitude()[source]#
spin_projection()[source]#
charge()[source]#
isospin_magnitude()[source]#
isospin_projection()[source]#
strangeness()[source]#
charmness()[source]#
bottomness()[source]#
topness()[source]#
baryon_number()[source]#
electron_lepton_number()[source]#
muon_lepton_number()[source]#
tau_lepton_number()[source]#
parity()[source]#
c_parity()[source]#
g_parity()[source]#
class NodeQuantumNumbers[source]#

Bases: object

Definition of quantum numbers for interaction nodes.

l_magnitude()[source]#
l_projection()[source]#
s_magnitude()[source]#
s_projection()[source]#
parity_prefactor()[source]#
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, the InteractionProperties class serves as an interface to the user.

l_magnitude: int | None[source]#
l_projection: int | None[source]#
s_magnitude: float | None[source]#
s_projection: float | None[source]#
parity_prefactor: float | None[source]#
arange(x_1: float, x_2: float, delta: float = 1.0) Generator[float, None, None][source]#

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.

STRONG = 1[source]#
EM = 2[source]#
WEAK = 3[source]#
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.

class NumberOfThreads[source]#

Bases: object

classmethod get() int[source]#
classmethod set(n_cores: int | None) None[source]#

Set the number of threads; use None for all available cores.

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]#
qn_domains: Dict[Any, list][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]#
qn_domains: Dict[Any, list][source]#
interaction_strength: float[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]#
property topology: Topology[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]#
not_executed_node_rules: Dict[int, Set[str]][source]#
violated_node_rules: Dict[int, Set[str]][source]#
not_executed_edge_rules: Dict[int, Set[str]][source]#
violated_edge_rules: Dict[int, Set[str]][source]#
extend(other_result: QNResult) None[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:

QNResult

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:

QNResult

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 reference state 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 InteractionDeterminator[source]#

Bases: ABC

Interface for interaction determination.

abstract check(in_states: List[Tuple[Particle, float]], out_states: List[Tuple[Particle, float]], interactions: InteractionProperties) List[InteractionType][source]#
class GammaCheck[source]#

Bases: InteractionDeterminator

Conservation check for photons.

check(in_states: List[Tuple[Particle, float]], out_states: List[Tuple[Particle, float]], interactions: InteractionProperties) List[InteractionType][source]#
class LeptonCheck[source]#

Bases: InteractionDeterminator

Conservation check lepton numbers.

check(in_states: List[Tuple[Particle, float]], out_states: List[Tuple[Particle, float]], interactions: InteractionProperties) List[InteractionType][source]#
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

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 with attr.frozen() and contain Mapping-like attributes. If these attributes were to be implemented with a normal dict, the instance is strictly speaking still mutable (even if those attributes are a property) 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 a Topology if its originating_node_id is None (see incoming_edge_ids).

ending_node_id: int | None[source]#

Node ID where the Edge ends.

An Edge is outgoing from a Topology if its ending_node_id is None (see outgoing_edge_ids).

get_connected_nodes() Set[int][source]#

Get all node IDs to which the Edge is connected.

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 a Topology 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 a topology.

As opposed to a MutableTopology, a Topology is frozen, hashable, and ordered, so that it can be used as a kind of fingerprint for a Transition. In addition, the IDs of edges are guaranteed to be sequential integers and follow a specific pattern:

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=...)
_images/graphviz_3.svg
nodes: FrozenSet[int][source]#

A node is a point where different edges connect.

edges: FrozenDict[int, Edge][source]#

Mapping of edge IDs to their corresponding Edge definition.

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.

intermediate_edge_ids: FrozenSet[int][source]#

Edge IDs of edges that connect two nodes.

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.

get_edge_ids_ingoing_to_node(node_id: int) Set[int][source]#
get_edge_ids_outgoing_from_node(node_id: int) Set[int][source]#
get_originating_final_state_edge_ids(node_id: int) Set[int][source]#
get_originating_initial_state_edge_ids(node_id: int) Set[int][source]#
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
swap_edges(edge_id1: int, edge_id2: int) Topology[source]#
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.

Parameters:
  • topology – The Topology on which to perform the search.

  • edge_ids ([int]) – A list of edge ids for which the origin node is searched for.

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 a Topology (see e.g. SimpleStateTransitionTopologyBuilder). It does not have restrictions on the numbering of edge and node IDs.

nodes: Set[int][source]#

See Topology.nodes.

edges: Dict[int, Edge][source]#

See Topology.edges.

add_node(node_id: int) None[source]#

Adds a node with number node_id.

Raises:

ValueError – if node_id already exists in nodes.

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 in edges.

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:
  • ingoing_edge_ids ([int]) – list of edge ids, that will be attached

  • node_id (int) – id of the node to which the edges will be attached

Raises:
  • ValueError – if an edge not doesn’t exist.

  • ValueError – if an edge ID is already an ingoing node.

attach_edges_to_node_outgoing(outgoing_edge_ids: Iterable[int], node_id: int) None[source]#
organize_edge_ids() MutableTopology[source]#

Organize edge IDS so that they lie in range [-m, n+i].

Here, m is the number of incoming_edge_ids, n is the number of outgoing_edge_ids, and i is the number of intermediate_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 this MutableTopology.

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.

number_of_ingoing_edges: int[source]#
number_of_outgoing_edges: int[source]#
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.

build(number_of_initial_edges: int, number_of_final_edges: int) Tuple[Topology, ...][source]#
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-isomorphic Topology 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
_images/graphviz_0.svg
_images/graphviz_1.svg
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() and conservation_rules.

Parameters:

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
_images/graphviz_2.svg
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 and nodes of its topology. Since a Topology behaves like a Feynman graph, edges are considered as “states” and nodes are considered as interactions between those states.

There are two implementation classes:

These classes are also provided with mixin attributes initial_states, final_states, intermediate_states, and filter_states().

abstract property topology: Topology[source]#

Topology over which states and interactions are defined.

abstract property states: Mapping[int, EdgeType][source]#

Mapping of properties over its topology edges.

abstract property interactions: Mapping[int, NodeType][source]#

Mapping of properties over its topology nodes.

property initial_states: Dict[int, EdgeType][source]#

Properties for the incoming_edge_ids.

property final_states: Dict[int, EdgeType][source]#

Properties for the outgoing_edge_ids.

property intermediate_states: Dict[int, EdgeType][source]#

Properties for the intermediate edges (connecting two nodes).

filter_states(edge_ids: Iterable[int]) Dict[int, EdgeType][source]#

Filter states by a selection of edge_ids.

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.

topology: Topology[source]#
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.

topology: Topology[source]#
states: Dict[int, EdgeType][source]#
interactions: Dict[int, NodeType][source]#
compare(other: MutableTransition, state_comparator: Callable[[EdgeType, EdgeType], bool] | None = None, interaction_comparator: Callable[[NodeType, NodeType], bool] | None = None) bool[source]#
swap_edges(edge_id1: int, edge_id2: int) None[source]#
freeze() FrozenTransition[EdgeType, NodeType][source]#

Convert into a FrozenTransition.

transition#

import qrules.transition

Find allowed transitions between an initial and final state.

class SolvingMode(value)[source]#

Bases: Enum

Types of modes for solving.

FAST = 1[source]#

Find “likeliest” solutions only.

FULL = 2[source]#

Find all possible solutions.

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

not_executed_node_rules: Dict[int, Set[str]][source]#
violated_node_rules: Dict[int, Set[str]][source]#
not_executed_edge_rules: Dict[int, Set[str]][source]#
violated_edge_rules: Dict[int, Set[str]][source]#
extend(other_result: ExecutionInfo, intersect_violations: bool = False) None[source]#
clear() 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.

topology: Topology[source]#

Topology over which the problem set is defined.

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.

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]#
property formalism: str[source]#
add_final_state_grouping(fs_group: List[str] | List[List[str]]) 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]#
create_problem_sets() Dict[float, List[ProblemSet]][source]#
find_solutions(problem_sets: Dict[float, List[ProblemSet]]) ReactionInfo[source]#

Check for solutions for a specific set of interaction settings.

find_quantum_number_transitions(problem_sets: Dict[float, List[ProblemSet]]) Dict[float, List[Tuple[QNProblemSet, QNResult]]][source]#

Find allowed transitions purely in terms of quantum number sets.

class State(particle: Particle, spin_projection: SupportsFloat)[source]#

Bases: object

particle: Particle[source]#
spin_projection: float[source]#
StateTransition: TypeAlias = 'FrozenTransition[State, InteractionProperties]'#

Transition of some initial State to a final State.

class ReactionInfo(transitions: Iterable[FrozenTransition[State, InteractionProperties]], formalism: str)[source]#

Bases: object

Ordered collection of StateTransition instances.

transitions: Tuple[FrozenTransition[State, InteractionProperties], ...][source]#
formalism: str[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]#