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}) add_executable(run_pbwo4 run_pbwo4.cc ${sources} ${headers})
target_link_libraries(run_pbwo4 ${Geant4_LIBRARIES} ${Python3_LIBRARIES}) 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 # 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. 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 ## 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 src/ Geant4 application sources
ActionInitialization.cc ActionInitialization.cc
DetectorConstruction.cc reads GeometryDescriptor, builds Geant4 volumes 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 ```bash
cmake -B build -S . cmake -B build -S .
cmake --build build --parallel $(nproc) cmake --build build --parallel $(nproc)
# executable: build/run_pbwo4 # executables: build/run_pbwo4, build/run_sampling
``` ```
### Option B — build Geant4 from submodule ### 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 ```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 geant4 # compile Geant4 (~3060 min)
cmake --build build --target run_pbwo4 # compile minicalosim cmake --build build --target run_pbwo4 # compile run_pbwo4
cmake --build build # both 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:** **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. 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 ## 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_BGO` | 7.13 g/cm³ | inorganic crystal, ~0.8 CHF/cm³ |
| `G4_LSO` | 7.4 g/cm³ | inorganic crystal, ~6 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_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) ### Absorbers (passive layers)
+1
View File
@@ -38,6 +38,7 @@ COPY include/ include/
COPY bind/ bind/ COPY bind/ bind/
COPY lib/ lib/ COPY lib/ lib/
COPY run_pbwo4.cc . COPY run_pbwo4.cc .
COPY run_sampling.cc .
# lib/geant4/.git is a submodule pointer to the host's .git/modules dir, which # 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 # 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 ${CMAKE_BINARY_DIR}/run_pbwo4
DEPENDS geant4 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
)