Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DecayLanguage compatibility #63

Merged
merged 73 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
9254e8f
Add files for compatibility between phasespace and decaylanguage
simonthor Sep 21, 2021
79d1ccf
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 21, 2021
6d5e448
Syntactic update to code for extracting mother name
simonthor Sep 26, 2021
ca5c115
Better error handling for module imports
simonthor Sep 27, 2021
54a789c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 27, 2021
e36319c
Merge branch 'decay2phasespace' of https://github.com/simonthor/phase…
simonthor Sep 27, 2021
0419501
Add pytest assert introspection to check_norm helper function
simonthor Sep 27, 2021
968cf0b
Better assertion errors for check_norm
simonthor Sep 27, 2021
03a8c80
Remove forgotten dummy assert
simonthor Sep 27, 2021
60ab982
Remove hardcoded K*0 mass and width with call to particle
simonthor Sep 27, 2021
8627d13
Move docstring for FullDecay to __init__
simonthor Sep 27, 2021
436a6d4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 27, 2021
06f0713
Make comment about map_fn more descriptive
simonthor Sep 27, 2021
f5f0ca9
Remove average from `from_dict` docstring
simonthor Sep 27, 2021
6d4268d
Remove part of comment
simonthor Sep 27, 2021
9e58f4a
Update docstrings to Google style instead of NumPy style.
simonthor Sep 27, 2021
33ef57b
Rename mass functions to use lowercase.
simonthor Oct 3, 2021
96aea99
Move most decays into a decfile
simonthor Oct 3, 2021
e610d11
Add mass functions and make tests pass
simonthor Oct 3, 2021
77830d1
Remove newline
simonthor Oct 3, 2021
f5a23ec
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 3, 2021
8e31148
Add test for mass_converter
simonthor Oct 3, 2021
090e65b
Fix test with mass_converter
simonthor Oct 3, 2021
3879e04
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 3, 2021
51e5ecb
Update default value
simonthor Oct 7, 2021
bf6f19a
Begin working on documentation notebook
simonthor Oct 7, 2021
4d9b009
Add docs about tolerance and improve former parts
simonthor Oct 7, 2021
be38a78
chore: add pre-commit hook to clean jupyter notebook
jonas-eschle Oct 8, 2021
79142cc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 8, 2021
d76a967
Write about all features in fulldecay
simonthor Oct 10, 2021
02549c5
Rename fuldecay to fromdecay.
simonthor Oct 10, 2021
dec6a23
Rename fulldecay to fromdecay
simonthor Oct 10, 2021
d14d69e
Revert "Rename fulldecay to fromdecay"
simonthor Oct 12, 2021
bba9a5c
Revert "Rename fuldecay to fromdecay."
simonthor Oct 12, 2021
32cc781
Rename fulldecay to fromdecay and FullDecay to MultiDecayChain
simonthor Oct 12, 2021
f5da87c
Update setup.cfg to work with fromdecay
simonthor Oct 13, 2021
75c4a87
Update particle package version requirement
simonthor Nov 3, 2021
128a577
Add decaylanguage to package requirements
simonthor Nov 3, 2021
58f1215
Rename class to GenMultiDecay
simonthor Nov 6, 2021
b6b0e33
Add examples to docstring of GenMultiDecay.from_dict
simonthor Nov 6, 2021
30d8ab0
Format classes and packages in monospaced font
simonthor Nov 8, 2021
e1aead9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 8, 2021
434cdb8
Clarify docstring
simonthor Nov 12, 2021
0e41fc3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 12, 2021
d8062e7
Reference examples in dec_dict docstring
simonthor Nov 12, 2021
36ae8b1
decaylanguage to DecayLanguage
simonthor Nov 12, 2021
bb80371
decaylanguage to DecayLanguage
simonthor Nov 12, 2021
632e443
Same as last commit
simonthor Nov 12, 2021
0b6ec07
Make constants public
simonthor Nov 12, 2021
bbdf890
Minor clarifications
simonthor Nov 12, 2021
2a299dd
Make type hints complaiant with python < 3.9.
simonthor Nov 12, 2021
5122942
Improve code quality
simonthor Nov 12, 2021
66d3ff3
Improve code quality (2)
simonthor Nov 12, 2021
4a47428
Shorten docstring summary to improve code quality
simonthor Nov 12, 2021
decf916
Change MASS_WIDTH_TOLERANCE and DEFAULT_MASS_FUNC to class variables.
simonthor Nov 13, 2021
03f4def
Change docstring to reference class variable
simonthor Nov 13, 2021
3374b3f
Add nbval to CI
jonas-eschle Nov 14, 2021
2cf9c8c
Update ci.yml
jonas-eschle Nov 14, 2021
690e16d
Add fromdecay to test requirements
jonas-eschle Nov 14, 2021
dfe5604
Update GenMultiDecay_Tutorial.ipynb
jonas-eschle Nov 14, 2021
5853811
Update setup.cfg
jonas-eschle Nov 14, 2021
e5c9f8a
CI: correct execution place for notebook
jonas-eschle Nov 14, 2021
eae7abb
ci: explicit test directory
jonas-eschle Nov 14, 2021
d446b7c
chore: add graphviz to dependencies for notebook
jonas-eschle Nov 14, 2021
649d86d
chore: remove duplicated requirements
jonas-eschle Nov 15, 2021
b46c064
ci: add dev installation
jonas-eschle Nov 15, 2021
c88957b
ci: ignore ipynb checkpoints
jonas-eschle Nov 15, 2021
f8d0341
ci: ignore notebook
jonas-eschle Nov 15, 2021
c529263
Update setup.cfg
jonas-eschle Nov 22, 2021
c1ef3b3
fix: error with new tf version, requiring keras < 2.7
jonas-eschle Nov 24, 2021
2bd2d2f
ci: debug notebook error 1
jonas-eschle Nov 24, 2021
80f8121
ci: debug notebook error 2, adding graphviz system package
jonas-eschle Nov 24, 2021
edebaf4
Merge branch 'master' into decay2phasespace
jonas-eschle Nov 24, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions phasespace/fulldecay/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import sys
simonthor marked this conversation as resolved.
Show resolved Hide resolved

from .fulldecay import FullDecay

try:
import zfit
import zfit_physics as zphys
from particle import Particle
except ModuleNotFoundError:
print(
"the fulldecay functionality in phasespace requires particle and zfit-physics. "
"Either install phasespace[fulldecay] or particle and zfit-physics.",
file=sys.stderr,
)
raise
simonthor marked this conversation as resolved.
Show resolved Hide resolved
259 changes: 259 additions & 0 deletions phasespace/fulldecay/fulldecay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import itertools
from typing import Callable, Union

import tensorflow as tf
import tensorflow.experimental.numpy as tnp
from particle import Particle

from phasespace import GenParticle

from .mass_functions import _DEFAULT_CONVERTER

_MASS_WIDTH_TOLERANCE = 0.01
_DEFAULT_MASS_FUNC = "rel-BW"


class FullDecay:
"""A container that works like GenParticle that can handle multiple decays."""

def __init__(self, gen_particles: list[tuple[float, GenParticle]]):
"""Create an instance of FullDecay.
simonthor marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
gen_particles : list[tuple[float, GenParticle]]
All the GenParticles and their corresponding probabilities.
The list must be of the format [[probability, GenParticle instance], [probability, ...
Notes
-----
Input format might change
"""
self.gen_particles = gen_particles

@classmethod
def from_dict(
cls,
dec_dict: dict,
mass_converter: dict[str, Callable] = None,
tolerance: float = _MASS_WIDTH_TOLERANCE,
):
"""Create a FullDecay instance from a dict in the decaylanguage format.
simonthor marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
dec_dict : dict
The input dict from which the FullDecay object will be created from.
mass_converter : dict[str, Callable]
simonthor marked this conversation as resolved.
Show resolved Hide resolved
A dict with mass function names and their corresponding mass functions.
These functions should take the average particle mass and the mass width as inputs
simonthor marked this conversation as resolved.
Show resolved Hide resolved
and return a mass function that phasespace can understand.
This dict will be combined with the predefined mass functions in this package.
tolerance : float
Minimum mass width of the particle to use a mass function instead of assuming the mass to be constant.

Returns
-------
FullDecay
The created FullDecay object.
"""
if mass_converter is None:
total_mass_converter = _DEFAULT_CONVERTER
else:
# Combine the mass functions specified by the package to the mass functions specified from the input.
total_mass_converter = {**_DEFAULT_CONVERTER, **mass_converter}

gen_particles = _recursively_traverse(
dec_dict, total_mass_converter, tolerance=tolerance
)
return cls(gen_particles)

def generate(
self, n_events: int, normalize_weights: bool = False, **kwargs
) -> Union[
tuple[list[tf.Tensor], list[tf.Tensor]],
tuple[list[tf.Tensor], list[tf.Tensor], list[tf.Tensor]],
]:
"""Generate four-momentum vectors from the decay(s).

Parameters
----------
n_events : int
Total number of events combined, for all the decays.
normalize_weights : bool
Normalize weights according to all events generated. This also changes the return values.
See the phasespace documentation for more details.
kwargs
Additional parameters passed to all calls of GenParticle.generate

Returns
-------
The arguments returned by GenParticle.generate are returned. See the phasespace documentation for details.
However, instead of being 2 or 3 tensors, it is 2 or 3 lists of tensors, each entry in the lists corresponding
to the return arguments from the corresponding GenParticle instances in self.gen_particles.
Note that when normalize_weights is True, the weights are normalized to the maximum of all returned events.
"""
# Input to tf.random.categorical must be 2D
rand_i = tf.random.categorical(
tnp.log([[dm[0] for dm in self.gen_particles]]), n_events
)
# Input to tf.unique_with_counts must be 1D
dec_indices, _, counts = tf.unique_with_counts(rand_i[0])
counts = tf.cast(counts, tf.int64)
weights, max_weights, events = [], [], []
for i, n in zip(dec_indices, counts):
weight, max_weight, four_vectors = self.gen_particles[i][1].generate(
n, normalize_weights=False, **kwargs
)
weights.append(weight)
max_weights.append(max_weight)
events.append(four_vectors)

if normalize_weights:
total_max = tnp.max([tnp.max(mw) for mw in max_weights])
normed_weights = [w / total_max for w in weights]
return normed_weights, events

return weights, max_weights, events


def _unique_name(name: str, preexisting_particles: set[str]) -> str:
"""Create a string that does not exist in preexisting_particles based on name.

Parameters
----------
name : str
Name that should be
preexisting_particles : set[str]
Preexisting names

Returns
-------
name : str
Will be `name` if `name` is not in preexisting_particles or of the format "name [i]" where i will begin at 0
and increase until the name is not preexisting_particles.
"""
if name not in preexisting_particles:
preexisting_particles.add(name)
return name

name += " [0]"
i = 1
while name in preexisting_particles:
name = name[: name.rfind("[")] + f"[{str(i)}]"
i += 1
preexisting_particles.add(name)
return name


def _get_particle_mass(
name: str,
mass_converter: dict[str, Callable],
mass_func: str,
tolerance: float = _MASS_WIDTH_TOLERANCE,
) -> Union[Callable, float]:
"""
Get mass or mass function of particle using the particle package.
Parameters
----------
name : str
Name of the particle. Name must be recognizable by the particle package.
tolerance : float
See _recursively_traverse

Returns
-------
Callable, float
Returns a function if the mass has a width smaller than tolerance.
Otherwise, return a constant mass.
TODO try to cache results for this function in the future for speedup.
"""
particle = Particle.from_evtgen_name(name)

if particle.width <= tolerance:
return tf.cast(particle.mass, tf.float64)
# If name does not exist in the predefined mass distributions, use Breit-Wigner
return mass_converter[mass_func](mass=particle.mass, width=particle.width)


def _recursively_traverse(
decaychain: dict,
mass_converter: dict[str, Callable],
preexisting_particles: set[str] = None,
tolerance: float = _MASS_WIDTH_TOLERANCE,
) -> list[tuple[float, GenParticle]]:
"""Create all possible GenParticles by recursively traversing a dict from decaylanguage.

Parameters
----------
decaychain: dict
Decay chain with the format from decaylanguage
preexisting_particles : set
names of all particles that have already been created.
tolerance : float
Minimum mass width for a particle to set a non-constant mass to a particle.

Returns
-------
list[tuple[float, GenParticle]]
The generated particle
"""
original_mother_name = list(decaychain.keys())[
0
] # Get the only key inside the dict

if preexisting_particles is None:
preexisting_particles = set()
is_top_particle = True
else:
is_top_particle = False

# This is in the form of dicts
decay_modes = decaychain[original_mother_name]
mother_name = _unique_name(original_mother_name, preexisting_particles)
# This will contain GenParticle instances and their probabilities
all_decays = []
for dm in decay_modes:
dm_probability = dm["bf"]
daughter_particles = dm["fs"]
daughter_gens = []

for daughter_name in daughter_particles:
if isinstance(daughter_name, str):
# Always use constant mass for stable particles
daughter = GenParticle(
_unique_name(daughter_name, preexisting_particles),
Particle.from_evtgen_name(daughter_name).mass,
)
daughter = [(1.0, daughter)]
elif isinstance(daughter_name, dict):
daughter = _recursively_traverse(
daughter_name,
mass_converter,
preexisting_particles,
tolerance=tolerance,
)
else:
raise TypeError(
f'Expected elements in decaychain["fs"] to only be str or dict '
f"but found an instance of type {type(daughter_name)}"
)
daughter_gens.append(daughter)

for daughter_combination in itertools.product(*daughter_gens):
p = tnp.prod([decay[0] for decay in daughter_combination]) * dm_probability
if is_top_particle:
mother_mass = Particle.from_evtgen_name(original_mother_name).mass
else:
mother_mass = _get_particle_mass(
original_mother_name,
mass_converter=mass_converter,
mass_func=dm.get("zfit", _DEFAULT_MASS_FUNC),
tolerance=tolerance,
)

one_decay = GenParticle(mother_name, mother_mass).set_children(
*(decay[1] for decay in daughter_combination)
)
all_decays.append((p, one_decay))

return all_decays
64 changes: 64 additions & 0 deletions phasespace/fulldecay/mass_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import tensorflow as tf
import zfit
import zfit_physics as zphys


# TODO refactor these mass functions using e.g. a decorator.
# Right now there is a lot of code repetition.
def gauss(mass, width):
particle_mass = tf.cast(mass, tf.float64)
particle_width = tf.cast(width, tf.float64)

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


def breitwigner(mass, width):
particle_mass = tf.cast(mass, tf.float64)
particle_width = tf.cast(width, tf.float64)

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)
pdf = zfit.pdf.Cauchy(m=particle_mass, gamma=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


def relativistic_breitwigner(mass, width):
particle_mass = tf.cast(mass, tf.float64)
particle_width = tf.cast(width, tf.float64)

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)
pdf = zphys.pdf.RelativisticBreitWigner(
m=particle_mass, gamma=particle_width, obs=""
)
iterator = tf.stack([min_mass, max_mass], axis=-1)
# TODO this works with map_fn but not with vectorized_map for some reason.
simonthor marked this conversation as resolved.
Show resolved Hide resolved
# Does not work for e.g., zfit.pdf.CrystalBall either
return tf.map_fn(
lambda lim: pdf.sample(1, limits=(lim[0], lim[1])).unstack_x(), iterator
)

return mass_func


_DEFAULT_CONVERTER = {
"gauss": gauss,
"BW": breitwigner,
simonthor marked this conversation as resolved.
Show resolved Hide resolved
"rel-BW": relativistic_breitwigner,
}
Loading