11d01d7015
Mirrors run_pbwo4 but alternates thin absorber and active layers, with four selectable configs (by name or 1-based index): pb_scint, fe_scint, w_scint_ecal (homogeneous ECAL-like preshower + W/scint sampling section), and pb_lar. Each is sized to reach ~20-25 X0 of absorber despite the dilution from inactive gaps. Wired into the top-level CMakeLists, superbuild, and Dockerfile alongside run_pbwo4. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
314 lines
13 KiB
Markdown
314 lines
13 KiB
Markdown
# 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. 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.
|
||
|
||
```bash
|
||
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:
|
||
|
||
```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 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) |
|
||
|
||
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_<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`)
|
||
|
||
```bash
|
||
# 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 ~20–25 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:
|
||
```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³ |
|
||
| `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):
|
||
```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.
|