Lecture 12 – Phase space simulation#
How to perform a phase space simulation with Python?
Prepare the notebook by importing numpy, matplotlib.pyplot, and pylorentz and download the data file (CSV format) from Google Drive.
Two-body decay#
Let’s start with a simple two body decay at rest: \(B^0\rightarrow K^+\pi^-\).
B0_MASS = 5279.65
PION_MASS = 139.57018
KAON_MASS = 493.677
n_events = 100_000
decay = phasespace.nbody_decay(B0_MASS, [PION_MASS, KAON_MASS])
weights, four_momenta = decay.generate(n_events=n_events)
The simulation produces a dictionary (four_momenta) of tf.Tensor objects. Each object can be addressed with particles['p_i'], where i is the number of the \(i\)-th generated particle.
four_momenta
{'p_0': <tf.Tensor: shape=(100000, 4), dtype=float64, numpy=
array([[-1953.35066075, -1269.81015386, 1187.23711166, 2618.58901417],
[ 409.47748872, 2582.47726353, -25.84491361, 2618.58901417],
[ 2110.34130906, 423.82134907, -1484.70994249, 2618.58901417],
...,
[ -520.26561727, -1293.73019839, -2212.03852843, 2618.58901417],
[ 2008.55204458, -1443.46615759, -848.32347964, 2618.58901417],
[ 183.70960461, -1307.10739107, 2257.26596558, 2618.58901417]],
shape=(100000, 4))>,
'p_1': <tf.Tensor: shape=(100000, 4), dtype=float64, numpy=
array([[ 1953.35066075, 1269.81015386, -1187.23711166, 2661.06098583],
[ -409.47748872, -2582.47726353, 25.84491361, 2661.06098583],
[-2110.34130906, -423.82134907, 1484.70994249, 2661.06098583],
...,
[ 520.26561727, 1293.73019839, 2212.03852843, 2661.06098583],
[-2008.55204458, 1443.46615759, 848.32347964, 2661.06098583],
[ -183.70960461, 1307.10739107, -2257.26596558, 2661.06098583]],
shape=(100000, 4))>}
Each tf.Tensor can be converted to a NumPy array, which can then be converted to a pylorentz.
def to_lorentz(p: tf.Tensor) -> Momentum4:
p = p.numpy().T
return Momentum4(p[3], *p[:3])
pion = to_lorentz(four_momenta["p_0"])
kaon = to_lorentz(four_momenta["p_1"])
These objects can be used to do kinematic computations. Let’s first verify that the invariant mass of the kaon+pion system corresponds to the mass of the mother \(B^0\):
B0 = pion + kaon
np.testing.assert_almost_equal(B0.m.mean(), B0_MASS)
Let’s also plot the momentum components of the two daugther particles.
But it’s monochromatic!! of course it is… it’s a decay at rest. The momentum components are uniformly distributed in the available phase space.
Three-body decay#
Let’s consider now a three body decay like \(B^0\rightarrow K^+\pi^-\pi^0\) and repeat the plot of the relevant kinematic variables. We can also make Dalitz plots this time.
n_events = 50_000
PION0_MASS = 134.9766
decay = phasespace.nbody_decay(B0_MASS, [PION_MASS, PION0_MASS, KAON_MASS])
weights, four_momenta = decay.generate(n_events=n_events)
pim = to_lorentz(four_momenta["p_0"])
pi0 = to_lorentz(four_momenta["p_1"])
kaon = to_lorentz(four_momenta["p_2"])
s1 = (kaon + pim).m2
s2 = (kaon + pi0).m2
s3 = (pim + pi0).m2
Decay chain#
The phasespace package allows to treat also multiple decays. Let’s consider the \(B^0\rightarrow K^{\ast 0}\gamma\) decay, followed by \(K^{\ast 0}\rightarrow \pi^-K^+\). It can be simulated using the following procedure:
from phasespace import GenParticle
B0_MASS = 5279.65
K0STAR_MASS = 895.55
PION_MASS = 139.57018
KAON_MASS = 493.677
GAMMA_MASS = 0.0
Kp = GenParticle("K+", KAON_MASS)
pim = GenParticle("pi-", PION_MASS)
Kstar = GenParticle("KStar", K0STAR_MASS).set_children(Kp, pim)
gamma = GenParticle("gamma", GAMMA_MASS)
B0 = GenParticle("B0", B0_MASS).set_children(Kstar, gamma)
weights, four_momenta = B0.generate(n_events=100_000)
four_momenta
{'KStar': <tf.Tensor: shape=(100000, 4), dtype=float64, numpy=
array([[-1379.25447889, -2028.64998442, 745.43698631, 2715.77793272],
[ -364.56562634, 2481.05553265, 533.75586683, 2715.77793272],
[-1545.09722708, -494.67480541, -1985.29881205, 2715.77793272],
...,
[ 1529.18882305, 1896.96128765, -797.84672336, 2715.77793272],
[ -416.44071995, 1153.19503863, 2251.70120287, 2715.77793272],
[ 1468.02251093, 1748.74919829, 1166.28732577, 2715.77793272]],
shape=(100000, 4))>,
'gamma': <tf.Tensor: shape=(100000, 4), dtype=float64, numpy=
array([[ 1379.25447889, 2028.64998442, -745.43698631, 2563.87206728],
[ 364.56562634, -2481.05553265, -533.75586683, 2563.87206728],
[ 1545.09722708, 494.67480541, 1985.29881205, 2563.87206728],
...,
[-1529.18882305, -1896.96128765, 797.84672336, 2563.87206728],
[ 416.44071995, -1153.19503863, -2251.70120287, 2563.87206728],
[-1468.02251093, -1748.74919829, -1166.28732577, 2563.87206728]],
shape=(100000, 4))>,
'K+': <tf.Tensor: shape=(100000, 4), dtype=float64, numpy=
array([[-1114.83947352, -1133.68059754, 538.09055029, 1749.67344656],
[ 45.40721127, 1181.46889269, 385.08728859, 1337.88630287],
[ -556.54499515, -94.29062859, -591.04370683, 954.82076746],
...,
[ 1210.1893969 , 1478.28818303, -335.73298814, 2001.58136185],
[ -253.75254829, 626.9517895 , 805.27403642, 1161.7410022 ],
[ 1480.22619515, 1685.2191228 , 1065.01549274, 2531.60187644]],
shape=(100000, 4))>,
'pi-': <tf.Tensor: shape=(100000, 4), dtype=float64, numpy=
array([[ -264.41500537, -894.96938687, 207.34643602, 966.10448616],
[ -409.97283761, 1299.58663995, 148.66857824, 1377.89162986],
[ -988.55223194, -400.38417682, -1394.25510522, 1760.95716526],
...,
[ 318.99942614, 418.67310462, -462.11373523, 714.19657087],
[ -162.68817165, 526.24324913, 1446.42716644, 1554.03693052],
[ -12.20368422, 63.53007548, 101.27183303, 184.17605629]],
shape=(100000, 4))>}
gamma = to_lorentz(four_momenta["gamma"])
pion = to_lorentz(four_momenta["pi-"])
kaon = to_lorentz(four_momenta["K+"])
Kstar = to_lorentz(four_momenta["KStar"])
Let’s build the Dalitz plots matching particle pairs. The particles measured in the final state are \(K^-,\; \pi^-\) and \(\gamma\).
s1 = (pion + kaon).m2
s2 = (gamma + kaon).m2
s3 = (gamma + pion).m2
Width distribution#
These distributions aren’t so interesting, because the masses of each particle are one fixed value. So let’s simulate a more realistic \(K^\ast\) particle; not monochromatic, but with a width of 47 MeV.[1] The mass is extracted from a Gaussian distribution centered at the B0_MASS value and with \(\sigma = 47/2.36 \sim 20\) MeV. See more info on how to do this with the phasespace package here.
import tensorflow as tf
import tensorflow_probability as tfp
K0STAR_WIDTH = 47 / 2.36
def kstar_mass(min_mass, max_mass, n_events):
min_mass = tf.cast(min_mass, tf.float64)
max_mass = tf.cast(max_mass, tf.float64)
kstar_mass_cast = tf.cast(K0STAR_MASS, dtype=tf.float64)
tf.cast(K0STAR_WIDTH, tf.float64)
tf.broadcast_to(kstar_mass_cast, shape=(n_events,))
return tfp.distributions.TruncatedNormal(
loc=K0STAR_MASS,
scale=K0STAR_WIDTH,
low=min_mass,
high=max_mass,
).sample()
K = GenParticle("K+", KAON_MASS)
pion = GenParticle("pi-", PION_MASS)
Kstar = GenParticle("KStar", kstar_mass).set_children(K, pion)
gamma = GenParticle("gamma", GAMMA_MASS)
B0 = GenParticle("B0", B0_MASS).set_children(Kstar, gamma)
weights, four_momenta = B0.generate(n_events=100_000)
gamma = to_lorentz(four_momenta["gamma"])
pion = to_lorentz(four_momenta["pi-"])
kaon = to_lorentz(four_momenta["K+"])
Kstar = to_lorentz(four_momenta["KStar"])
Now you have all the 4-vectors to plot the invariant mass distributions for the different steps of the decay chains.
s1 = (pion + kaon).m2
s2 = (gamma + kaon).m2
s3 = (gamma + pion).m2