Add run_sampling executable for sampling calorimeter geometries

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>
This commit is contained in:
2026-06-24 09:45:31 +02:00
parent 942130c667
commit 11d01d7015
5 changed files with 180 additions and 9 deletions
+3
View File
@@ -60,6 +60,9 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
add_executable(run_pbwo4 run_pbwo4.cc ${sources} ${headers})
target_link_libraries(run_pbwo4 ${Geant4_LIBRARIES} ${Python3_LIBRARIES})
add_executable(run_sampling run_sampling.cc ${sources} ${headers})
target_link_libraries(run_sampling ${Geant4_LIBRARIES} ${Python3_LIBRARIES})
#----------------------------------------------------------------------------
# Install the executable to 'bin' directory under CMAKE_INSTALL_PREFIX
#
+36 -9
View File
@@ -26,14 +26,15 @@ A Geant4-based calorimeter simulator for teaching and research. Supports arbitra
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.
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 main executable entry point
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
@@ -69,22 +70,23 @@ Requires a Geant4 ≥ 11 installation visible to CMake (e.g. via `Geant4_DIR` or
```bash
cmake -B build -S .
cmake --build build --parallel $(nproc)
# executable: build/run_pbwo4
# 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. Three targets are available:
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 -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
cmake --build build --target geant4 # compile Geant4 (~3060 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 executable is at `build/run_pbwo4` (symlink in the superbuild case).
After the build, the executables are at `build/run_pbwo4` and `build/run_sampling` (symlinks in the superbuild case).
**Options:**
@@ -110,6 +112,30 @@ 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 ~2025 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
@@ -195,6 +221,7 @@ A few material choices available via NIST names:
| `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)
+1
View File
@@ -38,6 +38,7 @@ COPY include/ include/
COPY bind/ bind/
COPY lib/ lib/
COPY run_pbwo4.cc .
COPY run_sampling.cc .
# lib/geant4/.git is a submodule pointer to the host's .git/modules dir, which
# is not copied into the image. Replace it with a fresh repo so Geant4's cmake
+124
View File
@@ -0,0 +1,124 @@
#include "GeometryDescriptor.hh"
#include "G4System.hh"
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace {
GeometryDescriptor buildPbScint() {
GeometryDescriptor gd;
for (int i = 0; i < 60; ++i) {
gd.addLayer(0.2, "G4_Pb", false);
gd.addLayer(0.3, "G4_PLASTIC_SC_VINYLTOLUENE", true, 10, 10);
}
return gd;
}
GeometryDescriptor buildFeScint() {
GeometryDescriptor gd;
for (int i = 0; i < 40; ++i) {
gd.addLayer(1.0, "G4_Fe", false);
gd.addLayer(0.5, "G4_PLASTIC_SC_VINYLTOLUENE", true, 10, 10);
}
return gd;
}
GeometryDescriptor buildWScintEcal() {
GeometryDescriptor gd;
// Thin homogeneous preshower: ~0.05 X0, sees MIPs/shower-start, not containment.
gd.addLayer(2.0, "G4_PLASTIC_SC_VINYLTOLUENE", true, 10, 10);
for (int i = 0; i < 75; ++i) {
gd.addLayer(0.1, "G4_W", false);
gd.addLayer(0.2, "G4_PLASTIC_SC_VINYLTOLUENE", true, 10, 10);
}
return gd;
}
GeometryDescriptor buildPbLAr() {
GeometryDescriptor gd;
for (int i = 0; i < 70; ++i) {
gd.addLayer(0.2, "G4_Pb", false);
gd.addLayer(0.4, "G4_lAr", true, 10, 10);
}
return gd;
}
// Ordered (not alphabetical) so configs can also be selected by 1-based index.
const std::vector<std::pair<std::string, GeometryDescriptor (*)()>> kConfigs = {
{"pb_scint", buildPbScint},
{"fe_scint", buildFeScint},
{"w_scint_ecal", buildWScintEcal},
{"pb_lar", buildPbLAr},
};
void printUsage() {
std::cerr << "Usage: run_sampling [configName|configIndex] [nEvents]" << std::endl;
std::cerr << "Available configs:" << std::endl;
for (std::size_t i = 0; i < kConfigs.size(); ++i) {
std::cerr << " " << (i + 1) << ": " << kConfigs[i].first << std::endl;
}
}
} // namespace
int main(int argc, char** argv) {
std::string configName = kConfigs.front().first;
int nEvents = 10;
if (argc > 3) {
printUsage();
return 1;
}
if (argc >= 2) {
configName = argv[1];
}
if (argc == 3) {
try {
nEvents = std::stoi(argv[2]);
if (nEvents <= 0) throw std::invalid_argument("must be positive");
} catch (const std::exception& e) {
std::cerr << "Invalid nEvents '" << argv[2] << "': " << e.what() << std::endl;
return 1;
}
}
GeometryDescriptor (*builder)() = nullptr;
try {
std::size_t pos = 0;
int index = std::stoi(configName, &pos);
if (pos == configName.size() && index >= 1 && index <= static_cast<int>(kConfigs.size())) {
configName = kConfigs[index - 1].first;
builder = kConfigs[index - 1].second;
}
} catch (const std::exception&) {
// Not a valid index; fall through to name lookup below.
}
if (builder == nullptr) {
for (const auto& kv : kConfigs) {
if (kv.first == configName) {
builder = kv.second;
break;
}
}
}
if (builder == nullptr) {
std::cerr << "Unknown config '" << configName << "'." << std::endl;
printUsage();
return 1;
}
GeometryDescriptor gd = builder();
std::string outfile = "sampling_" + configName + "_" + std::to_string(nEvents) + "events_hits.root";
G4System g4;
g4.init(gd, -1);
g4.run_batch(nEvents, {"e-"}, 1.0, 1.0, outfile);
std::cout << "Saved " << nEvents << " events to " << outfile << std::endl;
return 0;
}
+16
View File
@@ -48,3 +48,19 @@ ExternalProject_Add(run_pbwo4
${CMAKE_BINARY_DIR}/run_pbwo4
DEPENDS geant4
)
# ── minicalosim (run_sampling) ─────────────────────────────────────────────
# Shares run_pbwo4's BINARY_DIR (already configured against G4_INSTALL) and
# depends on it so the two ExternalProjects don't reconfigure that dir concurrently.
ExternalProject_Add(run_sampling
SOURCE_DIR ${MINI_SRC}
BINARY_DIR ${CMAKE_BINARY_DIR}/minicalosim-build
CMAKE_ARGS
-DCMAKE_PREFIX_PATH=${G4_INSTALL}
-DWITH_GEANT4_UIVIS=OFF
BUILD_COMMAND cmake --build <BINARY_DIR> --target run_sampling --parallel ${NPROC}
INSTALL_COMMAND ${CMAKE_COMMAND} -E create_symlink
${CMAKE_BINARY_DIR}/minicalosim-build/run_sampling
${CMAKE_BINARY_DIR}/run_sampling
DEPENDS run_pbwo4
)