2026-07-23 10:11:04 +02:00
2024-05-07 13:58:54 +02:00
2024-05-07 13:58:10 +02:00

miniCaloSim

A Geant4-based calorimeter simulator for teaching and research. Supports arbitrary sampling or homogeneous calorimeter geometries, runs batches of particle showers, and writes ROOT output for analysis.


Contents


Overview

miniCaloSim wraps the standard Geant4 B4a example into a configurable geometry driven by a GeometryDescriptor object. A geometry is a stack of layers; each layer is a slab of named NIST material with an optional sensor grid that accumulates deposited energy. The primary particle type and energy are configurable at run time.

The default executable run_pbwo4 simulates electron showers in a 20 cm PbWO₄ crystal with a 10×10 sensor grid and writes a ROOT file. A second executable, run_sampling, simulates the same kind of showers in a sampling calorimeter (alternating thin absorber and active layers); the layer configuration is selected by name at run time.


Repository layout

run_pbwo4.cc          homogeneous PbWO4 crystal executable entry point
run_sampling.cc       sampling calorimeter executable entry point (selectable layer configs)
src/                  Geant4 application sources
  ActionInitialization.cc
  DetectorConstruction.cc   reads GeometryDescriptor, builds Geant4 volumes
  EventAction.cc            fills the Hits ntuple at end of each event
  SteppingAction.cc         fills Steps and Spawning ntuples per step
  RunAction.cc              creates all three ROOT ntuples
  G4System.cc               owns the Geant4 run manager lifetime
  GeometryDescriptor.cc     geometry data model (layers + sensors)
  PrimaryGeneratorAction.cc
include/              corresponding headers
bind/                 Python bindings (pybind11 + pure-Python helpers)
  bindings.cpp        exposes Sensor, Layer, GeometryDescriptor, G4System
  G4Calo.py           high-level Python API (run_batch, display_event)
  G4Calo_exec.py      worker subprocess entry point
  minicalo_tools.py   TempFileManager for subprocess IPC via /dev/shm
  minipandas.py       MiniFrame: minimal NumPy-backed dataframe
lib/
  geant4/             Geant4 source submodule (fork with modifications)
  pybind11/           pybind11 submodule
superbuild/           CMake superbuild to compile Geant4 from source
docker/               Dockerfile-mini
docs/                 design documents

Build

Option A — system Geant4

Requires a Geant4 ≥ 11 installation visible to CMake (e.g. via Geant4_DIR or CMAKE_PREFIX_PATH), plus pybind11 (submodule) and Python 3 development headers.

cmake -B build -S .
cmake --build build --parallel $(nproc)
# executables: build/run_pbwo4, build/run_sampling

Option B — build Geant4 from submodule

The superbuild compiles Geant4 from lib/geant4 into build/geant4-install, then builds minicalosim against it. Four targets are available:

cmake -B build -S superbuild/              # configure once

cmake --build build --target geant4        # compile Geant4 (~3060 min)
cmake --build build --target run_pbwo4     # compile run_pbwo4
cmake --build build --target run_sampling  # compile run_sampling
cmake --build build                        # all

After the build, the executables are at build/run_pbwo4 and build/run_sampling (symlinks in the superbuild case).

Options:

CMake option Default Meaning
GEANT4_INSTALL_DATA ON Download Geant4 physics data tables (~2 GB)
GEANT4_USE_GDML ON Build Geant4 with GDML support (requires Xerces-C); set OFF to build without Xerces-C

The superbuild caps parallelism at min(nproc, 16) to avoid memory exhaustion.


Running

# default: 10 events, writes pbwo4_10events_hits.root
build/run_pbwo4

# custom number of events
build/run_pbwo4 500

Output file name: pbwo4_<nEvents>events_hits.root.

The geometry is hardcoded in run_pbwo4.cc: a single 20 cm PbWO₄ layer with a 10×10 sensor grid, bombarded with 1 GeV electrons.

Sampling calorimeter (run_sampling)

# default config (pb_scint), 10 events, writes sampling_pb_scint_10events_hits.root
build/run_sampling

# pick a config by name, default event count
build/run_sampling fe_scint

# pick a config by 1-based index, and a number of events
build/run_sampling 3 500

The config can be given either by name or by its 1-based index (run_sampling --help-style usage, including the index list, is printed on an invalid selection). Output file name: sampling_<configName>_<nEvents>events_hits.root (always uses the resolved name, even when selected by index).

All configs use a 10×10 sensor grid per active layer and are sized to reach ~2025 X₀ of absorber, comparable to run_pbwo4's 20 cm PbWO₄ block. Because a sampling stack dilutes the absorber with inactive gaps, the lower the absorber's Z the physically deeper the stack has to be to reach the same number of radiation lengths — fe_scint is twice as deep as pb_scint for this reason. Configs, defined in run_sampling.cc:

# Config name Layers Total depth ~X₀ Notes
1 pb_scint 60× (0.2 cm G4_Pb + 0.3 cm scint) 30 cm 21 classic fine-sampling EM calo
2 fe_scint 40× (1.0 cm G4_Fe + 0.5 cm scint) 60 cm 23 coarse sampling, Tile-cal-like
3 w_scint_ecal 1× 2.0 cm homogeneous scint, then 75× (0.1 cm G4_W + 0.2 cm scint) 24.5 cm 21 thin homogeneous ECAL-like preshower (~0.05 X₀, sees MIPs/shower-start only) followed by a compact W/scint HCAL-like sampling section
4 pb_lar 70× (0.2 cm G4_Pb + 0.4 cm G4_lAr) 42 cm 25 same absorber as pb_scint, liquid argon active medium

ROOT output format

Each run produces a ROOT file with three TTrees.

Hits tree

One row per simulated event. Contains integrated quantities over the whole calorimeter.

Column Type Description
true_energy double Primary particle energy [MeV]
total_dep_energy double Total energy deposited in all sensors [MeV]
N_layers int Number of layers
N_active_layers int Number of active (sensitive) layers
N_sensors int Total number of sensors
sensor_energy[N] double[] Deposited energy per sensor [MeV]
sensor_x/y/z[N] double[] Sensor centre position [mm]
sensor_dx/dy/dz[N] double[] Sensor half-size [mm]
sensor_layer[N] int[] Layer index of each sensor
sensor_copy_number[N] int[] Copy number within its layer

Steps tree

One row per Geant4 step. Every step of every track of every event is recorded.

Column Type Description
event_id int Event index within the run
track_id int Track ID (1 = primary, ≥2 = secondaries)
step_no int Step number within this track (starts at 1)
pdg int PDG encoding of the particle
pre_x/y/z double Pre-step position [mm]
pre_E double Kinetic energy at step start [MeV]
post_x/y/z double Post-step position [mm]
post_E double Kinetic energy at step end [MeV]
edep double Energy deposited in medium at this step [MeV]
step_length double Geometric step length [mm]
process string Physics process that ended this step
layer_id int Layer index at the pre-step point (-1 = outside all layers)
material string Material name at the pre-step point
pre_dx/dy/dz double Pre-step momentum unit direction
post_dx/dy/dz double Post-step momentum unit direction
Bx/By/Bz double Magnetic field at pre-step position [T]
Ex/Ey/Ez double Electric field at pre-step position [V/m]
child_track_ids int[] Track IDs of all secondaries spawned during this step (empty if none)

Track IDs are assigned sequentially per event starting from 1. The primary particle is always track 1. Within an event, track_id uniquely identifies a particle. See also the Spawning tree.

Spawning tree

One row per secondary particle birth. Enables direct shower-tree reconstruction without position joins.

Column Type Description
event_id int Event index
parent_track_id int Track ID of the parent particle
parent_step_no int Step number of the parent at which this secondary was created
child_track_id int Track ID of the newly born secondary

Example join: to find all secondaries born at step 5 of track 3 in event 0:

spawning[(spawning.event_id == 0) &
         (spawning.parent_track_id == 3) &
         (spawning.parent_step_no == 5)]

The same information is available in the Steps tree via the child_track_ids array column on the parent's row.


Calorimeter materials

A few material choices available via NIST names:

Scintillators (active layers)

NIST name Density Notes
G4_POLYSTYRENE 1.06 g/cm³ cheap plastic scintillator
G4_PLASTIC_SC_VINYLTOLUENE 1.032 g/cm³ plastic scintillator
G4_BGO 7.13 g/cm³ inorganic crystal, ~0.8 CHF/cm³
G4_LSO 7.4 g/cm³ inorganic crystal, ~6 CHF/cm³
G4_LYSO 7.1 g/cm³ inorganic crystal, ~30 CHF/cm³
G4_lAr 1.40 g/cm³ liquid argon, cryogenic (ATLAS EM calo-style)

Absorbers (passive layers)

NIST name Density X₀ Notes
G4_Pb 11.35 g/cm³ 0.56 cm standard EM calorimeter absorber
G4_Fe 7.87 g/cm³ hadronic calorimeter absorber
G4_W 19.3 g/cm³ 0.35 cm compact high-resolution EM
G4_Cu 8.96 g/cm³ 1.43 cm EM calorimeter absorber
G4_BRASS 8.53 g/cm³ 1.46 cm 70% Cu / 30% Zn

Homogeneous calorimeters

NIST name Density X₀ Notes
G4_CESIUM_IODIDE 4.51 g/cm³ ~1.9 cm inorganic crystal
G4_PbWO4 8.28 g/cm³ 0.89 cm lead tungstate (CMS ECAL)

Docker

docker/Dockerfile-mini builds a minimal Ubuntu 22.04 image with:

  • Geant4 compiled from the lib/geant4 submodule (batch mode, no vis, no multithreading)
  • The minicalo Python package installed system-wide
  • Python dependencies: numpy, pandas, uproot, awkward, plotly

Build (from the repo root, passing COMMIT to pin the source):

docker build \
  --build-arg COMMIT=$(git rev-parse HEAD) \
  --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
  -f docker/Dockerfile-mini \
  -t minicalosim:latest \
  --build-context minicalosim=. \
  .

Geant4 modifications

The lib/geant4 submodule is a fork of Geant4 with three small changes to enable pre-assigning track IDs to secondary particles at the moment they are created, so that UserSteppingAction can record which child track IDs a step spawned (the child_track_ids column and the Spawning tree).

Background

In unmodified Geant4, track IDs are assigned in G4EventManager::StackTracks(), which is called only after the parent track has finished all its steps. At the time UserSteppingAction fires, the secondaries visible via G4Step::GetSecondaryInCurrentStep() have not yet received their IDs.

Changes

All changes are on the minicalosim-spawning branch of git@gitlab.etp.kit.edu:lbogner/geant4.git.

source/tracking/include/G4SteppingManager.hh

Adds a G4int* fTrackIDCounter member and a SetTrackIDCounter(G4int*) setter. The pointer is set by G4EventManager (via G4TrackingManager) at the start of each event and points directly into G4EventManager::trackIDCounter.

source/tracking/include/G4TrackingManager.hh

Adds a SetTrackIDCounter(G4int*) pass-through that forwards the pointer into G4SteppingManager.

source/tracking/src/G4SteppingManager.cc

In ProcessSecondariesFromParticleChange(), after SetParentID and SetCreatorProcess, immediately assigns a track ID to each secondary by incrementing through the pointer:

if (fTrackIDCounter) {
    tempSecondaryTrack->SetTrackID(++(*fTrackIDCounter));
}

source/event/src/G4EventManager.cc

  • Calls trackManager->SetTrackIDCounter(&trackIDCounter) once before the event's tracking loop.
  • StackTracks() is updated to skip counter increment and ID assignment for tracks that already have a non-zero ID (i.e. were pre-assigned above):
if (IDhasAlreadySet) {
    ++trackIDCounter;          // advance for primaries (ID set externally)
} else if (newTrack->GetTrackID() == 0) {
    ++trackIDCounter;          // normal secondary: assign as before
    newTrack->SetTrackID(trackIDCounter);
    ...
}
// else: pre-assigned secondary — counter already incremented, skip

Why not include G4EventManager.hh in G4SteppingManager.cc?

G4event already depends on G4tracking (G4EventManager uses G4TrackingManager), so including G4EventManager.hh in the tracking module would create a circular dependency. The pointer approach avoids any new include: G4SteppingManager only needs a G4int*, which requires no additional header.

S
Description
No description provided
Readme 291 KiB
Languages
C++ 61.3%
Python 32.1%
CMake 3.5%
Dockerfile 1.3%
Jupyter Notebook 1%
Other 0.8%