Custom functions¶
MG7 can call your own code for four parts of the calculation:
cutDecides if a phase-space point passes the cuts. It is applied on top of the
[cuts]of the run card.scaleThe renormalization scale and the factorization scales of both incoming partons. It replaces
dynamical_scale_choice, likeuser_dynamical_scalein MadEvent.pdfThe parton densities. It replaces the LHAPDF set of
[beam] pdf.biasAn event bias. More events are generated where the bias is large, and their weights are lowered to compensate.
You write the function either for a single phase-space point in C, C++, Fortran, CUDA or HIP, or for a whole batch of points in Python with numpy or PyTorch. MG7 builds a compiled function into a shared library and calls it on batches of events. The library implements the UMAMI interface, the same interface the matrix elements use. A generated wrapper takes care of that, so you only write the function itself. A Python function needs no build. MG7 imports it and calls it through the same interface.
Creating a custom function¶
Run bin/init_custom_func in the process directory. Without options, it asks which
function and language you want:
$ ./bin/init_custom_func
Which custom function do you want to create?
1. cut a cut, true keeps the event and false rejects it
2. scale the renormalization and factorization scales
3. pdf a parton density x f(x, Q)
4. bias an event bias for the unweighting
Which language do you want to write it in?
1. c C
2. cpp C++
3. fortran Fortran
4. cuda CUDA, for runs on NVIDIA GPUs
5. hip HIP, for runs on AMD GPUs
6. python-numpy Python with numpy, for CPU runs, no compilation
7. python-torch Python with PyTorch, for CPU or GPU runs, no compilation
The same choices are available as options:
./bin/init_custom_func --function cut --language cpp
-f and -l are short for --function and --language. -y takes the
defaults (cut and cpp) for the options you leave out instead of asking.
--force overwrites an existing directory. --no-build skips the build.
--no-set-run-card leaves the run card unchanged.
For a compiled language, the script creates a directory custom_<function>:
custom_<function>.<ext>The function body.
parameters.hThe parameters of the function, see Parameters.
wrapper_cpu.corwrapper_gpu.cuThe UMAMI wrapper. It unpacks the batched inputs and calls your function once per event.
umami.hThe UMAMI header.
MakefileBuilds
lib/custom_<function>.so.
For python-numpy and python-torch, the directory only holds the function,
custom_<function>.py.
By default, the script builds the default function right away and enables it in
Cards/run_card.toml:
[custom_functions]
cut = "lib/custom_cut.so"
A Python function is enabled with the path of its file:
[custom_functions]
cut = "custom_cut/custom_cut.py"
bin/generate_events runs make in custom_<function> before each run. Your
changes are therefore picked up without a manual rebuild. The build output goes to
custom_<function>/compile.log.
Function signatures¶
The cut, scale and bias functions receive the external particles of one event. The
incoming particles come first. The momenta are in the lab frame, in GeV, with components
(E, px, py, pz). The PDG ids are those of the first flavor of the subprocess for cuts
and scales, like the built-in cuts, and those of the sampled flavor for the bias. Every
function also receives its parameters, see Parameters.
C, C++ and CUDA/HIP:
#include "parameters.h"
bool custom_cut(int particle_count, const int* pdg_ids, const double momenta[][4],
const CustomParameters* params);
void custom_scale(int particle_count, const int* pdg_ids, const double momenta[][4],
const CustomParameters* params,
double* ren_scale, double* fact_scale1, double* fact_scale2);
double custom_bias(int particle_count, const int* pdg_ids, const double momenta[][4],
const CustomParameters* params);
double custom_pdf(int beam_index, int pdg_id, double x, double q,
const CustomParameters* params);
In C, bool comes from <stdbool.h>. In C++, the functions are declared
extern "C". In CUDA and HIP, they are __device__ functions and run on the GPU, one
thread per event.
Fortran, in a .F90 file, so that parameters.h goes through the preprocessor:
function custom_cut(particle_count, pdg_ids, momenta, params) bind(C, name="custom_cut") result(value)
use iso_c_binding
implicit none
#include "parameters.h"
integer(c_int), value :: particle_count
integer(c_int), intent(in) :: pdg_ids(particle_count)
real(c_double), intent(in) :: momenta(0:3, particle_count)
type(custom_parameters), intent(in) :: params
logical(c_bool) :: value
The bias returns real(c_double). The scale is a subroutine with the additional
arguments real(c_double), intent(out) :: ren_scale, fact_scale1, fact_scale2. The PDF
takes integer(c_int), value :: beam_index, pdg_id and real(c_double), value :: x, q
before params.
What the functions return:
cutTrue to keep the event, false to reject it.
scaleThe renormalization scale
ren_scaleand the factorization scalesfact_scale1andfact_scale2of the first and second incoming particle,momenta[0]andmomenta[1], in GeV.scale_factoris applied on top.fixed_ren_scaleandfixed_fact_scalestill take precedence.pdf\(x f(x, Q)\) for the parton
pdg_id(21 for the gluon) in beambeam_index(0 or 1), at momentum fractionxand factorization scaleq.biasThe bias factor \(b > 0\). The phase space is sampled and unweighted according to \(|w|\,b\). The written events carry the weight divided by \(b\). Distributions filled with the event weights therefore stay unbiased, while regions with a large bias get more events. The reported cross section does not include the bias.
Parameters¶
A function can take parameters whose values are set in the run card, so that a cut value,
for example, can be changed without editing the function. For a compiled function, they
are declared with their default values in custom_<function>/parameters.h:
#define CUSTOM_PARAMETERS(X) \
X(pt_min, 20.) \
X(eta_max, 2.5)
The function reads them from params, as params->pt_min in C, C++ and CUDA/HIP and
as params%pt_min in Fortran. Rebuild the function after changing the list. A Python
function declares them in the dictionary PARAMETERS and gets them as its last argument,
params.pt_min:
PARAMETERS = {"pt_min": 20., "eta_max": 2.5}
def custom_cut(momenta, pdg_ids, params):
...
A Python module without PARAMETERS has no params argument. The values are set in
the [custom_functions] section of the run card, as <function>_parameters.<name>:
[custom_functions]
cut = "lib/custom_cut.so"
cut_parameters.pt_min = 30.0
Parameters that are not set keep their default value. Setting a parameter that the function does not declare is an error. All parameters are floating-point numbers. A gridpack keeps the values of the run that made it.
Python functions¶
A Python function is called once per batch of phase-space points, with arrays that hold
one entry per point. The file defines the function custom_<function>, the framework it
uses and optionally its parameters:
import numpy as np
FRAMEWORK = "numpy" # or "torch"
PARAMETERS = {"pt_min": 20.}
def custom_cut(momenta, pdg_ids, params):
...
The cut, scale and bias functions take (momenta, pdg_ids, params):
momentafloat64, shape
(batch, particle_count, 4). The momenta(E, px, py, pz)of the external particles in GeV, lab frame, incoming particles first.pdg_idsint32, shape
(batch, particle_count). Their PDG ids, as for the compiled functions.paramsThe values of
PARAMETERS, as attributes.
The PDF function takes (beam_index, pdg_id, x, q, params), each with shape
(batch,) except params. beam_index and pdg_id are int32, x and q
are float64.
The cut returns a boolean array of shape (batch,). The scale returns a tuple of the
three float64 arrays (ren_scale, fact_scale1, fact_scale2), or a single array used for
all three. The PDF and the bias return a float64 array of shape (batch,). The meaning
is the same as for the compiled functions. MG7 checks the type, the dtype, the shape and the
device of the result. With numpy the arguments are numpy arrays, and with
FRAMEWORK = "torch" they are torch tensors. They are views of MG7’s own buffers, so they
must not be modified or kept after the call.
numpy functions run on the CPU only. torch functions run on the device of the run, so the same file works for CPU and GPU runs. On the GPU the function runs on the CUDA or HIP stream of the MG7 runtime, and the arguments and the result stay on the GPU.
The same cut as in the C++ example below, with numpy:
import numpy as np
FRAMEWORK = "numpy"
PARAMETERS = {"pt_min": 150.}
def custom_cut(momenta, pdg_ids, params):
pt = np.hypot(momenta[:, 2:4, 1], momenta[:, 2:4, 2])
return (pt >= params.pt_min).all(axis=1)
\(H_T/2\) with torch, for CPU or GPU runs:
import torch
FRAMEWORK = "torch"
def custom_scale(momenta, pdg_ids):
final = momenta[:, 2:]
mt = torch.sqrt(torch.clamp(final[..., 0] ** 2 - final[..., 3] ** 2, min=0.))
return mt.sum(dim=1) / 2.
On the CPU, MG7 splits each batch over the threads of cpu_thread_pool_size and calls
the function from these threads. Python runs one call at a time, but numpy and torch
release this lock inside their array operations, so vectorized functions still run
partly in parallel. MG7 limits torch to one thread on the CPU, so that it does not start
threads of its own on top. numpy uses several threads only for linear algebra, such as
matrix products. If your function relies on it, set OMP_NUM_THREADS=1.
The Python API behind these functions is madspace.PythonFunction. It can also be used
directly with madspace.
Examples¶
A cut requiring \(p_T > p_{T,\min}\) for both particles of a p p > t t~ process,
in C++, with X(pt_min, 150.) in parameters.h:
#include <cmath>
#include "parameters.h"
extern "C" bool custom_cut(int particle_count, const int* pdg_ids,
const double momenta[][4], const CustomParameters* params) {
for (int i = 2; i < 4; ++i) {
if (std::hypot(momenta[i][1], momenta[i][2]) < params->pt_min) {
return false;
}
}
return true;
}
\(H_T/2\) as all three scales, in Fortran:
subroutine custom_scale(particle_count, pdg_ids, momenta, params, ren_scale, fact_scale1, fact_scale2) &
bind(C, name="custom_scale")
use iso_c_binding
implicit none
#include "parameters.h"
integer(c_int), value :: particle_count
integer(c_int), intent(in) :: pdg_ids(particle_count)
real(c_double), intent(in) :: momenta(0:3, particle_count)
type(custom_parameters), intent(in) :: params
real(c_double), intent(out) :: ren_scale, fact_scale1, fact_scale2
integer :: i
ren_scale = 0d0
do i = 3, particle_count
ren_scale = ren_scale + sqrt(max(momenta(0, i)**2 - momenta(3, i)**2, 0d0))
end do
ren_scale = ren_scale / 2d0
fact_scale1 = ren_scale
fact_scale2 = ren_scale
end subroutine custom_scale
A bias that populates the \(p_T\) tail of the first final-state particle, in C, with
X(pt0, 100.) in parameters.h:
#include <math.h>
#include "parameters.h"
double custom_bias(int particle_count, const int* pdg_ids, const double momenta[][4],
const CustomParameters* params) {
double pt2 = momenta[2][1] * momenta[2][1] + momenta[2][2] * momenta[2][2];
return fmax(pt2 / (params->pt0 * params->pt0), 1.);
}
An LHAPDF set called through the LHAPDF C++ API:
#include <mutex>
#include "LHAPDF/LHAPDF.h"
#include "parameters.h"
extern "C" double custom_pdf(int beam_index, int pdg_id, double x, double q,
const CustomParameters* params) {
static LHAPDF::PDF* pdf = LHAPDF::mkPDF("NNPDF40_lo_as_01180", 0);
static std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex);
return pdf->xfxQ(pdg_id, x, q);
}
The function is called from several threads, so the call into LHAPDF is protected by a
lock. To build it, pass the LHAPDF flags to make:
make -C custom_pdf CFLAGS="$(lhapdf-config --cflags)" \
LIBS="$(lhapdf-config --libs) -Wl,-rpath,$(lhapdf-config --libdir)"
Building¶
The Makefile takes the usual CC, CXX and FC compilers, with gfortran
as the default for Fortran. OPTFLAGS defaults to -O3. Extra compiler flags go into
CFLAGS and extra libraries into LIBS. The CUDA and HIP builds detect the GPU
architecture like the matrix elements, and accept the same MADGRAPH_CUDA_ARCHITECTURE
and MADGRAPH_HIP_ARCHITECTURE overrides.
The language decides the device the function runs on. C, C++ and Fortran functions run on
the CPU, and CUDA and HIP functions run on the GPU. Use the language that matches the
device of the run. Python functions are not built. numpy functions run on the CPU, and
torch functions run on the device of the run.
Limitations¶
With a custom PDF, \(\alpha_s\) still comes from the PDF set of
[beam] pdf, and the scale and PDF variations of[systematics]are disabled.With a custom bias, the weighted integration histograms of
[run] weighted_histogramsinclude the bias. The event weights do not.Decays ignore a custom scale and a custom PDF.
A gridpack stores the custom libraries and Python files it was made with. They cannot be changed in the gridpack. A Python function in a gridpack needs the same Python packages, such as numpy or torch, where the gridpack runs.