Phase Space¶
Overview¶
The rambo
module contains the hazma.rambo.PhaseSpace
class for
working with N-body phase-space. In addition, it contains older functions
that use Cython and multiproccessing which tend to be slower. hazma.rambo.PhaseSpace
contains methods for generating phase-space points, integrating over N-body
phase-space, computing cross-sections and computing decay widths.
The hazma.rambo.PhaseSpace
class is instantiated by supplying the
center-of-mass energy, the masses of the final-state particles and optionally a
function to compute the squared matrix element. The function to compute the
squared matrix element must be a vectorized uniary function that return the
squared matrix elements given four-momenta. The internal momenta generated by
hazma.rambo.PhaseSpace
have the a shape of (4, nfsp, n)
where nfsp in the number of final-state particles and n is the
number of points generated simultaneously. The leading dimension holds the
energy, x, y and z-components of the four-momenta. If no function to compute
the squared matrix element is supplied, it will be taken to be 1.
In the following snippet, we create a hazma.rambo.PhaseSpace
object
for a process with 3 final-state particles, and a squared matrix element equal
to \(p_{1}\cdot p_{2}\). Note the Utilities (hazma.utils) module contains
a function hazma.utils.ldot()
to compute the Lorentzian scalar product
between two four-vectors.
from hazma import rambo
from hazma import utils
cme = 10.0
masses = [1.0, 2.0, 3.0]
def msqrd(momenta):
p1 = momenta[:, 0] # Pick out the 4-momenta of particle 1
p2 = momenta[:, 1] # Pick out the 4-momenta of particle 2
return utils.ldot(p1, p2) # Compute p1.p2
phase_space = PhaseSpace(cme, masses, msqrd)
Integrating over phase-space¶
The hazma.rambo.PhaseSpace.integrate()
method computes the following
integral:
This function takes in an integer specifying the number of points to use for the Monte-Carlo integration and computes the integral using the RAMBO algorithm. As a simple example, let’s compute the N-body integral for massless particles. The analytical result is:
We can verify this using the hazma.rambo.PhaseSpace.integrate()
method:
import math
import numpy as np
from hazma import rambo
def analytic(n):
fact = math.factorial(n - 2) * math.factorial(n - 1)
return (0.5 * math.pi) ** (n - 1) * (2 * np.pi) ** (4 - 3 * n) / fact
analytic_integrals = [analytic(n) for n in range(2, 10)]
integrals = []
for n in range(2, 10):
phase_space = rambo.PhaseSpace(1.0, [0.0] * n)
integral, error = phase_space.integrate(n=10)
integrals.append(integral)
np.max([abs(i1 - i2) / i2 for i1, i2 in zip(integrals, analytic_integrals)])
# Possible output: 2.2608966769988504e-15
Generating phase-space points¶
Sometimes it is useful to have access the the momenta and weights of N-body
phase-space. The are two methods for generating momenta and weights:
hazma.rambo.PhaseSpace.generate()
and
hazma.rambo.PhaseSpace.generator()
. The
hazma.rambo.PhaseSpace.generate()
method will return a specified
number of momenta and weights. The hazma.rambo.PhaseSpace.generator()
method returns a python generator, allowing for iterating over batches of
momenta and weights.
As described above, the momenta will have a shape of (4, nfsp, n) where nfsp is the number of final-state particles and n is the number of requested points. The weights will have a shape of (n,). To see this explicity, consider the following. Here we generate 10 phase-space points:
from hazma import rambo
phase_space = rambo.PhaseSpace(cme=10.0, masses=[1.0, 2.0, 3.0])
momenta, weights = phase_space.generate(n=10)
print(momenta.shape)
print(weights.shape)
# (4, 3, 10)
# (10,)
In some cases, one may not want to generate all points at once (since it is
costly memory-wise or maybe one wants to monitor convergence.) We supply the
hazma.rambo.PhaseSpace.generator()
method for generating batches of
phase-space points. For example, suppose we want to integrate over phase-space
ourselves using 1M points and batches of 50,000 points at a time. To do this,
we can use:
import numpy as np
from hazma import rambo
phase_space = rambo.PhaseSpace(cme=10.0, masses=[1.0, 2.0, 3.0])
n = int(1e6)
batch_size = 50_000
integrals = []
for momenta, weights in phase_space.generator(n, batch_size, seed=1234):
integrals.append(np.nanmean(weights))
np.average(integrals)
# Output: 0.0036118278252665406
Note
The above code is equivalent to using
hazma.rambo.PhaseSpace.generate()
with n=int(1e6)
and
batch_size=50_000
. All methods accept a batch_size
argument that can
be used to split up the computation if memory is limited.
Computing decay widths and cross sections¶
The most common use of the hazma.rambo.PhaseSpace()
is computing cross
sections or decay widths. The functions
hazma.rambo.PhaseSpace.cross_section()
and
hazma.rambo.PhaseSpace.decay_width()
can be used for these purposes.
These methods just wrap hazma.rambo.PhaseSpace.integrate()
and append
the appropriate prefactors. As an example, let’s compute the muon decay width
for the process \(\mu\to e\nu_{e}\nu_{\mu}\). The squared matrix element
(ignoring the electron mass) is given by:
where \(G_{F}\) is the Fermi constant and \(t=(p_{e}+p_{\nu_{\mu}})^2\). The analytic result is
To compute this, we use the following:
from hazma import rambo
from hazma import utils
GF = 1.166379e-05
MMU = 1.056584e-01
def msqrd(momenta):
p1 = momenta[:, 0]
p3 = momenta[:, 2]
t = utils.lnorm_sqr(p1 + p3)
return 16.0 * gf**2 * t * (MMU**2 - t)
phase_space = rambo.PhaseSpace(cme=mmu, masses=[0.0, 0.0, 0.0], msqrd=msqrd)
width, error = phase_space.decay_width(n=50_000, seed=1234)
analytic = GF**2 * MMU**5 / (192 * np.pi**3)
actual_error = abs(width - analytic)
print(f"width = {width:.2e} +- {error:.2e}")
print(f"actual error = {actual_error:.2e} = {actual_error / analytic * 100:.2f} %")
# Output:
# width = 3.02e-19 +- 5.99e-22
# actual error = 9.50e-22 = 0.32 %
Note
The Utilities (hazma.utils) module contains a couple of functions useful for dealing
with four-vectors, namely, hazma.utils.ldot()
for computing scalar
products and hazma.utils.lnorm_sqr()
for computing the squared
norm.