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([[ 656.17808335, 1989.52651609, -1564.84604824, 2618.58901417],
[ 500.7815615 , -1196.71370564, 2270.379423 , 2618.58901417],
[-2263.49679302, -1028.95680669, -809.54230769, 2618.58901417],
...,
[ -553.66787284, 2361.49318705, -976.89835913, 2618.58901417],
[ 302.66831823, -1620.85980524, 2029.46642516, 2618.58901417],
[ 2610.295607 , 84.81494083, 129.1969808 , 2618.58901417]],
shape=(100000, 4))>,
'p_1': <tf.Tensor: shape=(100000, 4), dtype=float64, numpy=
array([[ -656.17808335, -1989.52651609, 1564.84604824, 2661.06098583],
[ -500.7815615 , 1196.71370564, -2270.379423 , 2661.06098583],
[ 2263.49679302, 1028.95680669, 809.54230769, 2661.06098583],
...,
[ 553.66787284, -2361.49318705, 976.89835913, 2661.06098583],
[ -302.66831823, 1620.85980524, -2029.46642516, 2661.06098583],
[-2610.295607 , -84.81494083, -129.1969808 , 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([[ 471.39775005, -395.60211006, -2488.92006885, 2715.77793272],
[ 1889.5113931 , -1159.49157494, -1287.93088337, 2715.77793272],
[ -277.99918845, -1188.2861134 , -2254.8021069 , 2715.77793272],
...,
[-1932.05773208, 1682.79599246, -93.75790652, 2715.77793272],
[ 915.3605135 , 1979.33382607, -1348.25543302, 2715.77793272],
[-2323.84917072, -928.15858109, 558.2890447 , 2715.77793272]],
shape=(100000, 4))>,
'gamma': <tf.Tensor: shape=(100000, 4), dtype=float64, numpy=
array([[ -471.39775005, 395.60211006, 2488.92006885, 2563.87206728],
[-1889.5113931 , 1159.49157494, 1287.93088337, 2563.87206728],
[ 277.99918845, 1188.2861134 , 2254.8021069 , 2563.87206728],
...,
[ 1932.05773208, -1682.79599246, 93.75790652, 2563.87206728],
[ -915.3605135 , -1979.33382607, 1348.25543302, 2563.87206728],
[ 2323.84917072, 928.15858109, -558.2890447 , 2563.87206728]],
shape=(100000, 4))>,
'K+': <tf.Tensor: shape=(100000, 4), dtype=float64, numpy=
array([[ 379.26030028, -551.75200587, -1673.23063319, 1868.60546492],
[ 1487.20343321, -1107.44290049, -1238.79593018, 2283.97376661],
[ -8.68595548, -899.00172646, -1929.60013893, 2185.25816014],
...,
[-1835.09170606, 1670.38692188, -20.23752503, 2530.19378234],
[ 851.38211318, 1750.58097468, -1382.55907665, 2438.14926375],
[-1909.35840288, -477.21859692, 403.555192 , 2068.80663021]],
shape=(100000, 4))>,
'pi-': <tf.Tensor: shape=(100000, 4), dtype=float64, numpy=
array([[ 92.13744977, 156.14989581, -815.68943566, 847.17246781],
[ 402.30795989, -52.04867446, -49.13495319, 431.80416611],
[-269.31323297, -289.28438694, -325.20196798, 530.51977258],
...,
[ -96.96602602, 12.40907058, -73.52038149, 185.58415039],
[ 63.97840032, 228.75285138, 34.30364363, 277.62866897],
[-414.49076785, -450.93998417, 154.7338527 , 646.97130251]],
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_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