Constructing and calling a phase-space mapping

This example builds a phase-space mapping for the process g g > t t~ g and uses it to draw a batch of momenta from random numbers. It uses PhaseSpaceMapping with a flat list of external masses. This builds a mapping without needing a Topology first; the next example covers that case. Everything here is plain NumPy. MadSpace does not require NumPy specifically. It accepts whichever array library the input arrays belong to.

Building the mapping

We pick a collision energy and the external masses, beams first, then the outgoing particles in the order t, t~, g:

import numpy as np
import madspace as ms

E_CM = 13000.0
masses = [0.0, 0.0, 173.0, 173.0, 0.0]  # g g -> t t~ g

mapping = ms.PhaseSpaceMapping(masses, E_CM, mode="rambo")
print(f"random numbers per event: {mapping.random_dim()}")

mode selects how the mapping handles the two incoming (t-channel) legs. "rambo" uses the FastRamboMapping building block. It does not follow any particular Feynman diagram. The output is:

random numbers per event: 7

Sampling momenta

map_forward turns a batch of uniform random numbers into momenta. It returns a namedtuple. For this mapping its fields are momenta, x1, x2 (the parton momentum fractions) and det, the Jacobian of the transformation:

rng = np.random.default_rng(1234)
r = rng.random((10000, mapping.random_dim()))
momenta, x1, x2, det = mapping.map_forward([r])
print(f"momenta: {momenta.shape}, x1: {x1.shape}, det: {det.shape}")
np.set_printoptions(precision=1, suppress=True)
print("first event, (E, px, py, pz) per particle:")
print(momenta[0])

momenta holds one four-vector per external particle for every event in the batch. The order matches masses: beams first, then the outgoing particles:

momenta: (10000, 5, 4), x1: (10000,), det: (10000,)
first event, (E, px, py, pz) per particle:
[[ 6281.4     0.      0.   6281.4]
 [ 6147.4     0.      0.  -6147.4]
 [ 5163.9   199.8 -2715.4  4384.4]
 [ 4392.7  -199.6  -153.8 -4382. ]
 [ 2872.2    -0.3  2869.1   131.6]]

Checking the output

The sampled momenta reproduce the requested external masses. They also conserve momentum, up to floating-point precision:

m = np.sqrt(np.abs(momenta[..., 0] ** 2 - np.sum(momenta[..., 1:] ** 2, axis=-1)))
print("masses of the first event:", m[0])

p_in = momenta[:, :2].sum(axis=1)
p_out = momenta[:, 2:].sum(axis=1)
print("largest momentum-conservation violation:", np.abs(p_out - p_in).max())
masses of the first event: [  0.   0. 173. 173.   0.]
largest momentum-conservation violation: 9.458744898438454e-11

Inverting the mapping

Every Mapping is invertible. map_inverse recovers the random numbers from the momenta. The two Jacobians are reciprocal:

r_inv, det_inv = mapping.map_inverse([momenta, x1, x2])
print("largest deviation of the recovered random numbers:", np.abs(r_inv - r).max())
print("largest deviation of det * det_inv from 1:", np.abs(det * det_inv - 1).max())
largest deviation of the recovered random numbers: 3.0175417720101905e-11
largest deviation of det * det_inv from 1: 1.37008404621497e-10

Switching to Chili

Passing mode="chili" instead builds a ChiliMapping. It samples directly in observable variables such as transverse momenta and rapidities, rather than through recursive two-body decays. This suits processes with many jets. The cost is a mapping that misses part of phase space: points outside its support come back with det == 0 and must be discarded.

chili = ms.PhaseSpaceMapping(masses, E_CM, mode="chili")
r_chili = rng.random((10000, chili.random_dim()))
momenta_chili, _, _, det_chili = chili.map_forward([r_chili])
physical = det_chili != 0.0
print(f"fraction of physical Chili points: {physical.mean():.3f}")

pt_rambo = np.hypot(momenta[:, 2, 1], momenta[:, 2, 2])
pt_chili = np.hypot(momenta_chili[physical, 2, 1], momenta_chili[physical, 2, 2])
print(f"mean top pT: rambo = {pt_rambo.mean():.0f} GeV, chili = {pt_chili.mean():.0f} GeV")

Chili concentrates its samples at lower transverse momentum, closer to where a physical cross section peaks:

fraction of physical Chili points: 0.881
mean top pT: rambo = 1272 GeV, chili = 634 GeV