rewrite README with full project and Geant4 modification docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 11:15:11 +02:00
parent 38a0273087
commit 2f74ee0637
+272 -18
View File
@@ -1,31 +1,285 @@
# miniCaloSim # 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.
---
## Getting started ## 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)
### Materials ---
Below are some useful material choices. Not all information is provided, but can be found on the internet ;) ## Overview
## Scintillator materials: 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.
* G4_POLYSTYRENE: a polymer of styrene with a density of 1.06 g/cm3 and a refractive index of 1.57. Cost: basically nothing. 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.
* G4_PLASTIC_SC_VINYLTOLUENE: a plastic scintillator based on vinyltoluene with a density of 1.032 g/cm3 and a refractive index of 1.58. Cost: basically nothing.
* G4_BGO: bismuth germanate, an inorganic crystal scintillator with a density of 7.13 g/cm3 and a refractive index of 2.15. Cost 0.8 CHF/cm3
* G4_LSO: lutetium oxyorthosilicate, an inorganic crystal scintillator with a density of 7.4 g/cm3 and a refractive index of 1.82. Cost: 6 CHF/cm3
* G4_LYSO: lutetium yttrium oxyorthosilicate, an inorganic crystal scintillator with a density of 7.1 g/cm3 and a refractive index of 1.81. 30CHF / cm3
## Absorber materials ---
* G4_Pb: lead, a high-density and high-Z material that is commonly used as an absorber for electromagnetic calorimeters. It has a density of 11.35 g/cm3 and a radiation length of 0.56 cm. Cost: 0.03 CHF/cm3. ## Repository layout
* G4_Fe: iron, a medium-density and medium-Z material that is often used as an absorber for hadronic calorimeters. It has a density of 7.87 g/cm3 and an interaction length of 16.77 cm. Cost: 0.005 CHF/cm3.
* G4_W: tungsten, a very high-density and very high-Z material that is suitable for compact and high-resolution electromagnetic calorimeters. It has a density of 19.3 g/cm3 and a radiation length of 0.35 cm. 0.6 CHF/cm3.
* G4_Cu: copper, a high-density and high-Z material that is also used as an absorber for electromagnetic calorimeters. It has a density of 8.96 g/cm3 and a radiation length of 1.43 cm. 0.08 CHF/cm3
* G4_BRASS: It has a density of 8.53 g/cm3 and a composition of 70% copper and 30% zinc by mass. It has a radiation length of 1.46 cm and an interaction length of 16.95 cm. Cost: 0.02 CHF/cm3
## For homogenous calorimeters ```
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
```
* G4_CESIUM_IODIDE: cesium iodide, an inorganic crystal scintillator with a density of 4.51 g/cm3 and a refractive index of 1.79. radiation length about 1.9 cm. Cost: about 2 CHF/cm3 ---
* G4_PbWO4: lead tungstate crystal (CMS ECAL). Lead tungstate has a very high density of 19.3 g/cm3 and a very short radiation length of 0.89 cm, which makes it suitable for compact and high-resolution electromagnetic calorimeters. Cost: about 3 CHF/cm3
## 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.