DLPGenerator is a configurable particle event generator used to produce synthetic interaction topologies for simulation and downstream ML workflows. The core generator is the C++ class DLPGenerator::ParticleBomb, built with ROOT dictionary support so it can be driven from PyROOT and simple Python helpers.
At a high level, the package lets you:
- describe one or more interaction templates in YAML
- sample particle content, positions, times, and kinetic energies from configurable ranges
- run the generator from Python or the
dlpgenCLI - export generated events as HEPEVT-like text, CSV with identifiers, or an
edep-simbomb macro
One call to Generate() returns one batch of generated interactions. A configuration can ask for a variable number of interactions per call via NumEvent, and each interaction can contain a variable number of particles selected from configured species and multiplicity ranges.
The generator can then flatten that nested structure into the 15-column HEPEVT-like row format commonly used to hand particles to Geant-based simulation stages.
To build and run the native generator outside Docker, you need:
- ROOT 6 with
root-config,rootcling, and PyROOT available in your environment - a C++ compiler (
clang++org++) - Python 3
PyYAMLNumPyfor the optional include-path helper used bysetup.sh
If you already have a working ROOT installation, the native build is short:
source /path/to/your/root/bin/thisroot.sh
source setup.sh
make
What this does:
source setup.shchecks thatrootclingis onPATH- it exports
DLPGENERATOR_DIR,DLPGENERATOR_BINDIR,DLPGENERATOR_LIBDIR,PYTHONPATH, and the shared-library search path makebuilds theParticleBombpackage and ROOT dictionary intobuild/
Useful follow-up commands:
dlpgen --help
make test
make clean
make test rebuilds the native library and runs the Python unittest suite
through the public PyROOT interface.
If PyYAML is missing, install it in the Python you plan to use for the CLI or Python bindings. If rootcling is missing, fix the ROOT environment first and then re-run source setup.sh.
After building, you can verify that the package imports and generates events:
python3 - <<'PY'
import yaml
from dlp_generator import create_generator, EXAMPLE_CONFIG
cfg = yaml.load(EXAMPLE_CONFIG, Loader=yaml.Loader)
gen = create_generator(cfg)
result = gen.Generate()
print(f"Generated {len(result)} interaction block(s)")
print(f"Flattened particle count: {len(gen.Flatten(result))}")
PY
The repository includes a Dockerfile based on the official rootproject/root image so you can build and run the generator without installing ROOT directly on the host.
Pull a published release image from GitHub Container Registry:
docker pull ghcr.io/deeplearnphysics/dlpgen:latest
Build the image from the repository root:
docker build -t dlpgen .
If you are building on Apple Silicon and want to match the upstream ROOT image architecture explicitly:
docker build --platform=linux/amd64 -t dlpgen .
Start an interactive container with the repository mounted at runtime:
docker run --rm -it \
-v "$PWD":/workspace/DLPGenerator \
-w /workspace/DLPGenerator \
dlpgen /bin/bash
Inside the container, source the setup script before using the Python module or the compiled library interactively:
source setup.sh
Published images are pushed to GitHub Container Registry as ghcr.io/deeplearnphysics/dlpgen:<tag> when a GitHub Release is published.
If the GitHub release tag is prefixed with v (for example v1.0.0), the published container tag is the stripped semver form (1.0.0) so pulls match the convention used by other DeepLearnPhysics images.
After source setup.sh, the repository exposes a dlpgen command from bin/. In the Docker image, the command is on PATH by default. The Python import path remains dlp_generator.
Run a config and call the generator a fixed number of times, dumping the resulting rows in the default HEPEVT-like text format. Each Generate() call is emitted as one event block: a particle-count header followed by 15-column particle rows.
dlpgen my_config.yaml 10
Write the dumped output to a file instead of stdout:
dlpgen my_config.yaml 10 --output events.hepevt
Emit CSV with a header and explicit identifiers for generator call, interaction, and particle row:
dlpgen my_config.yaml 10 --format csv --output events.csv
Emit an edep-sim bomb macro modeled on the production setup:
dlpgen my_config.yaml 10 --format bomb-macro --output g4.mac
Override the config seed or enable debug output:
dlpgen my_config.yaml 10 --seed 123 --debug
The positional count is the number of Generate() calls, not the number of interactions. If the config has NumEvent: [1, 10], one call can emit between 1 and 10 interactions. In the default HEPEVT-like output, all rows from one call are grouped under one particle-count header, and calls are separated by a blank line.
To emit exactly one interaction per call while drawing from a seeded mixture of interaction types, add weighted interaction selection:
SEED: 12345
InteractionSelection:
Mode: weighted_random
CC:
SelectionWeight: 1
NumEvent: [1, 1]
# ...
NC:
SelectionWeight: 1
NumEvent: [1, 1]
# ...Every call makes an independent weighted draw whose result is reproducible from
SEED. The example therefore approaches 50% CC and 50% NC over a large sample
while allowing consecutive calls of the same type. Selection uses its own
counter-based stream, so interaction type is independent of the selected
block's particle multiplicity. Selected blocks must use NumEvent: [1, 1];
this prevents the selector from reintroducing multiple interactions per call.
Keeping SelectionWeight on each named block lets its probability travel with
its independently configurable particle distribution.
In bomb-macro mode, the positional count is emitted as /run/beamOn <count>, which is the production-style mapping of one bomb generator invocation per Geant event. The production workflow uses one macro per job, not one macro per event.
In CSV mode, each row includes a header plus these identifiers:
call_id= zero-basedGenerate()call indexinteraction_id= zero-based interaction index within that callparticle_id= zero-based particle row index within the flattened callparticle_in_interaction= zero-based particle row index within that interaction
The helper create_generator parses a Python dict with the expected YAML
structure and returns a configured generator object. Ordinary configurations
return DLPGenerator.ParticleBomb; configurations with InteractionSelection
return an object exposing the same Generate, Flatten, PrintHierarchy,
Configured, and Seed interface.
import yaml
from dlp_generator import create_generator, EXAMPLE_CONFIG
cfg = yaml.load(EXAMPLE_CONFIG, Loader=yaml.Loader)
cfg['Debug'] = True
gen = create_generator(cfg)
batch = gen.Generate()
hepevt_rows = gen.Flatten(batch)
gen.PrintHierarchy(hepevt_rows)
Each top-level YAML key other than SEED, Debug, and the optional
InteractionSelection mapping is treated as an interaction block. An interaction block configures:
NumEvent: number of interactions to generate perGenerate()callNumParticle: total particle multiplicity range for an interactionXRange,YRange,ZRange,TRange: uniform position and time rangesAddParent: whether to add a synthetic parent/root particleShootInward: bias particle directions toward the bulk of theXRange/YRange/ZRangevolume so that fewer particles leave it immediately.Trueselects the default strength,False(the default) disables the bias, and a number sets the strength directly:0is isotropic,1is the balanced default, and values much above4are counterproductive (if any spatial range has zero width, the bias is disabled and directions follow the usual isotropic sampling within the configured angular window)Particles: one or more particle templatesShootInwardStrict: optional boolean, defaultFalse. IfTrue, exhausting 1,000 inward direction proposals throws an exception andGenerate()returns no batch. IfFalse, the existing warning and last-proposal fallback remain; that fallback can distort the requested direction distribution.SelectionWeight: relative probability when root-levelInteractionSelectionis enabledBoost: optional common momentum boost for each interaction.BetaGammaRangesets a uniform, dimensionless βγ magnitude range. Directions are isotropic unlessPhiRange/ThetaRange(radians) or a fixedDirectionvector is supplied.
For example, add this to an interaction block to boost along the +z beam axis:
Boost:
BetaGammaRange: [0.5, 2.0]
Direction: [0, 0, 1]Omit Direction for isotropic boost directions, or replace it with angular
bounds such as PhiRange: [0, 6.283185307179586] and ThetaRange: [0, 0.1].
Do not combine Direction with angular bounds. Equal range endpoints specify
a fixed value. Omitting Boost, or setting BetaGammaRange: [0, 0], preserves
the original seeded output.
The boost changes particle energies and momenta together, leaving the sampled
vertex and time fixed. Particle energy ranges and ShootInward apply before
the boost; final energies can exceed those ranges and final directions need
not point inward. This is a boost of the generated configuration, without
first transforming it to its center-of-momentum frame.
Each particle template configures:
PDG: allowed PDG codes to sample fromNumRange: multiplicity range for that particle templateKERange: kinetic-energy rangeUseMom: interpret the energy range as momentum instead of kinetic energyWeight: relative sampling weight
See the documentation notebooks in book/Introduction.md and book/Configuration.md for worked examples.