Skip to content

Backgrounds in ePIC simulation ​

How signal events get mixed with the official ePIC beam backgrounds into 2 µs timeframes, which tool does it, and which flags matter downstream.

TL;DR

generator  →  abconv (afterburner)  →  SignalBackgroundMerger  →  npsim  →  eicrecon
   (rest frame)     (beam effects        (Poisson into 2 µs       (with status-
                     applied once)        timeframes, every         offset flags)
                                          source tagged with a
                                          generator-status offset)

This is the chain our pipeline runs (stages 10 → 11 → 22 → 30). Our stage 11 calls the SignalBackgroundMerger binary from eic/HEPMC_Merger. Its successor is eic/TimeframeBuilder (executable timeframe_builder).

The big picture ​

Background-mixed simulation is built in three independent stages.

  1. Afterburner — applies crossing angle and beam effects to the signal.
  2. Mixer — takes one afterburned signal file and four or five background files and Poisson-samples them into 2 µs timeframes. Every background source's particles get a fixed generator-status offset added, so downstream code can tell what came from where.
  3. npsim — runs Geant4 on the merged HepMC3. It needs extra flags that declare the shifted status codes (2001, 3001, …) as "stable, propagate" and (2002, 3002, …) as "decayed"; otherwise these particles are not handed to Geant4.

Mixing tools ​

TimeframeBuilder — the successor ​

Repo: eic/TimeframeBuilder · executable timeframe_builder · Spack package timeframebuilder · latest release v0.9.2 (2026-10-01)

Shipped in the eic_xl container from v26.08.0-stable on (containers spack env; master pins timeframebuilder@0.9.2). Earlier images, including eic_xl:26.06.0-stable and 26.07.x, have only SignalBackgroundMerger.

eic_xl:26.10.0-stable (also on CVMFS) carries both timeframe_builder 0.9.2 and SignalBackgroundMerger in /opt/local/bin, with epic 26.10.0, EICrecon 1.42.0, DD4hep 01-38, irt2 2.2.3 and an XRootD 5.9.2 client.

Production status (public eic repositories, 2026-10-14):

  • Packaged: Spack package since 2026-07 (eic-spack#992), added to the containers in containers#364, pinned to 0.9.2 for v26.10.0-stable (containers#444).
  • Campaign switch prepared: simulation_campaign_hepmc3#102 replaces SignalBackgroundMerger with timeframe_builder in run.sh (kHz → events/ns, per-source status_offset, repeat_on_eof). It was under review, with an open question on output equivalence, when the repository was archived on 2026-09-18.
  • Still active: eic/HEPMC_Merger (pushed 2026-10-05). No public eic repository runs timeframe_builder in production yet.
  • In development: podio and Arrow reader/writer backends and a JANA2 event source (open PRs).

Differences from SignalBackgroundMerger:

  • inputs can be .hepmc3.tree.root (merge before simulation) or already simulated .edm4hep.root (merge hits after simulation, keeping particle–hit references); the output format follows the output file extension;
  • per-source options for bunch-crossing time discretisation, attachment of background vertices to the beam (beam_angle, beam_speed) and Gaussian time spread (beam_spread);
  • already merged timeframe files can be used as a source (already_merged), so overlays can be built step by step;
  • CI compares EICrecon output for hepmc3-merged vs edm4hep-merged inputs (capybara report).

Configuration is a YAML file, a CLI, or both (CLI overrides YAML). Abridged from configs/config_epic_18x275.yml:

yaml
timeframe_duration: 2000.0      # ns
bunch_crossing_period: 40.0     # ns
introduce_offsets: true
sources:
  - name: signal
    input_files: [signal.edm4hep.root]
    static_number_of_events: true
    static_events_per_timeframe: 1      # one signal per frame
    use_bunch_crossing: true
    generator_status_offset: 0
  - name: electron_beamgas_brems
    input_files: [ebrems.edm4hep.root]
    static_number_of_events: false
    mean_event_frequency: 0.00031694    # GHz = events/ns (316.94 kHz)
    use_bunch_crossing: true
    attach_to_beam: true
    beam_angle: -3.141592653589793      # rad
    beam_speed: 299.792458              # mm/ns
    beam_spread: 0.003                  # ns
    generator_status_offset: 3000
    repeat_on_eof: true
  # ... synrad, coulomb, touschek, hadron beam gas, bethe_heitler, minbias

Other per-source keys: skip, keep_weight, tree_name. Global keys: output_file, max_events, random_seed, writer.

The same settings on the command line:

bash
timeframe_builder --config config_epic_18x275.yml \
    -o merged.edm4hep.root -n 1000 --random-seed 42 \
    --source:signal:input_files signal.edm4hep.root \
    --source:ebrems:frequency 0.00031694 \
    --source:ebrems:status_offset 3000

Units differ from SignalBackgroundMerger: frequencies are in events/ns (GHz), so the cocktail value 316.94 kHz becomes 0.00031694. The timeframe length defaults to 2000 ns in the code (MergerConfig.h); the README table still says 20 ns.

SignalBackgroundMerger — what our pipeline runs ​

Repo: eic/HEPMC_Merger · executable SignalBackgroundMerger · Spack package hepmcmerger (@2.4.0 in current eic_xl). There is no separate eic/SignalBackgroundMerger repository; the binary is built from HEPMC_Merger.

For each timeframe (--intWindow, default 2000 ns) the merger:

  • places exactly one signal event when --signalFreq 0 (the default); a nonzero kHz value Poisson-samples the signal too, so some frames are pure background;
  • draws each background source's event count from a Poisson with mean freq × window;
  • distributes events uniformly in time inside the frame;
  • adds the source's status offset to every particle's generator status;
  • cycles a background file from its start when it reaches the end.
OptionMeaning
--signalFileAfterburned signal .hepmc3.tree.root
--signalFreqSignal rate in kHz; 0 = one signal per frame
--signalSkipSignal events to skip
--signalStatusStatus offset for the signal (campaigns use 0)
--bgFile <file> <freq kHz> [skip] [status]One background source; repeatable
--intWindowTimeframe length, ns (default 2000)
--nSlicesNumber of timeframes (default 10000; -1 = whole signal file)
--rngSeedRandom seed
--outputFileOutput file (default bgmerged.hepmc3.tree.root)
--squashTimeIntegrate, but write no time information to vertices

Generator-status offsets ​

Two conventions are in use. The campaign cocktail JSONs, and our copies of them, swap Coulomb and Touschek relative to the eic.github.io background_mixed_samples page and the TimeframeBuilder ePIC configs.

SourceCocktail JSONs (our runs)eic.github.io, TimeframeBuilder configs
signal00
synrad20002000
ebrems30003000
ecoulomb40005000
etouschek50004000
p-beam-gas60006000
bethe_heitler—7000 (TimeframeBuilder only)
minbias—0 (TimeframeBuilder only)

Stable code = offset + 1, decay code = offset + 2. To tell Coulomb from Touschek in a given sample, read the cocktail JSON that produced it. The eic2ai converter does not need the per-source value: its prt_origin treats generator status 1–2 as signal and ≥ 1000 as background, and Geant4 secondaries inherit from their first generator ancestor (data format).

Cocktails ​

Upstream: config_data/*.json in eic/simulation_campaign_datasets — one cocktail per beam energy / vacuum condition. Our copies live in background_cocktails/ of the pipeline repo; they match upstream except for the XrootD host: ours still point at root://dtn-eic.jlab.org//volatile/eic/EPIC/..., a host that no longer resolves; upstream moved to root://dtn2304.jlab.org:8443//jlab-osdf-ro/eic/EPIC/volatile/... (simulation_campaign_datasets#66). On the farm strip_xrootd_prefix: true turns them into local /volatile paths.

Each file is a list of sources:

json
[
  {"file": "root://.../dataprod_rel_1.0.0_synrad_18x275_run001.preproc_10000repeats.hepmc3.tree.root",
   "freq": 3324000, "skip": 0.0, "status": 2000},
  {"file": "root://.../GETaLM1.0.0-1.1_ElectronBeamGas_18GeV_emin10keV_run001.hepmc3.tree.root",
   "freq": 316.94,  "skip": 0.0, "status": 3000},
  {"file": "root://.../dataprod_rel_1.0.1_electron_coulomb_18x275_10000Ahr.hepmc3.tree.root",
   "freq": 0.86,    "skip": 0.0, "status": 4000},
  {"file": "root://.../dataprod_rel_1.0.1_electron_touschek_18x275_10000Ahr.hepmc3.tree.root",
   "freq": 0.55,    "skip": 0.0, "status": 5000},
  {"file": "root://.../pythia8.306-1.0_ProtonBeamGas_275GeV_combined.hepmc3.tree.root",
   "freq": 22.5,    "skip": 0.0, "status": 6000}
]
FieldMeaning
fileXrootD URL of a prepared background HepMC3.
freqPoisson rate in kHz (3 324 000 kHz = 3.3 GHz for synrad).
skipBackground events to skip before sampling. 0.0 in all current JSONs.
statusGenerator-status offset added to every particle from this source.

Files with hgas in the name add the proton beam-gas source (status 6000).

Merge commands ​

Our pipeline (stage 11) ​

simulation_pipeline/11_create_background_jobs.py reads the cocktail named in the config's background_configs[energy] and writes one job per afterburned file:

bash
SignalBackgroundMerger \
    --rngSeed      <md5 of the file basename> \
    --nSlices      <event_count> \
    --signalFile   <afterburned>.hepmc3.tree.root \
    --signalFreq   0 \
    --signalStatus 0 \
    --bgFile <synrad>   3324000 0 2000 \
    --bgFile <ebrems>    316.94 0 3000 \
    --bgFile <coulomb>     0.86 0 4000 \
    --bgFile <touschek>    0.55 0 5000 \
    --bgFile <p-gas>       22.5 0 6000 \
    --outputFile <afterburned>.bg.hepmc3.tree.root
  • --intWindow is not passed; the 2000 ns default applies.
  • skip comes from the JSON (0) and is cast to an integer — SignalBackgroundMerger only reads the 3rd and 4th --bgFile values when they are plain integers.
  • With strip_xrootd_prefix: true the root://host/ prefix is removed and the files are read from the local mount (/volatile/eic/EPIC/EVGEN/BACKGROUNDS on the JLab farm).
  • --signalSkip is not passed: one job per afterburned file, read from its first event.

ePIC campaigns (archived driver) ​

The campaign driver scripts/run.sh in eic/simulation_campaign_hepmc3 (archived 2026-09) ran:

bash
SignalBackgroundMerger \
    --rngSeed     <per-job seed> \
    --nSlices     <events_per_task> \
    --signalSkip  <events consumed by earlier tasks> \
    --signalFile  <afterburned signal>.hepmc3.tree.root \
    --signalFreq  ${SIGNAL_FREQ:-0} \
    --signalStatus ${SIGNAL_STATUS:-0} \
    --intWindow   2000 \
    --bgFile <file> <freq> <skip> <status>   # one per cocktail entry
    --outputFile  <task>.hepmc3.tree.root

Here the per-source <skip> ignores the JSON value: it is freq × window × task index plus a seed-driven random offset, so parallel tasks read decorrelated parts of the same background file.

npsim with status-offset flags ​

DD4hep maps generator status 1 to stable and 2 to decayed, then looks codes it does not know up in two extra sets; any other code is marked "other" and is not handed to Geant4 as a primary. The sets are filled from the npsim CLI:

bash
npsim ... \
    --runType                           batch \
    --hepmc3.useHepMC3                  true \
    --physics.alternativeStableStatuses "1 2001 3001 4001 5001 6001" \
    --physics.alternativeDecayStatuses  "2 2002 3002 4002 5002 6002" \
    --inputFiles                        merged.hepmc3.tree.root \
    --outputFile                        sim.edm4hep.root

Our stage 22 (22_create_npsim_background_jobs.py) and the campaign run.sh both build these lists from the cocktail: signalStatus + 1/2 plus status + 1/2 for every source. A four-source cocktail gives "1 2001 3001 4001 5001"; hgas cocktails add 6001 / 6002.

  • The alternative sets only add codes; 1 and 2 are handled before them in Geant4InputAction::setGeneratorStatus. Listing 1 and 2 is harmless.
  • --hepmc3.useHepMC3 defaults to true when DD4hep is built with HepMC3; both scripts set it explicitly.
  • --runType batch is used with --inputFiles; the campaign used --runType run only for particle-gun steering files.

The options are defined in DD4hep DDG4/python/DDSim/Helper/Physics.py and exposed unchanged on the npsim CLI. In a steering file:

python
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 = True

Where the code lives ​

WhatRepo
Afterburner (abconv)eic/afterburner
Current timeframe mixer (timeframe_builder)eic/TimeframeBuilder
Mixer used by our pipeline (SignalBackgroundMerger)eic/HEPMC_Merger
Cocktail JSONs + CSV chunk manifestseic/simulation_campaign_datasets
Our pipeline: stages 11 / 22, cocktail copiesJeffersonLab/msf-simulation-pipeline
Campaign driver (run.sh), Rucio registrationeic/simulation_campaign_hepmc3 — archived 2026-09
Condor / Slurm submitterseic/job_submission_condor, eic/job_submission_slurm — both archived
ePIC production docs (release tags, paths)eic/epic-prod
Official background-mixed sample docseic/eic.github.io / background_mixed_samples
Spack packageshepmcmerger, timeframebuilder in eic/eic-spack

XrootD / Rucio access ​

XRootD needs a 5.9+ client with TLS

JLab XRootD (root://dtn2304.jlab.org:8443) accepts only XRootD clients 5.9 and newer, and the login runs over TLS, so the client needs a CA directory. Check with xrdcp --version. eic_xl 26.06-stable and later work as shipped (5.9.1+, X509_CERT_DIR=/opt/local/certificates; 26.10.0-stable: 5.9.2). Both problems show as Login failed: Got invalid login response: an older client, or a 5.9 client without CA certificates (XRD_LOGLEVEL=Debug then prints Failed to initialize TLS context: CA directory does not exist). For the second, set export X509_CERT_DIR=/etc/ssl/certs. The old host dtn-eic.jlab.org is retired.

The archived campaign driver wrote outputs to xroots://dtn2201.jlab.org//eic/eic2/EPIC/{RECO,FULL,LOG}/<release>/<detector>/... and registered them to Rucio under account eicprod, with metadata that includes is_background_mixed: true|false. The update-data-md skill queries Rucio with that flag to rebuild Data.

Mixed outputs of official campaigns can be browsed at the read endpoint used by eic.github.io:

root://dtn2304.jlab.org:8443//jlab-osdf-ro/eic/EPIC/volatile/FULL/<release>/epic_craterlake/Bkg_1SignalPer2usFrame/...
root://dtn2304.jlab.org:8443//jlab-osdf-ro/eic/EPIC/volatile/RECO/<release>/epic_craterlake/Bkg_1SignalPer2usFrame/...

Other modes: Bkg_RealisticSignalPer2usFrame (signal Poisson-sampled) and Bkg_OnlyPer2usFrame (background only). Increment <release> (e.g. 25.06.1) to find the latest campaign; RECO is kept for every campaign, FULL not always.

Released under the MIT License.