Tutorial for GenMultiDecay class¶
This tutorial shows how phasespace.fromdecay.GenMultiDecay
can be used.
In order to use this functionality, you need to install the extra fromdecay
, for example through
pip install phasespace[fromdecay]
.
This submodule makes it possible for phasespace
and DecayLanguage
to work together.
More generally, GenMultiDecay
can also be used as a high-level interface for simulating particles that can decay in multiple different ways.
# Import libraries
from pprint import pprint
import zfit
from particle import Particle
from decaylanguage import DecFileParser, DecayChainViewer
import tensorflow as tf
from phasespace.fromdecay import GenMultiDecay
/home/docs/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/zfit/__init__.py:37: UserWarning: TensorFlow warnings are by default suppressed by zfit. In order to show them, set the environment variable ZFIT_DISABLE_TF_WARNINGS=0. In order to suppress the TensorFlow warnings AND this warning, set ZFIT_DISABLE_TF_WARNINGS=1.
warnings.warn("TensorFlow warnings are by default suppressed by zfit."
Quick Intro to DecayLanguage¶
DecayLanguage can be used to parse and view .dec files. These files contain information about how a particle decays and with which probability. For more information about DecayLanguage and .dec files, see the DecayLanguage documentation.
We will begin by parsing a .dec file using DecayLanguage:
parser = DecFileParser('../tests/fromdecay/example_decays.dec')
parser.parse()
From the parser
variable, one can access a certain decay for a particle using parser.build_decay_chains
. This will be a dict
that contains all information about how the mother particle, daughter particles etc. decay.
pi0_chain = parser.build_decay_chains("pi0")
pprint(pi0_chain)
{
'pi0'
:
[
{
'bf'
:
0.988228297
,
'fs'
:
['gamma', 'gamma']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
,
{
'bf'
:
0.011738247
,
'fs'
:
['e+', 'e-', 'gamma']
,
'model'
:
'PI0_DALITZ'
,
'model_params'
:
''
}
,
{
'bf'
:
3.3392e-05
,
'fs'
:
['e+', 'e+', 'e-', 'e-']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
,
{
'bf'
:
6.5e-08
,
'fs'
:
['e+', 'e-']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
]
}
This dict
can also be displayed in a more human-readable way using DecayChainViewer
:
DecayChainViewer(pi0_chain)
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/backend/execute.py in run_check(cmd, input_lines, encoding, capture_output, quiet, **kwargs)
82 assert iter(input_lines) is input_lines
---> 83 proc = _run_input_lines(cmd, input_lines, kwargs=kwargs)
84 else:
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/backend/execute.py in _run_input_lines(cmd, input_lines, kwargs)
102 def _run_input_lines(cmd, input_lines, *, kwargs):
--> 103 popen = subprocess.Popen(cmd, stdin=subprocess.PIPE, **kwargs)
104
~/.asdf/installs/python/3.8.12/lib/python3.8/subprocess.py in __init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, encoding, errors, text)
857
--> 858 self._execute_child(args, executable, preexec_fn, close_fds,
859 pass_fds, cwd, env,
~/.asdf/installs/python/3.8.12/lib/python3.8/subprocess.py in _execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, start_new_session)
1703 err_msg = os.strerror(errno_num)
-> 1704 raise child_exception_type(errno_num, err_msg, err_filename)
1705 raise child_exception_type(err_msg)
FileNotFoundError: [Errno 2] No such file or directory: PosixPath('dot')
The above exception was the direct cause of the following exception:
ExecutableNotFound Traceback (most recent call last)
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/IPython/core/formatters.py in __call__(self, obj)
343 method = get_real_method(obj, self.print_method)
344 if method is not None:
--> 345 return method()
346 return None
347 else:
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/decaylanguage/decay/viewer.py in _repr_svg_(self)
241 IPython display in SVG format.
242 """
--> 243 return self._graph._repr_svg_()
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/jupyter_integration.py in _repr_svg_(self)
10
11 def _repr_svg_(self):
---> 12 return self.pipe(format='svg', encoding=self._encoding)
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/piping.py in pipe(self, format, renderer, formatter, quiet, engine, encoding)
111 if codecs.lookup(encoding) is codecs.lookup(self._encoding):
112 # common case: both stdin and stdout need the same encoding
--> 113 return self._pipe_lines_string(*args, encoding=encoding, **kwargs)
114 raw = self._pipe_lines(*args, input_encoding=self._encoding, **kwargs)
115 return raw.decode(encoding)
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/backend/piping.py in pipe_lines_string(engine, format, input_lines, encoding, renderer, formatter, quiet)
195 kwargs = {'input_lines': input_lines, 'encoding': encoding}
196
--> 197 proc = execute.run_check(cmd, capture_output=True, quiet=quiet, **kwargs)
198 return proc.stdout
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/backend/execute.py in run_check(cmd, input_lines, encoding, capture_output, quiet, **kwargs)
86 except OSError as e:
87 if e.errno == errno.ENOENT:
---> 88 raise ExecutableNotFound(cmd) from e
89 raise
90
ExecutableNotFound: failed to execute PosixPath('dot'), make sure the Graphviz executables are on your systems' PATH
<decaylanguage.decay.viewer.DecayChainViewer at 0x7f9e580f0e00>
Creating a GenMultiDecay object¶
A regular phasespace.GenParticle
instance would not be able to simulate this decay, since the \(\pi^0\) particle can decay in four different ways. However, a GenMultiDecay
object can be created directly from a DecayLanguage dict:
pi0_decay = GenMultiDecay.from_dict(pi0_chain)
When creating a GenMultiDecay
object, the DecayLanguage dict is “unpacked” into separate GenParticle instances, where each GenParticle instance corresponds to one way that the particle can decay.
These GenParticle instances and the probabilities of that decay mode can be accessed via GenMultiDecay.gen_particles
. This is a list of tuples, where the first element in the tuple is the probability and the second element is the GenParticle.
for probability, particle in pi0_decay.gen_particles:
print(f"There is a probability of {probability} "
f"that pi0 decays into {', '.join(child.name for child in particle.children)}")
There is a probability of 0.988228297 that pi0 decays into gamma, gamma [0]
There is a probability of 0.011738247 that pi0 decays into e+, e-, gamma [1]
There is a probability of 3.3392e-05 that pi0 decays into e+ [0], e+ [1], e- [0], e- [1]
There is a probability of 6.5e-08 that pi0 decays into e+ [2], e- [2]
One can simulate this decay using the .generate
method, which works the same as the GenParticle.generate
method.
When calling the GenMultiDecay.generate
method, it internally calls the generate method on the of the GenParticle instances in GenMultiDecay.gen_particles
. The outputs are placed in a list, which is returned.
weights, events = pi0_decay.generate(n_events=10_000)
print("Number of events for each decay mode:", ", ".join(str(len(w)) for w in weights))
Number of events for each decay mode:
9883, 117
We can confirm that the counts above are close to the expected counts based on the probabilities.
Changing mass settings¶
Since DecayLanguage dicts do not contain any information about the mass of a particle, the fromdecay
submodule uses the particle package to find the mass of a particle based on its name.
The mass can either be a constant value or a function (besides the top particle, which is always a constant).
These settings can be modified by passing in additional parameters to GenMultiDecay.from_dict
.
There are two optional parameters that can be passed to GenMultiDecay.from_dict
: tolerance
and mass_converter
.
Constant vs variable mass¶
If a particle has a width less than tolerance
, its mass is set to a constant value.
This will be demonsttrated with the decay below:
dsplus_chain = parser.build_decay_chains("D*+", stable_particles=["D+"])
DecayChainViewer(dsplus_chain)
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/backend/execute.py in run_check(cmd, input_lines, encoding, capture_output, quiet, **kwargs)
82 assert iter(input_lines) is input_lines
---> 83 proc = _run_input_lines(cmd, input_lines, kwargs=kwargs)
84 else:
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/backend/execute.py in _run_input_lines(cmd, input_lines, kwargs)
102 def _run_input_lines(cmd, input_lines, *, kwargs):
--> 103 popen = subprocess.Popen(cmd, stdin=subprocess.PIPE, **kwargs)
104
~/.asdf/installs/python/3.8.12/lib/python3.8/subprocess.py in __init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, encoding, errors, text)
857
--> 858 self._execute_child(args, executable, preexec_fn, close_fds,
859 pass_fds, cwd, env,
~/.asdf/installs/python/3.8.12/lib/python3.8/subprocess.py in _execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, start_new_session)
1703 err_msg = os.strerror(errno_num)
-> 1704 raise child_exception_type(errno_num, err_msg, err_filename)
1705 raise child_exception_type(err_msg)
FileNotFoundError: [Errno 2] No such file or directory: PosixPath('dot')
The above exception was the direct cause of the following exception:
ExecutableNotFound Traceback (most recent call last)
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/IPython/core/formatters.py in __call__(self, obj)
343 method = get_real_method(obj, self.print_method)
344 if method is not None:
--> 345 return method()
346 return None
347 else:
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/decaylanguage/decay/viewer.py in _repr_svg_(self)
241 IPython display in SVG format.
242 """
--> 243 return self._graph._repr_svg_()
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/jupyter_integration.py in _repr_svg_(self)
10
11 def _repr_svg_(self):
---> 12 return self.pipe(format='svg', encoding=self._encoding)
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/piping.py in pipe(self, format, renderer, formatter, quiet, engine, encoding)
111 if codecs.lookup(encoding) is codecs.lookup(self._encoding):
112 # common case: both stdin and stdout need the same encoding
--> 113 return self._pipe_lines_string(*args, encoding=encoding, **kwargs)
114 raw = self._pipe_lines(*args, input_encoding=self._encoding, **kwargs)
115 return raw.decode(encoding)
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/backend/piping.py in pipe_lines_string(engine, format, input_lines, encoding, renderer, formatter, quiet)
195 kwargs = {'input_lines': input_lines, 'encoding': encoding}
196
--> 197 proc = execute.run_check(cmd, capture_output=True, quiet=quiet, **kwargs)
198 return proc.stdout
~/checkouts/readthedocs.org/user_builds/phasespace/envs/1.5.0/lib/python3.8/site-packages/graphviz/backend/execute.py in run_check(cmd, input_lines, encoding, capture_output, quiet, **kwargs)
86 except OSError as e:
87 if e.errno == errno.ENOENT:
---> 88 raise ExecutableNotFound(cmd) from e
89 raise
90
ExecutableNotFound: failed to execute PosixPath('dot'), make sure the Graphviz executables are on your systems' PATH
<decaylanguage.decay.viewer.DecayChainViewer at 0x7f9ddffba540>
print(f"pi0 width = {Particle.from_evtgen_name('pi0').width}\n"
f"D0 width = {Particle.from_evtgen_name('D0').width}")
pi0 width = 7.81e-06
D0 width = 1.605e-09
\(\pi^0\) has a greater width than \(D^0\). If the tolerance is set to a value between their widths, the \(D^0\) particle will have a constant mass while \(\pi^0\) will not.
dstar_decay = GenMultiDecay.from_dict(dsplus_chain, tolerance=1e-8)
# Loop over D0 and pi+ particles, see graph above
for particle in dstar_decay.gen_particles[0][1].children:
# If a particle width is less than tolerance or if it does not have any children, its mass will be fixed.
assert particle.has_fixed_mass
# Loop over D+ and pi0. See above.
for particle in dstar_decay.gen_particles[1][1].children:
if particle.name == "pi0":
assert not particle.has_fixed_mass
Configuring mass functions¶
By default, the mass function used for variable mass is the relativistic Breit-Wigner distribution. This can however be changed. If you want the mother particle to have a specific mass function for a specific decay, you can add a zfit
parameter to the DecayLanguage dict. Consider for example the previous \(D^{*+}\) example:
dsplus_custom_mass_func = dsplus_chain.copy()
dsplus_chain_subset = dsplus_custom_mass_func["D*+"][1]["fs"][1]
print("Before:")
pprint(dsplus_chain_subset)
# Set the mass function of pi0 to a gaussian distribution when it decays into two photons (gamma)
dsplus_chain_subset["pi0"][0]["zfit"] = "gauss"
print("After:")
pprint(dsplus_chain_subset)
Before:
{
'pi0'
:
[
{
'bf'
:
0.988228297
,
'fs'
:
['gamma', 'gamma']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
,
{
'bf'
:
0.011738247
,
'fs'
:
['e+', 'e-', 'gamma']
,
'model'
:
'PI0_DALITZ'
,
'model_params'
:
''
}
,
{
'bf'
:
3.3392e-05
,
'fs'
:
['e+', 'e+', 'e-', 'e-']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
,
{
'bf'
:
6.5e-08
,
'fs'
:
['e+', 'e-']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
]
}
After:
{
'pi0'
:
[
{
'bf'
:
0.988228297
,
'fs'
:
['gamma', 'gamma']
,
'model'
:
'PHSP'
,
'model_params'
:
''
,
'zfit'
:
'gauss'
}
,
{
'bf'
:
0.011738247
,
'fs'
:
['e+', 'e-', 'gamma']
,
'model'
:
'PI0_DALITZ'
,
'model_params'
:
''
}
,
{
'bf'
:
3.3392e-05
,
'fs'
:
['e+', 'e+', 'e-', 'e-']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
,
{
'bf'
:
6.5e-08
,
'fs'
:
['e+', 'e-']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
]
}
Notice the added zfit
field to the first decay mode of the \(\pi^0\) particle. This dict can then be passed to GenMultiDecay.from_dict
, like before.
GenMultiDecay.from_dict(dsplus_custom_mass_func)
<phasespace.fromdecay.genmultidecay.GenMultiDecay at 0x7f9ddc2faac0>
The built-in supported mass function names are gauss
, bw
, and relbw
, with gauss
being the gaussian distribution, bw
being the Breit-Wigner distribution, and relbw
being the relativistic Breit-Wigner distribution.
If a non-supported value for the zfit
parameter is used or if it is not specified, it will automatically use the relativistic Breit-Wigner distribution. This behavior can be changed by changing the value of GenMultiDecay.DEFAULT_MASS_FUNC
to a different string, e.g., "gauss"
.
It is also possible to add your own mass functions besides the built-in ones. You should then create a function that takes the mass and width of a particle and returns a mass function which with the format that is used for all phasespace mass functions. Below is an example of a custom gaussian distribution (implemented in the same way as the built-in gaussian distribution), which uses zfit
PDFs:
def custom_gauss(mass, width):
particle_mass = tf.cast(mass, tf.float64)
particle_width = tf.cast(width, tf.float64)
# This is the actual mass function that will be returned
def mass_func(min_mass, max_mass, n_events):
min_mass = tf.cast(min_mass, tf.float64)
max_mass = tf.cast(max_mass, tf.float64)
# Use a zfit PDF
pdf = zfit.pdf.Gauss(mu=particle_mass, sigma=particle_width, obs="")
iterator = tf.stack([min_mass, max_mass], axis=-1)
return tf.vectorized_map(
lambda lim: pdf.sample(1, limits=(lim[0], lim[1])), iterator
)
return mass_func
This function can then be passed to GenMultiDecay.from_dict
as a dict, where the key specifies the zfit
parameter name. In the example below, it is set to "custom_gauss"
. However, this name can be chosen arbitrarily and does not need to be the same as the function name.
dsplus_chain_subset = dsplus_custom_mass_func["D*+"][1]["fs"][1]
print("Before:")
pprint(dsplus_chain_subset)
# Set the mass function of pi0 to the custom gaussian distribution
# when it decays into an electron-positron pair and a photon (gamma)
dsplus_chain_subset["pi0"][1]["zfit"] = "custom_gauss"
print("After:")
pprint(dsplus_chain_subset)
Before:
{
'pi0'
:
[
{
'bf'
:
0.988228297
,
'fs'
:
['gamma', 'gamma']
,
'model'
:
'PHSP'
,
'model_params'
:
''
,
'zfit'
:
'gauss'
}
,
{
'bf'
:
0.011738247
,
'fs'
:
['e+', 'e-', 'gamma']
,
'model'
:
'PI0_DALITZ'
,
'model_params'
:
''
}
,
{
'bf'
:
3.3392e-05
,
'fs'
:
['e+', 'e+', 'e-', 'e-']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
,
{
'bf'
:
6.5e-08
,
'fs'
:
['e+', 'e-']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
]
}
After:
{
'pi0'
:
[
{
'bf'
:
0.988228297
,
'fs'
:
['gamma', 'gamma']
,
'model'
:
'PHSP'
,
'model_params'
:
''
,
'zfit'
:
'gauss'
}
,
{
'bf'
:
0.011738247
,
'fs'
:
['e+', 'e-', 'gamma']
,
'model'
:
'PI0_DALITZ'
,
'model_params'
:
''
,
'zfit'
:
'custom_gauss'
}
,
{
'bf'
:
3.3392e-05
,
'fs'
:
['e+', 'e+', 'e-', 'e-']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
,
{
'bf'
:
6.5e-08
,
'fs'
:
['e+', 'e-']
,
'model'
:
'PHSP'
,
'model_params'
:
''
}
]
}
GenMultiDecay.from_dict(dsplus_custom_mass_func, {"custom_gauss": custom_gauss})
<phasespace.fromdecay.genmultidecay.GenMultiDecay at 0x7f9ddc2fa820>