# WARNING: advised to install a specific version, e.g. qrules==0.1.2
%pip install -q qrules[doc,viz] IPython
import os
STATIC_WEB_PAGE = {"EXECUTE_NB", "READTHEDOCS"}.intersection(os.environ)
{autolink-concat}
:::{warning}
Currently the main user-interface is the StateTransitionManager
. There is work in progress to remove it and split its functionality into several functions/classes to separate concerns
and to facilitate the modification of intermediate results like the filtering of QNProblemSet
s, setting allowed interaction types, etc. (see below)
:::
{margin}
```{warning}
{class}`graphviz.Source` requires your system to have DOT installed, see {doc}`Installation <graphviz:index>`.
```
The {mod}~qrules.io
module allows you to convert {class}.MutableTransition
, {class}.Topology
instances, and {class}.ProblemSet
s to DOT language with {func}.asdot
. You can visualize its output with third-party libraries, such as Graphviz. This is particularly useful after running {meth}~.StateTransitionManager.find_solutions
, which produces a {class}.ReactionInfo
object with a {class}.list
of {class}.MutableTransition
instances (see {doc}/usage/reaction
).
First of all, here are is an example of how to visualize a group of {class}.Topology
instances. We use {func}.create_isobar_topologies
and {func}.create_n_body_topology
to create a few standard topologies.
import graphviz
from IPython.display import display
import qrules
from qrules.conservation_rules import (
parity_conservation,
spin_magnitude_conservation,
spin_validity,
)
from qrules.particle import Spin
from qrules.quantum_numbers import EdgeQuantumNumbers, NodeQuantumNumbers
from qrules.solving import (
CSPSolver,
dict_set_intersection,
filter_quantum_number_problem_set,
)
from qrules.topology import create_isobar_topologies, create_n_body_topology
from qrules.transition import State
topology = create_n_body_topology(2, 4)
graphviz.Source(qrules.io.asdot(topology, render_initial_state_id=True))
Note the IDs of the {attr}~.Topology.nodes
is also rendered if there is more than node:
topologies = create_isobar_topologies(4)
graphviz.Source(qrules.io.asdot(topologies))
This can be turned on or off with the arguments of {func}.asdot
:
topologies = create_isobar_topologies(3)
graphviz.Source(qrules.io.asdot(topologies, render_node=False))
{func}.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))
(problem-sets)=
.ProblemSet
s¶As noted in {doc}reaction
, the {class}.StateTransitionManager
provides more control than the façade function {func}.generate_transitions
. One advantages, is that the {class}.StateTransitionManager
first generates a set of {class}.ProblemSet
s with {meth}.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 {meth}.create_problem_sets
is a {obj}dict
with {obj}float
values as keys (representing the interaction strength) and {obj}list
s of {obj}.ProblemSet
s as values.
sorted(problem_sets, reverse=True)
problem_set = problem_sets[60.0][0]
dot = qrules.io.asdot(problem_set, render_node=True)
graphviz.Source(dot)
As noted in {ref}usage/reaction:3. Find solutions
, a {obj}.ProblemSet
can be fed to {meth}.StateTransitionManager.find_solutions
directly to get a {obj}.ReactionInfo
object. {obj}.ReactionInfo
is a final result that consists of {obj}.Particle
s, but in the intermediate steps, QRules works with sets of quantum numbers. One can inspect these intermediate generated quantum numbers by using {meth}.find_quantum_number_transitions
and inspecting is output. Note that the resulting object is again a {obj}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()}
The list of solutions consist of a {obj}tuple
of a {obj}.QNProblemSet
(compare {ref}problem-sets
) and a {obj}.QNResult
:
strong_qn_solutions = qn_solutions[3600.0]
qn_problem_set, qn_result = strong_qn_solutions[0]
dot = qrules.io.asdot(qn_problem_set, render_node=True)
graphviz.Source(dot)
dot = qrules.io.asdot(qn_result, render_node=True)
graphviz.Source(dot)
Sometimes, only a certain subset of quantum numbers and conservation rules are relevant, or the number of solutions the {class}.StateTransitionManager
gives by default is too large for the follow-up analysis.
The {func}.filter_quantum_number_problem_set
function can be used to produce a {class}.QNProblemSet
where only the desired quantum numbers and conservation rules are considered when fed back to the solver.
desired_edge_properties = {EdgeQuantumNumbers.spin_magnitude, EdgeQuantumNumbers.parity}
desired_node_properties = {
NodeQuantumNumbers.l_magnitude,
NodeQuantumNumbers.s_magnitude,
} # has to be reused in the CSPSolver-constructor
filtered_qn_problem_set = filter_quantum_number_problem_set(
qn_problem_set,
edge_rules={spin_validity},
node_rules={spin_magnitude_conservation, parity_conservation},
edge_properties=desired_edge_properties,
node_properties=desired_node_properties,
)
dot = qrules.io.asdot(filtered_qn_problem_set, render_node=True)
graphviz.Source(dot)
:::{warning}
The next cell will use some (currently) internal functionality. As stated at the top, a workflow similar to this will be used in future versions of {mod}qrules
, see e.g. ComPWA/qrules#305. Manual setup of the {obj}.CSPSolver
like in here will then also not be necessary.
:::
solver = CSPSolver([
dict_set_intersection(
qrules.system_control.create_edge_properties(part),
desired_edge_properties,
)
for part in qrules.particle.load_pdg()
])
filtered_qn_solutions = solver.find_solutions(filtered_qn_problem_set)
filtered_qn_result = filtered_qn_solutions.solutions[6]
dot = qrules.io.asdot(filtered_qn_result, render_node=True)
graphviz.Source(dot)
.StateTransition
s¶After finding the {ref}usage/visualize:Quantum number solutions
, QRules finds {obj}.Particle
definitions that match these quantum numbers. All these steps are hidden in the convenience functions {meth}.StateTransitionManager.find_solutions
and {func}.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 {ref}usage/reaction:3. Find solutions
, the {attr}~.ReactionInfo.transitions
contain all spin projection combinations (which is necessary for the {mod}ampform
package). It is possible to convert all these solutions to DOT language with {func}~.asdot
. To avoid visualizing all solutions, we just take a subset of the {attr}~.ReactionInfo.transitions
:
dot = qrules.io.asdot(reaction.transitions[::50][:3]) # just some selection
This {class}str
of DOT language for the list of {class}.MutableTransition
instances can then be visualized with a third-party library, for instance, with {class}graphviz.Source
:
import graphviz
dot = qrules.io.asdot(
reaction.transitions[::50][:3], render_node=False
) # just some selection
graphviz.Source(dot)
You can also serialize the DOT string to file with {func}.io.write
. The file extension for a DOT file is .gv
:
qrules.io.write(reaction, "decay_topologies_with_spin.gv")
Since this list of all possible spin projections {attr}~.ReactionInfo.transitions
is rather long, it is often useful to use strip_spin=True
or collapse_graphs=True
to bundle comparable graphs. First, {code}strip_spin=True
allows one collapse (ignore) the spin projections (we again show a selection only):
dot = qrules.io.asdot(reaction.transitions[:3], strip_spin=True)
graphviz.Source(dot)
or, with stripped node properties:
dot = qrules.io.asdot(reaction.transitions[:3], strip_spin=True, render_node=True)
graphviz.Source(dot)
{note}
By default, {func}`.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 {code}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)
The {meth}~.FrozenTransition.convert
method makes it possible to convert the types of its {attr}~.FrozenTransition.states
. This for instance allows us to only render the spin states on in a {class}.Transition
:
::::{margin}
:::{tip}
We use the fact that a {obj}.StateTransition
is frozen (and therefore hashable) to remove any duplicate transitions.
:::
::::
spin_transitions = sorted({
t.convert(lambda s: Spin(s.particle.spin, s.spin_projection))
for t in reaction.transitions
})
some_selection = spin_transitions[::67][:3]
dot = qrules.io.asdot(some_selection, render_node=True)
graphviz.Source(dot)
Or any other properties of a {class}.State
, such as masses or $J^{PC}(I^G)$ numbers:
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)
from fractions import Fraction
def render_jpc_ig(state: State) -> str:
particle = state.particle
text = render_fraction(particle.spin)
if particle.parity is not None:
text += render_sign(particle.parity)
if particle.c_parity is not None:
text += render_sign(particle.c_parity)
if particle.isospin is not None and particle.g_parity is not None:
text += "("
text += f"{render_fraction(particle.isospin.magnitude)}"
text += f"{render_sign(particle.g_parity)}"
text += ")"
return text
def render_fraction(value: float) -> str:
fraction = Fraction(value)
if fraction.denominator == 1:
return str(fraction.numerator)
return f"{fraction.numerator}/{fraction.denominator}"
def render_sign(parity: int) -> str:
if parity == -1:
return "⁻"
if parity == +1:
return "⁺"
raise NotImplementedError
jpc_ig_transitions = sorted({
t.convert(
state_converter=render_jpc_ig,
interaction_converter=lambda _: None,
)
for t in reaction.transitions
})
dot = qrules.io.asdot(jpc_ig_transitions, collapse_graphs=True)
graphviz.Source(dot)
:::{tip} Note that collapsing the graphs also works for custom edge properties. :::
The {func}.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))