Backgrounds in ePIC simulation
How signal events get mixed with the official ePIC background into 2 µs timeframes, what gets called when, and what flags actually matter.
TL;DR
generator → abconv (afterburner) → SignalBackgroundMerger → npsim → eicrecon
(rest frame) (beam effects (Poisson into 2 µs (with status-
applied once, timeframes, every offset flags)
cached on XrootD) source tagged with a
generator-status offset)The big picture
ePIC's background-mixed simulations are built in three independent stages.
Afterburner — Applies crossing angle, beam effects.
SignalBackgroundMerger — Pulls one already-afterburned signal file and background files (four or five with different backgrounds), and Poisson-samples them into 2 µs timeframes. Each background source's particles get a fixed generator-status offset added so downstream code can tell what came from where.
npsim — runs Geant4 on the merged HepMC3. Needs special flags (compared to usual EPIC runs) to be told that the shifted status codes (2001, 3001, 4001, 5001, …) also mean "stable, propagate" / "decayed, skip", otherwise it drops them silently.
The merger
Repo: eic/HEPMC_Merger · CLI binary SignalBackgroundMerger · Spack package: hepmcmerger
For each 2 µs timeframe (default 2000 ns) the merger:
- places exactly one signal event (because the campaigns set
--signalFreq 0, which is the merger's "guaranteed one signal per slice" mode), - draws each background source's event count from a Poisson with mean
freq × slice_length, - distributes particles uniformly in time inside the slice,
- and adds the source's
statusvalue to everyMCParticle.generatorStatusit touches.
The current campaign convention for status offsets is:
| Source | Offset | Stable code | Decay code |
|---|---|---|---|
| signal | 0 | 1 | 2 |
| synrad | 2000 | 2001 | 2002 |
| ebrems | 3000 | 3001 | 3002 |
| ecoulomb | 4000 | 4001 | 4002 |
| etouschek | 5000 | 5001 | 5002 |
| p-beam-gas | 6000 | 6001 | 6002 |
(See eic/eic.github.io / background_mixed_samples for the official conventions and worked examples with timeframe diagrams. Note: as of 2026-09 that page lists etouschek = 4000 / ecoulomb = 5000 — swapped relative to the config_data/*.json cocktails actually used by the campaigns, which assign coulomb = 4000 / touschek = 5000 as in the table above. The JSONs are the ground truth for what ran.)
Where the cocktail recipes live
Repo: eic/simulation_campaign_datasets
config_data/*.json — one cocktail per beam-energy / vacuum-condition combination. Each file is a list of entries like:
[
{"file": "root://.../synrad_18x275_run001....hepmc3.tree.root",
"freq": 3324000, "skip": 0.0, "status": 2000},
{"file": "root://.../GETaLM..._ElectronBeamGas_18GeV_….hepmc3.tree.root",
"freq": 316.94, "skip": 0.0, "status": 3000},
{"file": "root://.../electron_coulomb_18x275_….hepmc3.tree.root",
"freq": 0.86, "skip": 0.0, "status": 4000},
{"file": "root://.../electron_touschek_18x275_….hepmc3.tree.root",
"freq": 0.55, "skip": 0.0, "status": 5000}
]Field meanings:
| Field | Meaning |
|---|---|
file | XrootD URL of an already-prepared background HepMC3. |
freq | Poisson rate in kHz (so 3 324 000 kHz = 3.3 GHz for synrad). |
skip | Number of events to skip before sampling. 0.0 in the current JSONs — run.sh ignores it and computes its own per-job skip (rate-scaled by task index plus a seed-driven random offset) so parallel jobs read decorrelated windows of the same source. |
status | Generator-status offset added to every particle from this source. |
Merger command (what the campaign actually runs)
SignalBackgroundMerger \
--rngSeed <per-job seed> \
--nSlices <events_per_task> \
--signalSkip <events already consumed by earlier tasks> \
--signalFile <afterburned signal>.hepmc3.tree.root \
--signalFreq 0 \
--signalStatus 0 \
--intWindow 2000 \
--bgFile <synrad_url> 3324000 <skip> 2000 \
--bgFile <egas_url> 316.94 <skip> 3000 \
--bgFile <coulomb_url> 0.86 <skip> 4000 \
--bgFile <touschek_url> 0.55 <skip> 5000 \
--outputFile merged.hepmc3.tree.root(--bgFile takes <file> <freq kHz> <n-events-to-skip> <status offset>; the per-source <skip> is computed by run.sh per job, see above. Cocktails with hadron beam gas add a fifth --bgFile ... 6000 line.)
--signalFreq 0 is the special "one signal per slice" mode. Setting it to a nonzero kHz value would make signals Poisson-sampled too, which is the right thing for low-rate processes like SIDIS-pythia6 where you may want events that are pure background.
There is a second mixer
eic/TimeframeBuilder is the newer mixer (evaluated in detector_benchmarks). It can mix in pre-simulated edm4hep files too, supports beam-attachment / bunch-crossing discretisation / Gaussian time spread, and is reviewed via CI capybara reports. Production campaigns still use SignalBackgroundMerger, so that's what this pipeline calls.
Stage 3 — npsim with status-offset flags
DD4hep / npsim by default treats only generatorStatus == 1 particles as Geant4 primaries and == 2 as decayed-intermediate. The merger has just created particles with statuses like 2001 or 3002 — unknown numbers, silently dropped.
The two flags that fix this:
npsim ... \
--hepmc3.useHepMC3 true \
--runType batch \
--physics.alternativeStableStatuses "1 2001 3001 4001 5001 6001" \
--physics.alternativeDecayStatuses "2 2002 3002 4002 5002 6002" \
--inputFiles merged.hepmc3.tree.root \
--outputFile sim.edm4hep.rootThe campaign's run.sh builds these lists dynamically from the cocktail: signalStatus+1/+2 plus status+1/+2 for every --bgFile source. So a four-source (vacuum) cocktail yields "1 2001 3001 4001 5001", and the hgas cocktails with proton beam gas add 6001 / 6002 as shown above.
- The lists fully replace, not augment. Forgetting
1in the stable list = signal events stop reaching Geant4. - Don't forget
--hepmc3.useHepMC3 true. Without it npsim falls back to the older HepMC2 reader, which handles.hepmc3.tree.rootinput differently and may not honour the alternative status lists at all. --runType batch, notrun.runis for steering/gun input;batchis the right mode for--inputFiles.
The defining file in DD4hep is DDG4/python/DDSim/Helper/Physics.py — that's where _alternativeStableStatuses / _alternativeDecayStatuses live, and they are exposed verbatim on the npsim CLI.
If you write a steering file instead of using CLI flags:
from DDSim.DD4hepSimulation import DD4hepSimulation
SIM = DD4hepSimulation()
SIM.physics.alternativeStableStatuses = {1, 2001, 3001, 4001, 5001}
SIM.physics.alternativeDecayStatuses = {2, 2002, 3002, 4002, 5002}
SIM.hepmc3.useHepMC3 = TrueWhere the campaign code lives (for cross-reference)
| What | Repo |
|---|---|
Afterburner (abconv) | eic/afterburner |
| Signal-background merger | eic/HEPMC_Merger |
| Newer timeframe mixer | eic/TimeframeBuilder |
Per-job campaign driver (run.sh) | eic/simulation_campaign_hepmc3 |
| Condor submitter, BG-fetch glue | eic/job_submission_condor — archived 2026-08; eic/job_submission_slurm is the active submitter repo |
| Cocktail JSONs + CSV chunk manifests | eic/simulation_campaign_datasets |
| ePIC production docs (release tags, paths) | eic/epic-prod |
| Official background-mixed sample docs | eic/eic.github.io / background_mixed_samples |
| Spack package for the merger | spack_repo/eic/packages/hepmcmerger/package.py in eic/eic-spack |
Background-mixed Rucio metadata (is_background_mixed) | scripts/register_to_rucio.py in eic/simulation_campaign_hepmc3 |
XrootD / Rucio access
Outputs of the campaign land in xroots://dtn2201.jlab.org//eic/eic2/EPIC/{RECO,FULL,LOG}/<release>/<detector>/... and are registered to Rucio under account eicprod with a metadata schema that includes is_background_mixed: true|false. The update-data-md skill in this repo queries Rucio with that flag to rebuild docs/data.md.
Mixed simulation outputs from official campaigns can also be browsed directly:
root://dtn-eic.jlab.org//volatile/eic/EPIC/FULL/<release>/epic_craterlake/Bkg_1SignalPer2usFrame/...
root://dtn-eic.jlab.org//volatile/eic/EPIC/RECO/<release>/epic_craterlake/Bkg_1SignalPer2usFrame/...(Increment the <release> tag — e.g. 25.06.1 — to find the latest campaign. RECO is preserved across campaigns; FULL is not always.)