diff --git a/CMakeLists.txt b/CMakeLists.txt index 381d34d..b6ce8ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 # diff --git a/README.md b/README.md index 8d2f147..0634da7 100644 --- a/README.md +++ b/README.md @@ -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 (~30–60 min) -cmake --build build --target run_pbwo4 # compile minicalosim -cmake --build build # both +cmake --build build --target geant4 # compile Geant4 (~30–60 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_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__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 ~20–25 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) diff --git a/docker/Dockerfile b/docker/Dockerfile index 756d9b4..f34f7c3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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 diff --git a/run_sampling.cc b/run_sampling.cc new file mode 100644 index 0000000..1f329c6 --- /dev/null +++ b/run_sampling.cc @@ -0,0 +1,124 @@ +#include "GeometryDescriptor.hh" +#include "G4System.hh" + +#include +#include +#include +#include +#include + +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> 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(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; +} diff --git a/superbuild/CMakeLists.txt b/superbuild/CMakeLists.txt index 20b8514..4d58e14 100644 --- a/superbuild/CMakeLists.txt +++ b/superbuild/CMakeLists.txt @@ -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 --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 +)