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 dependencies, 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, DecayChain, DecayMode
import tensorflow as tf

from phasespace.fromdecay import GenMultiDecay
/home/docs/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/zfit/__init__.py:63: 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(
2024-04-01 23:19:16.324910: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:9261] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2024-04-01 23:19:16.324967: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:607] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2024-04-01 23:19:16.326167: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1515] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
No module named 'nlopt'

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)
File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/execute.py:76, in run_check(cmd, input_lines, encoding, quiet, **kwargs)
     75         kwargs['stdout'] = kwargs['stderr'] = subprocess.PIPE
---> 76     proc = _run_input_lines(cmd, input_lines, kwargs=kwargs)
     77 else:

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/execute.py:96, in _run_input_lines(cmd, input_lines, kwargs)
     95 def _run_input_lines(cmd, input_lines, *, kwargs):
---> 96     popen = subprocess.Popen(cmd, stdin=subprocess.PIPE, **kwargs)
     98     stdin_write = popen.stdin.write

File ~/.asdf/installs/python/3.11.6/lib/python3.11/subprocess.py:1026, in Popen.__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, user, group, extra_groups, encoding, errors, text, umask, pipesize, process_group)
   1023             self.stderr = io.TextIOWrapper(self.stderr,
   1024                     encoding=encoding, errors=errors)
-> 1026     self._execute_child(args, executable, preexec_fn, close_fds,
   1027                         pass_fds, cwd, env,
   1028                         startupinfo, creationflags, shell,
   1029                         p2cread, p2cwrite,
   1030                         c2pread, c2pwrite,
   1031                         errread, errwrite,
   1032                         restore_signals,
   1033                         gid, gids, uid, umask,
   1034                         start_new_session, process_group)
   1035 except:
   1036     # Cleanup if the child failed starting.

File ~/.asdf/installs/python/3.11.6/lib/python3.11/subprocess.py:1950, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session, process_group)
   1949         err_msg = os.strerror(errno_num)
-> 1950     raise child_exception_type(errno_num, err_msg, err_filename)
   1951 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)
File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/IPython/core/formatters.py:977, in MimeBundleFormatter.__call__(self, obj, include, exclude)
    974     method = get_real_method(obj, self.print_method)
    976     if method is not None:
--> 977         return method(include=include, exclude=exclude)
    978     return None
    979 else:

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/decaylanguage/decay/viewer.py:259, in DecayChainViewer._repr_mimebundle_(self, include, exclude, **kwargs)
    255 """
    256 IPython display helper.
    257 """
    258 try:
--> 259     return self._graph._repr_mimebundle_(
    260         include=include, exclude=exclude, **kwargs
    261     )
    262 except AttributeError:
    263     return {"image/svg+xml": self._graph._repr_svg_()}

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/jupyter_integration.py:98, in JupyterIntegration._repr_mimebundle_(self, include, exclude, **_)
     96 include = set(include) if include is not None else {self._jupyter_mimetype}
     97 include -= set(exclude or [])
---> 98 return {mimetype: getattr(self, method_name)()
     99         for mimetype, method_name in MIME_TYPES.items()
    100         if mimetype in include}

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/jupyter_integration.py:98, in <dictcomp>(.0)
     96 include = set(include) if include is not None else {self._jupyter_mimetype}
     97 include -= set(exclude or [])
---> 98 return {mimetype: getattr(self, method_name)()
     99         for mimetype, method_name in MIME_TYPES.items()
    100         if mimetype in include}

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/jupyter_integration.py:112, in JupyterIntegration._repr_image_svg_xml(self)
    110 def _repr_image_svg_xml(self) -> str:
    111     """Return the rendered graph as SVG string."""
--> 112     return self.pipe(format='svg', encoding=SVG_ENCODING)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/piping.py:104, in Pipe.pipe(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
     55 def pipe(self,
     56          format: typing.Optional[str] = None,
     57          renderer: typing.Optional[str] = None,
   (...)
     61          engine: typing.Optional[str] = None,
     62          encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
     63     """Return the source piped through the Graphviz layout command.
     64 
     65     Args:
   (...)
    102         '<?xml version='
    103     """
--> 104     return self._pipe_legacy(format,
    105                              renderer=renderer,
    106                              formatter=formatter,
    107                              neato_no_op=neato_no_op,
    108                              quiet=quiet,
    109                              engine=engine,
    110                              encoding=encoding)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/_tools.py:171, in deprecate_positional_args.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    162     wanted = ', '.join(f'{name}={value!r}'
    163                        for name, value in deprecated.items())
    164     warnings.warn(f'The signature of {func.__name__} will be reduced'
    165                   f' to {supported_number} positional args'
    166                   f' {list(supported)}: pass {wanted}'
    167                   ' as keyword arg(s)',
    168                   stacklevel=stacklevel,
    169                   category=category)
--> 171 return func(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/piping.py:121, in Pipe._pipe_legacy(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
    112 @_tools.deprecate_positional_args(supported_number=2)
    113 def _pipe_legacy(self,
    114                  format: typing.Optional[str] = None,
   (...)
    119                  engine: typing.Optional[str] = None,
    120                  encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
--> 121     return self._pipe_future(format,
    122                              renderer=renderer,
    123                              formatter=formatter,
    124                              neato_no_op=neato_no_op,
    125                              quiet=quiet,
    126                              engine=engine,
    127                              encoding=encoding)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/piping.py:149, in Pipe._pipe_future(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
    146 if encoding is not None:
    147     if codecs.lookup(encoding) is codecs.lookup(self.encoding):
    148         # common case: both stdin and stdout need the same encoding
--> 149         return self._pipe_lines_string(*args, encoding=encoding, **kwargs)
    150     try:
    151         raw = self._pipe_lines(*args, input_encoding=self.encoding, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/piping.py:212, in pipe_lines_string(engine, format, input_lines, encoding, renderer, formatter, neato_no_op, quiet)
    206 cmd = dot_command.command(engine, format,
    207                           renderer=renderer,
    208                           formatter=formatter,
    209                           neato_no_op=neato_no_op)
    210 kwargs = {'input_lines': input_lines, 'encoding': encoding}
--> 212 proc = execute.run_check(cmd, capture_output=True, quiet=quiet, **kwargs)
    213 return proc.stdout

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/execute.py:81, in run_check(cmd, input_lines, encoding, quiet, **kwargs)
     79 except OSError as e:
     80     if e.errno == errno.ENOENT:
---> 81         raise ExecutableNotFound(cmd) from e
     82     raise
     84 if not quiet and proc.stderr:

ExecutableNotFound: failed to execute PosixPath('dot'), make sure the Graphviz executables are on your systems' PATH
<decaylanguage.decay.viewer.DecayChainViewer at 0x7f1c45ee7000>

You can also create a decay using the DecayChain and DecayMode classes. However, a DecayChain can only contain one chain, i.e., a particle cannot decay in multiple ways.

dplus_decay = DecayMode(1, "K- pi+ pi+ pi0", model="PHSP")
pi0_decay = DecayMode(1, "gamma gamma")
dplus_single = DecayChain("D+", {"D+": dplus_decay, "pi0": pi0_decay})
DecayChainViewer(dplus_single.to_dict())
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/execute.py:76, in run_check(cmd, input_lines, encoding, quiet, **kwargs)
     75         kwargs['stdout'] = kwargs['stderr'] = subprocess.PIPE
---> 76     proc = _run_input_lines(cmd, input_lines, kwargs=kwargs)
     77 else:

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/execute.py:96, in _run_input_lines(cmd, input_lines, kwargs)
     95 def _run_input_lines(cmd, input_lines, *, kwargs):
---> 96     popen = subprocess.Popen(cmd, stdin=subprocess.PIPE, **kwargs)
     98     stdin_write = popen.stdin.write

File ~/.asdf/installs/python/3.11.6/lib/python3.11/subprocess.py:1026, in Popen.__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, user, group, extra_groups, encoding, errors, text, umask, pipesize, process_group)
   1023             self.stderr = io.TextIOWrapper(self.stderr,
   1024                     encoding=encoding, errors=errors)
-> 1026     self._execute_child(args, executable, preexec_fn, close_fds,
   1027                         pass_fds, cwd, env,
   1028                         startupinfo, creationflags, shell,
   1029                         p2cread, p2cwrite,
   1030                         c2pread, c2pwrite,
   1031                         errread, errwrite,
   1032                         restore_signals,
   1033                         gid, gids, uid, umask,
   1034                         start_new_session, process_group)
   1035 except:
   1036     # Cleanup if the child failed starting.

File ~/.asdf/installs/python/3.11.6/lib/python3.11/subprocess.py:1950, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session, process_group)
   1949         err_msg = os.strerror(errno_num)
-> 1950     raise child_exception_type(errno_num, err_msg, err_filename)
   1951 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)
File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/IPython/core/formatters.py:977, in MimeBundleFormatter.__call__(self, obj, include, exclude)
    974     method = get_real_method(obj, self.print_method)
    976     if method is not None:
--> 977         return method(include=include, exclude=exclude)
    978     return None
    979 else:

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/decaylanguage/decay/viewer.py:259, in DecayChainViewer._repr_mimebundle_(self, include, exclude, **kwargs)
    255 """
    256 IPython display helper.
    257 """
    258 try:
--> 259     return self._graph._repr_mimebundle_(
    260         include=include, exclude=exclude, **kwargs
    261     )
    262 except AttributeError:
    263     return {"image/svg+xml": self._graph._repr_svg_()}

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/jupyter_integration.py:98, in JupyterIntegration._repr_mimebundle_(self, include, exclude, **_)
     96 include = set(include) if include is not None else {self._jupyter_mimetype}
     97 include -= set(exclude or [])
---> 98 return {mimetype: getattr(self, method_name)()
     99         for mimetype, method_name in MIME_TYPES.items()
    100         if mimetype in include}

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/jupyter_integration.py:98, in <dictcomp>(.0)
     96 include = set(include) if include is not None else {self._jupyter_mimetype}
     97 include -= set(exclude or [])
---> 98 return {mimetype: getattr(self, method_name)()
     99         for mimetype, method_name in MIME_TYPES.items()
    100         if mimetype in include}

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/jupyter_integration.py:112, in JupyterIntegration._repr_image_svg_xml(self)
    110 def _repr_image_svg_xml(self) -> str:
    111     """Return the rendered graph as SVG string."""
--> 112     return self.pipe(format='svg', encoding=SVG_ENCODING)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/piping.py:104, in Pipe.pipe(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
     55 def pipe(self,
     56          format: typing.Optional[str] = None,
     57          renderer: typing.Optional[str] = None,
   (...)
     61          engine: typing.Optional[str] = None,
     62          encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
     63     """Return the source piped through the Graphviz layout command.
     64 
     65     Args:
   (...)
    102         '<?xml version='
    103     """
--> 104     return self._pipe_legacy(format,
    105                              renderer=renderer,
    106                              formatter=formatter,
    107                              neato_no_op=neato_no_op,
    108                              quiet=quiet,
    109                              engine=engine,
    110                              encoding=encoding)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/_tools.py:171, in deprecate_positional_args.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    162     wanted = ', '.join(f'{name}={value!r}'
    163                        for name, value in deprecated.items())
    164     warnings.warn(f'The signature of {func.__name__} will be reduced'
    165                   f' to {supported_number} positional args'
    166                   f' {list(supported)}: pass {wanted}'
    167                   ' as keyword arg(s)',
    168                   stacklevel=stacklevel,
    169                   category=category)
--> 171 return func(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/piping.py:121, in Pipe._pipe_legacy(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
    112 @_tools.deprecate_positional_args(supported_number=2)
    113 def _pipe_legacy(self,
    114                  format: typing.Optional[str] = None,
   (...)
    119                  engine: typing.Optional[str] = None,
    120                  encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
--> 121     return self._pipe_future(format,
    122                              renderer=renderer,
    123                              formatter=formatter,
    124                              neato_no_op=neato_no_op,
    125                              quiet=quiet,
    126                              engine=engine,
    127                              encoding=encoding)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/piping.py:149, in Pipe._pipe_future(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
    146 if encoding is not None:
    147     if codecs.lookup(encoding) is codecs.lookup(self.encoding):
    148         # common case: both stdin and stdout need the same encoding
--> 149         return self._pipe_lines_string(*args, encoding=encoding, **kwargs)
    150     try:
    151         raw = self._pipe_lines(*args, input_encoding=self.encoding, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/piping.py:212, in pipe_lines_string(engine, format, input_lines, encoding, renderer, formatter, neato_no_op, quiet)
    206 cmd = dot_command.command(engine, format,
    207                           renderer=renderer,
    208                           formatter=formatter,
    209                           neato_no_op=neato_no_op)
    210 kwargs = {'input_lines': input_lines, 'encoding': encoding}
--> 212 proc = execute.run_check(cmd, capture_output=True, quiet=quiet, **kwargs)
    213 return proc.stdout

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/execute.py:81, in run_check(cmd, input_lines, encoding, quiet, **kwargs)
     79 except OSError as e:
     80     if e.errno == errno.ENOENT:
---> 81         raise ExecutableNotFound(cmd) from e
     82     raise
     84 if not quiet and proc.stderr:

ExecutableNotFound: failed to execute PosixPath('dot'), make sure the Graphviz executables are on your systems' PATH
<decaylanguage.decay.viewer.DecayChainViewer at 0x7f1c45af55c0>

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))
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1712013561.849163     745 device_compiler.h:186] Compiled cluster using XLA!  This line is logged at most once for the lifetime of the process.
2024-04-01 23:19:21.850196: E external/local_xla/xla/stream_executor/stream_executor_internal.h:177] SetPriority unimplemented for this stream.
2024-04-01 23:19:21.900163: E external/local_xla/xla/stream_executor/stream_executor_internal.h:177] SetPriority unimplemented for this stream.
Number of events for each decay mode:
 
9890, 110

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)
File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/execute.py:76, in run_check(cmd, input_lines, encoding, quiet, **kwargs)
     75         kwargs['stdout'] = kwargs['stderr'] = subprocess.PIPE
---> 76     proc = _run_input_lines(cmd, input_lines, kwargs=kwargs)
     77 else:

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/execute.py:96, in _run_input_lines(cmd, input_lines, kwargs)
     95 def _run_input_lines(cmd, input_lines, *, kwargs):
---> 96     popen = subprocess.Popen(cmd, stdin=subprocess.PIPE, **kwargs)
     98     stdin_write = popen.stdin.write

File ~/.asdf/installs/python/3.11.6/lib/python3.11/subprocess.py:1026, in Popen.__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, user, group, extra_groups, encoding, errors, text, umask, pipesize, process_group)
   1023             self.stderr = io.TextIOWrapper(self.stderr,
   1024                     encoding=encoding, errors=errors)
-> 1026     self._execute_child(args, executable, preexec_fn, close_fds,
   1027                         pass_fds, cwd, env,
   1028                         startupinfo, creationflags, shell,
   1029                         p2cread, p2cwrite,
   1030                         c2pread, c2pwrite,
   1031                         errread, errwrite,
   1032                         restore_signals,
   1033                         gid, gids, uid, umask,
   1034                         start_new_session, process_group)
   1035 except:
   1036     # Cleanup if the child failed starting.

File ~/.asdf/installs/python/3.11.6/lib/python3.11/subprocess.py:1950, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session, process_group)
   1949         err_msg = os.strerror(errno_num)
-> 1950     raise child_exception_type(errno_num, err_msg, err_filename)
   1951 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)
File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/IPython/core/formatters.py:977, in MimeBundleFormatter.__call__(self, obj, include, exclude)
    974     method = get_real_method(obj, self.print_method)
    976     if method is not None:
--> 977         return method(include=include, exclude=exclude)
    978     return None
    979 else:

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/decaylanguage/decay/viewer.py:259, in DecayChainViewer._repr_mimebundle_(self, include, exclude, **kwargs)
    255 """
    256 IPython display helper.
    257 """
    258 try:
--> 259     return self._graph._repr_mimebundle_(
    260         include=include, exclude=exclude, **kwargs
    261     )
    262 except AttributeError:
    263     return {"image/svg+xml": self._graph._repr_svg_()}

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/jupyter_integration.py:98, in JupyterIntegration._repr_mimebundle_(self, include, exclude, **_)
     96 include = set(include) if include is not None else {self._jupyter_mimetype}
     97 include -= set(exclude or [])
---> 98 return {mimetype: getattr(self, method_name)()
     99         for mimetype, method_name in MIME_TYPES.items()
    100         if mimetype in include}

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/jupyter_integration.py:98, in <dictcomp>(.0)
     96 include = set(include) if include is not None else {self._jupyter_mimetype}
     97 include -= set(exclude or [])
---> 98 return {mimetype: getattr(self, method_name)()
     99         for mimetype, method_name in MIME_TYPES.items()
    100         if mimetype in include}

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/jupyter_integration.py:112, in JupyterIntegration._repr_image_svg_xml(self)
    110 def _repr_image_svg_xml(self) -> str:
    111     """Return the rendered graph as SVG string."""
--> 112     return self.pipe(format='svg', encoding=SVG_ENCODING)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/piping.py:104, in Pipe.pipe(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
     55 def pipe(self,
     56          format: typing.Optional[str] = None,
     57          renderer: typing.Optional[str] = None,
   (...)
     61          engine: typing.Optional[str] = None,
     62          encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
     63     """Return the source piped through the Graphviz layout command.
     64 
     65     Args:
   (...)
    102         '<?xml version='
    103     """
--> 104     return self._pipe_legacy(format,
    105                              renderer=renderer,
    106                              formatter=formatter,
    107                              neato_no_op=neato_no_op,
    108                              quiet=quiet,
    109                              engine=engine,
    110                              encoding=encoding)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/_tools.py:171, in deprecate_positional_args.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    162     wanted = ', '.join(f'{name}={value!r}'
    163                        for name, value in deprecated.items())
    164     warnings.warn(f'The signature of {func.__name__} will be reduced'
    165                   f' to {supported_number} positional args'
    166                   f' {list(supported)}: pass {wanted}'
    167                   ' as keyword arg(s)',
    168                   stacklevel=stacklevel,
    169                   category=category)
--> 171 return func(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/piping.py:121, in Pipe._pipe_legacy(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
    112 @_tools.deprecate_positional_args(supported_number=2)
    113 def _pipe_legacy(self,
    114                  format: typing.Optional[str] = None,
   (...)
    119                  engine: typing.Optional[str] = None,
    120                  encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
--> 121     return self._pipe_future(format,
    122                              renderer=renderer,
    123                              formatter=formatter,
    124                              neato_no_op=neato_no_op,
    125                              quiet=quiet,
    126                              engine=engine,
    127                              encoding=encoding)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/piping.py:149, in Pipe._pipe_future(self, format, renderer, formatter, neato_no_op, quiet, engine, encoding)
    146 if encoding is not None:
    147     if codecs.lookup(encoding) is codecs.lookup(self.encoding):
    148         # common case: both stdin and stdout need the same encoding
--> 149         return self._pipe_lines_string(*args, encoding=encoding, **kwargs)
    150     try:
    151         raw = self._pipe_lines(*args, input_encoding=self.encoding, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/piping.py:212, in pipe_lines_string(engine, format, input_lines, encoding, renderer, formatter, neato_no_op, quiet)
    206 cmd = dot_command.command(engine, format,
    207                           renderer=renderer,
    208                           formatter=formatter,
    209                           neato_no_op=neato_no_op)
    210 kwargs = {'input_lines': input_lines, 'encoding': encoding}
--> 212 proc = execute.run_check(cmd, capture_output=True, quiet=quiet, **kwargs)
    213 return proc.stdout

File ~/checkouts/readthedocs.org/user_builds/phasespace/envs/latest/lib/python3.11/site-packages/graphviz/backend/execute.py:81, in run_check(cmd, input_lines, encoding, quiet, **kwargs)
     79 except OSError as e:
     80     if e.errno == errno.ENOENT:
---> 81         raise ExecutableNotFound(cmd) from e
     82     raise
     84 if not quiet and proc.stderr:

ExecutableNotFound: failed to execute PosixPath('dot'), make sure the Graphviz executables are on your systems' PATH
<decaylanguage.decay.viewer.DecayChainViewer at 0x7f1c45d14cc0>
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.604e-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 0x7f1c4415fd10>

If you want all \(\pi^0\) particles to decay with the same mass function, you do not need to specify the zfit parameter for each decay in the dict. Instead, one can pass the particle_model_map parameter to the constructor:

GenMultiDecay.from_dict(dsplus_chain, particle_model_map={'pi0': 'gauss'})    # pi0 always decays with a gaussian mass distribution.
<phasespace.fromdecay.genmultidecay.GenMultiDecay at 0x7f1c4549c350>

When using DecayChains, the syntax for specifying the mass function becomes cleaner:

dplus_decay = DecayMode(1, "K- pi+ pi+ pi0", model="PHSP")  # The model parameter will be ignored by GenMultiDecay
pi0_decay = DecayMode(1, "gamma gamma", zfit="gauss")   # Make pi0 have a gaussian mass distribution
dplus_single = DecayChain("D+", {"D+": dplus_decay, "pi0": pi0_decay})
GenMultiDecay.from_dict(dplus_single.to_dict())
<phasespace.fromdecay.genmultidecay.GenMultiDecay at 0x7f1c454ade90>

Custom mass functions

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 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". If an invalid value for the zfit parameter is used, a KeyError is raised.

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 0x7f1c4549ccd0>