Files
minicalosim/README.md
T
2026-06-11 11:15:11 +02:00

286 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (~3060 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_<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.
---
## 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.