From 2f74ee0637bf57c72e1ddb7e26fb9d0b8dae8f43 Mon Sep 17 00:00:00 2001 From: Lars Bogner Date: Thu, 11 Jun 2026 11:15:11 +0200 Subject: [PATCH] rewrite README with full project and Geant4 modification docs Co-Authored-By: Claude Sonnet 4.6 --- README.md | 292 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 273 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a3e64bb..a887aad 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,285 @@ # 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. - * 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 +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. -## 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. - * 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 +## Repository layout - * 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 +``` +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.