Building a simple integrator

The earlier examples sampled a phase-space mapping and evaluated a matrix element and a PDF as separate pieces. Integrating a cross section is putting those pieces together and averaging. This example builds a plain, non-adaptive integrator for g g > t t~ g, using the pure t-channel mapping, MadGraph7’s default PDF set and its default HT/2 scale choice.

Assembling the mapping

mode="propagator" follows the t-channel structure implied by the external masses, without RAMBO or Chili’s extra reshuffling. The cuts are the same ones as the cuts example, needed here to keep the matrix element from being evaluated in the soft and collinear regions where it diverges:

import os

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
pids = [21, 21, 6, -6, 21]

O = ms.Observable
cuts = ms.Cuts([
    ms.CutItem(O(pids, O.obs_pt, [O.jet_pids]), min=20.0),
    ms.CutItem(O(pids, O.obs_eta_abs, [O.jet_pids]), max=5.0),
    ms.CutItem(O(pids, O.obs_delta_r, [O.jet_pids]), min=0.4),
])
mapping = ms.PhaseSpaceMapping(masses, E_CM, mode="propagator", cuts=cuts)

Loading the matrix element and the PDF

The same pattern as the matrix element and PDF examples, generated with output mg7 PROC_ggttg and make BACKEND=scalar beforehand:

proc_dir = "PROC_ggttg"
me_path = os.path.join(proc_dir, "lib", "libmadmatrix_P0_gg_ttxg_scalar.so")
param_card = os.path.join(proc_dir, "Cards", "param_card.dat")

ctx = ms.default_context()
api = ctx.load_matrix_element(me_path, param_card)
MEI = ms.MatrixElement.MatrixElementInput
MEO = ms.MatrixElement.MatrixElementOutput
matrix_element = ms.MatrixElement(
    api, [MEI.momenta_in, MEI.alpha_s_in], [MEO.matrix_element_out], True
)

PDF_SET = "NNPDF40_lo_as_01180"  # MadGraph7's default PDF
import lhapdf
pdf_dir = os.path.join(lhapdf.paths()[0], PDF_SET)
pdf_grid = ms.PdfGrid(os.path.join(pdf_dir, f"{PDF_SET}_0000.dat"))
pdf_grid.initialize_globals(ctx)
alphas_grid = ms.AlphaSGrid(os.path.join(pdf_dir, f"{PDF_SET}.info"))
alphas_grid.initialize_globals(ctx)
alpha_s = ms.RunningCoupling(alphas_grid)

Choosing the scale

EnergyScale computes the renormalization and factorization scales from the event momenta. half_transverse_mass is MadGraph7’s default choice, \(H_\mathrm{T}/2\):

scale = ms.EnergyScale(
    particle_count=5, type=ms.EnergyScale.DynamicalScaleType.half_transverse_mass
)

Assembling the cross section

DifferentialCrossSection combines the matrix element with the flux factor, the two PDFs and \(\alpha_s\), all at the chosen scale. pid_options lists the incoming flavour combinations it should accept, here just gluon-gluon:

cross_section = ms.DifferentialCrossSection(
    matrix_element=matrix_element,
    cm_energy=E_CM,
    running_coupling=alpha_s,
    energy_scale=scale,
    pid_options=[[21, 21]],
    pdf1=pdf_grid, pdf2=pdf_grid,
    input_momentum_fraction=True,
)

Integrating

The mapping gives the momenta, the momentum fractions and the phase-space Jacobian. The cross section gives the differential rate at that point. Their product, averaged over the batch, is the Monte Carlo estimate of the total cross section, and its standard error:

rng = np.random.default_rng(0)
n = 200000
r = rng.random((n, mapping.random_dim()))
momenta, x1, x2, det = mapping.map_forward([r])
pdf_id = np.zeros(n, dtype=np.int32)  # index into pid_options; only one here

weight = cross_section(momenta, x1, x2, pdf_id) * det
sigma, error = weight.mean(), weight.std() / np.sqrt(n)
print(f"sigma = {sigma:.2f} +- {error:.2f} pb")
sigma = 246.03 +- 7.43 pb

The relative error, about 3%, is typical of plain sampling with no adaptive importance sampling. The next two examples reduce it.