# 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](#overview) - [Repository layout](#repository-layout) - [Build](#build) - [Option A — system Geant4](#option-a--system-geant4) - [Option B — build Geant4 from submodule](#option-b--build-geant4-from-submodule) - [Running](#running) - [ROOT output format](#root-output-format) - [Hits tree](#hits-tree) - [Steps tree](#steps-tree) - [Spawning tree](#spawning-tree) - [Calorimeter materials](#calorimeter-materials) - [Docker](#docker) - [Geant4 modifications](#geant4-modifications) --- ## 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. --- ## Repository layout ``` run_pbwo4.cc main executable entry point 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. ```bash cmake -B build -S . cmake --build build --parallel $(nproc) # executable: build/run_pbwo4 ``` ### Option B — build Geant4 from submodule The superbuild compiles Geant4 from `lib/geant4` into `build/geant4-install`, then builds minicalosim against it. Three targets are available: ```bash cmake -B build -S superbuild/ # configure once cmake --build build --target geant4 # compile Geant4 (~30–60 min) cmake --build build --target run_pbwo4 # compile minicalosim cmake --build build # both ``` After the build, the executable is at `build/run_pbwo4` (symlink in the superbuild case). **Options:** | CMake option | Default | Meaning | |---|---|---| | `GEANT4_INSTALL_DATA` | `ON` | Download Geant4 physics data tables (~2 GB) | The superbuild caps parallelism at `min(nproc, 16)` to avoid memory exhaustion. --- ## Running ```bash # default: 10 events, writes pbwo4_10events_hits.root build/run_pbwo4 # custom number of events build/run_pbwo4 500 ``` Output file name: `pbwo4_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. --- ## 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 | | `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: ```python 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³ | ### 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): ```bash 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: ```cpp 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): ```cpp 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.