From 41278a4e1b432eaf271e8f7771cce382deb4ef0b Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sat, 19 Sep 2026 03:21:39 +0200 Subject: [PATCH 1/9] Give the absorber mother volume a material of its own This adds a dedicated air material and medium for AFaM so that the front absorber can be addressed as a single region. - AFaM shared ABSO_AIR_C0 with AFaAcc, which is one of its own daughters. - Geant4-VMC selects fast-simulation regions by material name and puts every volume of that material into the region, so a volume can only be a region of its own if its material is its own. - ABSO_AIR_ENVELOPE0$ has the composition and density of ABSO_AIR0$ and takes the same global cuts and processes, so the physics is unchanged. - No other code refers to ABSO medium index 20. --- Detectors/Passive/src/Absorber.cxx | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Detectors/Passive/src/Absorber.cxx b/Detectors/Passive/src/Absorber.cxx index 97b091f5965a6..4798734cbf043 100644 --- a/Detectors/Passive/src/Absorber.cxx +++ b/Detectors/Passive/src/Absorber.cxx @@ -171,6 +171,21 @@ void Absorber::createMaterials() matmgr.Medium("ABSO", 35, "AIR_C1", 35, 0, isxfld, sxmgmx, tmaxfd, stemax, deemax, epsil, stmin); matmgr.Medium("ABSO", 55, "AIR_C2", 55, 0, isxfld, sxmgmx, tmaxfd, stemax, deemax, epsil, stmin); + // + // Air of the absorber envelope + // + // Chemically identical to AIR0$ above. It exists so that the absorber + // mother volume AFaM has a material no other volume shares: Geant4-VMC + // selects fast-simulation regions by MATERIAL name and adds every volume of + // that material to the region, so a volume can only be addressed as a + // region of its own if its material is its own. Naming ABSO_AIR_ENVELOPE + // therefore means exactly "the front absorber", with all of its daughters + // inside it. Cuts and processes are the global defaults, the same ones + // ABSO_AIR_C0 gets, so the physics is unchanged. + matmgr.Mixture("ABSO", 20, "AIR_ENVELOPE0$", aAir, zAir, dAir, 4, wAir); + matmgr.Medium("ABSO", 20, "AIR_ENVELOPE", 20, 0, isxfld, sxmgmx, tmaxfd, stemax, deemax, epsil, + stmin); + // // Vacuum matmgr.Mixture("ABSO", 16, "VACUUM0$", aAir, zAir, dAir1, 4, wAir); @@ -249,6 +264,8 @@ void Absorber::ConstructGeometry() auto kMedSteelSh = matmgr.getTGeoMedium("ABSO_ST_C3"); // auto kMedAir = matmgr.getTGeoMedium("ABSO_AIR_C0"); + // Air again, under a name only AFaM uses -- see the comment at its definition. + auto kMedAirEnvelope = matmgr.getTGeoMedium("ABSO_AIR_ENVELOPE"); // auto kMedPb = matmgr.getTGeoMedium("ABSO_PB_C0"); auto kMedPbSh = matmgr.getTGeoMedium("ABSO_PB_C2"); @@ -867,7 +884,9 @@ void Absorber::ConstructGeometry() shFaM->DefineSection(14, z, rInFaCH2Cone2 - dz * angle10, rOuSteelEnvelopeR2); z += dzSteelEnvelopeR / 2.; shFaM->DefineSection(15, z, rInFaCH2Cone2, rOuSteelEnvelopeR2); - TGeoVolume* voFaM = new TGeoVolume("AFaM", shFaM, kMedAir); + // AFaM is the mother of the whole absorber, and its dedicated medium is what + // makes "the absorber" addressable as one fast-simulation region. + TGeoVolume* voFaM = new TGeoVolume("AFaM", shFaM, kMedAirEnvelope); voFaM->SetVisibility(0); // From 78f7d7f9698c674add3c1f59ab1a724a8693479d Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sat, 19 Sep 2026 03:21:56 +0200 Subject: [PATCH 2/9] Add a fast simulation hook for Geant4 and a toy absorber model This commit provides the fast-simulation hook that O2 was missing, in a new Detectors/FastSim module, plus one toy model that exercises it end to end. The feature does nothing unless G4.fastSimModels names a model. - o2::fastsim::G4RunConfiguration overrides CreateUserFastSimulation, which is the only piece geant4_vmc needed and O2 did not supply. - TG4FastSimulationPhysics is already registered unconditionally by TG4SpecialPhysicsList, so the specialProcess string is unchanged. - FastSimModel::DoIt is shared plumbing: it measures the distance to the envelope surface, kills the incident particle, stacks what comes back and books the energy difference as a deposit. - A model implements sample(), which maps the particle that entered to the particles that leave. - ToyAbsorberFastSim returns one particle carrying on in the incident direction with an exponentially attenuated energy. - Regions are named as tracking media in G4.fastSimRegions. - With G4.fastSimModels empty the run configuration returns nullptr and the behaviour is identical to before. Usage: o2-sim -n 10 -g pythia8pp -e TGeant4 -m PIPE ABSO \ --configKeyValues "G4.fastSimModels=toyAbsorber;G4.fastSimRegions=ABSO_AIR_ENVELOPE" The steps inside the region disappear from the step log, which is the saving: a fast step defaults to AvoidHitInvocation, so Geant4 does not call the sensitive detector and TVirtualMCApplication::Stepping() is not invoked. That is correct for a passive envelope, which has no hits to lose. --- Common/SimConfig/include/SimConfig/G4Params.h | 6 + Detectors/CMakeLists.txt | 1 + Detectors/FastSim/CMakeLists.txt | 17 +++ .../FastSim/include/FastSim/FastSimModel.h | 82 +++++++++++++ .../include/FastSim/G4FastSimulation.h | 63 ++++++++++ .../include/FastSim/ToyAbsorberFastSim.h | 34 ++++++ Detectors/FastSim/src/FastSimModel.cxx | 115 ++++++++++++++++++ Detectors/FastSim/src/G4FastSimulation.cxx | 82 +++++++++++++ Detectors/FastSim/src/ToyAbsorberFastSim.cxx | 48 ++++++++ Detectors/gconfig/CMakeLists.txt | 2 +- Detectors/gconfig/g4Config.C | 9 +- 11 files changed, 456 insertions(+), 3 deletions(-) create mode 100644 Detectors/FastSim/CMakeLists.txt create mode 100644 Detectors/FastSim/include/FastSim/FastSimModel.h create mode 100644 Detectors/FastSim/include/FastSim/G4FastSimulation.h create mode 100644 Detectors/FastSim/include/FastSim/ToyAbsorberFastSim.h create mode 100644 Detectors/FastSim/src/FastSimModel.cxx create mode 100644 Detectors/FastSim/src/G4FastSimulation.cxx create mode 100644 Detectors/FastSim/src/ToyAbsorberFastSim.cxx diff --git a/Common/SimConfig/include/SimConfig/G4Params.h b/Common/SimConfig/include/SimConfig/G4Params.h index 2a333a39e4242..2c44f97ab1c61 100644 --- a/Common/SimConfig/include/SimConfig/G4Params.h +++ b/Common/SimConfig/include/SimConfig/G4Params.h @@ -54,6 +54,12 @@ struct G4Params : public o2::conf::ConfigurableParamHelper { bool g4scoring = false; bool g4fluenceweight = false; + + // Fast simulation. Empty fastSimModels (the default) disables the feature + // entirely; see Detectors/gconfig/include/SimSetup/G4FastSimulation.h. + std::string fastSimModels = ""; // comma-separated model names to activate + std::string fastSimRegions = ""; // tracking media the models apply to (wildcards allowed) + float fastSimMinEnergy = 1.f; // GeV; below this the detailed transport runs O2ParamDef(G4Params, "G4"); }; diff --git a/Detectors/CMakeLists.txt b/Detectors/CMakeLists.txt index 9143761508998..09e784b0338ee 100644 --- a/Detectors/CMakeLists.txt +++ b/Detectors/CMakeLists.txt @@ -53,6 +53,7 @@ add_subdirectory(ForwardAlign) if(BUILD_SIMULATION) + add_subdirectory(FastSim) add_subdirectory(gconfig) o2_data_file(COPY gconfig DESTINATION Detectors) endif() diff --git a/Detectors/FastSim/CMakeLists.txt b/Detectors/FastSim/CMakeLists.txt new file mode 100644 index 0000000000000..b721288809f0a --- /dev/null +++ b/Detectors/FastSim/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. + +o2_add_library(FastSim + SOURCES src/FastSimModel.cxx + src/ToyAbsorberFastSim.cxx + src/G4FastSimulation.cxx + PUBLIC_LINK_LIBRARIES MC::Geant4VMC MC::Geant4 O2::SimConfig +) diff --git a/Detectors/FastSim/include/FastSim/FastSimModel.h b/Detectors/FastSim/include/FastSim/FastSimModel.h new file mode 100644 index 0000000000000..86d7718ac772c --- /dev/null +++ b/Detectors/FastSim/include/FastSim/FastSimModel.h @@ -0,0 +1,82 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef O2_FASTSIM_MODEL_H_ +#define O2_FASTSIM_MODEL_H_ + +/// Base class for fast simulation models. +/// +/// A fast simulation model replaces the detailed transport through a region of +/// the geometry by a function from the particle that enters it to the particles +/// that leave it. `DoIt()` below is the plumbing and is the same for every +/// model; a model implements `sample()` and nothing else. + +#include "G4VFastSimulationModel.hh" + +#include + +class G4FastStep; +class G4FastTrack; +class G4ParticleDefinition; + +namespace o2::fastsim +{ + +/// The particle entering the envelope, plus the geometric context a model needs. +/// Units are the O2/VMC ones: cm, GeV, ns. +struct FastSimInput { + int pdg = 0; + double position[3] = {}; ///< global, on the envelope surface + double direction[3] = {}; ///< unit vector + double kineticEnergy = 0.; ///< GeV + double mass = 0.; ///< GeV + double time = 0.; ///< ns + double exitDistance = 0.; ///< cm from `position` to the envelope surface along `direction` +}; + +/// One particle leaving the envelope. +struct FastSimOutput { + int pdg = 0; + double position[3] = {}; ///< global; put it outside the envelope surface + double momentum[3] = {}; ///< GeV/c + double time = 0.; ///< ns +}; + +/// A secondary created exactly on the envelope surface is located by the +/// navigator in whichever daughter owns that point, which costs two extra +/// zero-length steps before it gets out. Models should emit just beyond it. +constexpr double kSurfaceEpsilonCm = 1e-5; + +class FastSimModel : public G4VFastSimulationModel +{ + public: + FastSimModel(const G4String& name, double minEnergyGeV); + + G4bool IsApplicable(const G4ParticleDefinition& particle) override; + G4bool ModelTrigger(const G4FastTrack& fastTrack) override; + + /// Measures the distance to the envelope surface, asks `sample()` what comes + /// out, kills the incident particle, stacks the result and books the energy + /// difference as a deposit. + void DoIt(const G4FastTrack& fastTrack, G4FastStep& fastStep) final; + + protected: + /// Given the particle that entered, return everything that leaves. This is + /// the function a trained model implements. + virtual std::vector sample(const FastSimInput& input) const = 0; + + private: + double mMinEnergy = 0.; ///< internal Geant4 units; below this the detailed transport runs +}; + +} // namespace o2::fastsim + +#endif // O2_FASTSIM_MODEL_H_ diff --git a/Detectors/FastSim/include/FastSim/G4FastSimulation.h b/Detectors/FastSim/include/FastSim/G4FastSimulation.h new file mode 100644 index 0000000000000..45d0e0a95cdb2 --- /dev/null +++ b/Detectors/FastSim/include/FastSim/G4FastSimulation.h @@ -0,0 +1,63 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef O2_FASTSIM_G4_FAST_SIMULATION_H_ +#define O2_FASTSIM_G4_FAST_SIMULATION_H_ + +/// Wiring of the fast simulation models into the Geant4 engine. +/// +/// The feature is OFF unless `G4.fastSimModels` names a model. +/// +/// o2-sim -n 10 -g pythia8pp -e TGeant4 -m PIPE ABSO +/// --configKeyValues "G4.fastSimModels=toyAbsorber; +/// G4.fastSimRegions=ABSO_AIR_ENVELOPE" +/// +/// Regions are selected by TRACKING MEDIUM name (wildcards allowed), which +/// Geant4-VMC maps to the medium's MATERIAL; every volume of that material joins +/// the region. That is why the absorber mother volume AFaM carries a material of +/// its own (see Detectors/Passive/src/Absorber.cxx). Selection by volume is not +/// available: the VMC special cuts already root every logical volume in a +/// per-material region, and Geant4 allows a volume in only one region. + +#include "TG4RunConfiguration.h" +#include "TG4VUserFastSimulation.h" + +#include +#include + +namespace o2::fastsim +{ + +/// Creates and registers the models named in `G4.fastSimModels`. +class G4FastSimulation : public TG4VUserFastSimulation +{ + public: + G4FastSimulation(std::vector models, const std::string& regions, + double minEnergyGeV); + void Construct() override; + + private: + std::vector mModels; + double mMinEnergy = 1.; +}; + +/// The one hook O2 was missing. Returns nullptr when no model is configured, +/// which is exactly the behaviour before this file existed. +class G4RunConfiguration : public TG4RunConfiguration +{ + public: + using TG4RunConfiguration::TG4RunConfiguration; + TG4VUserFastSimulation* CreateUserFastSimulation() override; +}; + +} // namespace o2::fastsim + +#endif // O2_FASTSIM_G4_FAST_SIMULATION_H_ diff --git a/Detectors/FastSim/include/FastSim/ToyAbsorberFastSim.h b/Detectors/FastSim/include/FastSim/ToyAbsorberFastSim.h new file mode 100644 index 0000000000000..88990288bd0d2 --- /dev/null +++ b/Detectors/FastSim/include/FastSim/ToyAbsorberFastSim.h @@ -0,0 +1,34 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef O2_FASTSIM_TOY_ABSORBER_H_ +#define O2_FASTSIM_TOY_ABSORBER_H_ + +#include "FastSim/FastSimModel.h" + +namespace o2::fastsim +{ + +/// A toy model: one particle out, continuing along the incident direction with +/// the energy exponentially attenuated over the path through the envelope. +/// It exists to exercise the machinery, not to describe an absorber. +class ToyAbsorberFastSim : public FastSimModel +{ + public: + using FastSimModel::FastSimModel; + + protected: + std::vector sample(const FastSimInput& input) const override; +}; + +} // namespace o2::fastsim + +#endif // O2_FASTSIM_TOY_ABSORBER_H_ diff --git a/Detectors/FastSim/src/FastSimModel.cxx b/Detectors/FastSim/src/FastSimModel.cxx new file mode 100644 index 0000000000000..828d5b1427fe9 --- /dev/null +++ b/Detectors/FastSim/src/FastSimModel.cxx @@ -0,0 +1,115 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "FastSim/FastSimModel.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace o2::fastsim +{ + +//_____________________________________________________________________________ +FastSimModel::FastSimModel(const G4String& name, double minEnergyGeV) + : G4VFastSimulationModel(name), mMinEnergy(minEnergyGeV * CLHEP::GeV) +{ +} + +//_____________________________________________________________________________ +G4bool FastSimModel::IsApplicable(const G4ParticleDefinition&) +{ + // Which particles a model sees is decided by the `setParticles` selection, + // not here. + return true; +} + +//_____________________________________________________________________________ +G4bool FastSimModel::ModelTrigger(const G4FastTrack& fastTrack) +{ + // Below the threshold the detailed transport is cheap and a surrogate would + // be extrapolating. + return fastTrack.GetPrimaryTrack()->GetKineticEnergy() > mMinEnergy; +} + +//_____________________________________________________________________________ +void FastSimModel::DoIt(const G4FastTrack& fastTrack, G4FastStep& fastStep) +{ + const G4Track* track = fastTrack.GetPrimaryTrack(); + const G4ThreeVector& position = track->GetPosition(); + const G4ThreeVector& direction = track->GetMomentumDirection(); + + FastSimInput input; + input.pdg = track->GetDefinition()->GetPDGEncoding(); + input.position[0] = position.x() / CLHEP::cm; + input.position[1] = position.y() / CLHEP::cm; + input.position[2] = position.z() / CLHEP::cm; + input.direction[0] = direction.x(); + input.direction[1] = direction.y(); + input.direction[2] = direction.z(); + input.kineticEnergy = track->GetKineticEnergy() / CLHEP::GeV; + input.mass = track->GetDefinition()->GetPDGMass() / CLHEP::GeV; + input.time = track->GetGlobalTime() / CLHEP::ns; + input.exitDistance = fastTrack.GetEnvelopeSolid()->DistanceToOut( + fastTrack.GetPrimaryTrackLocalPosition(), + fastTrack.GetPrimaryTrackLocalDirection()) / + CLHEP::cm; + + const std::vector outgoing = sample(input); + + fastStep.KillPrimaryTrack(); + fastStep.ProposePrimaryTrackPathLength(input.exitDistance * CLHEP::cm); + + // NOTE: a fast step defaults to AvoidHitInvocation, so Geant4 does not call + // the sensitive detector and TVirtualMCApplication::Stepping() is not invoked + // for it. For a passive envelope that is what we want -- there are no hits to + // lose, and the steps disappearing from the step log is the saving. A model + // covering a region that scores would add + // fastStep.ProposeSteppingControl(NormalCondition); + // here. + + double outgoingKineticEnergy = 0.; + fastStep.SetNumberOfSecondaryTracks(outgoing.size()); + for (const auto& out : outgoing) { + const G4ParticleDefinition* definition = + G4ParticleTable::GetParticleTable()->FindParticle(out.pdg); + if (definition == nullptr) { + LOG(error) << "fast simulation: model " << GetName() << " returned unknown pdg " << out.pdg + << "; particle dropped"; + continue; + } + const G4ThreeVector momentum(out.momentum[0] * CLHEP::GeV, out.momentum[1] * CLHEP::GeV, + out.momentum[2] * CLHEP::GeV); + G4DynamicParticle particle(definition, momentum); + outgoingKineticEnergy += particle.GetKineticEnergy(); + fastStep.CreateSecondaryTrack(particle, + G4ThreeVector(out.position[0] * CLHEP::cm, + out.position[1] * CLHEP::cm, + out.position[2] * CLHEP::cm), + out.time * CLHEP::ns, /*localCoordinates=*/false); + } + + // Whatever did not come out stayed in. + fastStep.ProposeTotalEnergyDeposited( + std::max(0., track->GetKineticEnergy() - outgoingKineticEnergy)); +} + +} // namespace o2::fastsim diff --git a/Detectors/FastSim/src/G4FastSimulation.cxx b/Detectors/FastSim/src/G4FastSimulation.cxx new file mode 100644 index 0000000000000..89127218ecc5e --- /dev/null +++ b/Detectors/FastSim/src/G4FastSimulation.cxx @@ -0,0 +1,82 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "FastSim/G4FastSimulation.h" +#include "FastSim/ToyAbsorberFastSim.h" +#include "SimConfig/G4Params.h" + +#include + +#include + +namespace o2::fastsim +{ + +namespace +{ +std::vector split(const std::string& value, char sep) +{ + std::vector out; + std::stringstream stream(value); + std::string token; + while (std::getline(stream, token, sep)) { + if (!token.empty()) { + out.push_back(token); + } + } + return out; +} +} // namespace + +//_____________________________________________________________________________ +G4FastSimulation::G4FastSimulation(std::vector models, const std::string& regions, + double minEnergyGeV) + : TG4VUserFastSimulation(), mModels(std::move(models)), mMinEnergy(minEnergyGeV) +{ + for (const auto& model : mModels) { + SetModel(model); + SetModelParticles(model, "all"); + if (!regions.empty()) { + SetModelRegions(model, regions); + } else { + LOG(warn) << "fast simulation: model " << model + << " has no G4.fastSimRegions; it will not be applied anywhere"; + } + } +} + +//_____________________________________________________________________________ +void G4FastSimulation::Construct() +{ + for (const auto& model : mModels) { + if (model == "toyAbsorber") { + LOG(info) << "fast simulation: registering model " << model << " above " << mMinEnergy + << " GeV"; + Register(new ToyAbsorberFastSim(model, mMinEnergy)); + } else { + LOG(error) << "fast simulation: unknown model " << model << "; ignored"; + } + } +} + +//_____________________________________________________________________________ +TG4VUserFastSimulation* G4RunConfiguration::CreateUserFastSimulation() +{ + const auto& params = o2::conf::G4Params::Instance(); + auto models = split(params.fastSimModels, ','); + if (models.empty()) { + return nullptr; // the default: no fast simulation, unchanged behaviour + } + LOG(info) << "fast simulation is ENABLED for regions '" << params.fastSimRegions << "'"; + return new G4FastSimulation(std::move(models), params.fastSimRegions, params.fastSimMinEnergy); +} + +} // namespace o2::fastsim diff --git a/Detectors/FastSim/src/ToyAbsorberFastSim.cxx b/Detectors/FastSim/src/ToyAbsorberFastSim.cxx new file mode 100644 index 0000000000000..bdb5b09cea59b --- /dev/null +++ b/Detectors/FastSim/src/ToyAbsorberFastSim.cxx @@ -0,0 +1,48 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "FastSim/ToyAbsorberFastSim.h" + +#include + +namespace o2::fastsim +{ + +namespace +{ +/// Attenuation length of the toy transformation. +constexpr double kAbsorptionLengthCm = 60.; +} // namespace + +//_____________________________________________________________________________ +std::vector ToyAbsorberFastSim::sample(const FastSimInput& input) const +{ + // A toy transformation, not a physics model: the incident particle carries on + // in its direction with the energy attenuated over the path through the + // envelope. A trained model returns a shower here instead. + const double kinetic = input.kineticEnergy * std::exp(-input.exitDistance / kAbsorptionLengthCm); + if (kinetic <= 0.) { + return {}; + } + const double momentum = std::sqrt(kinetic * (kinetic + 2. * input.mass)); + + FastSimOutput out; + out.pdg = input.pdg; + out.time = input.time; + for (int i = 0; i < 3; ++i) { + out.position[i] = + input.position[i] + (input.exitDistance + kSurfaceEpsilonCm) * input.direction[i]; + out.momentum[i] = momentum * input.direction[i]; + } + return {out}; +} + +} // namespace o2::fastsim diff --git a/Detectors/gconfig/CMakeLists.txt b/Detectors/gconfig/CMakeLists.txt index be50c9bcfe72f..282fa4c9d124e 100644 --- a/Detectors/gconfig/CMakeLists.txt +++ b/Detectors/gconfig/CMakeLists.txt @@ -16,7 +16,7 @@ o2_add_library(G3Setup o2_add_library(G4Setup SOURCES src/G4Config.cxx - PUBLIC_LINK_LIBRARIES MC::Geant4VMC MC::Geant4 FairRoot::Base O2::SimulationDataFormat O2::Generators O2::SimSetup + PUBLIC_LINK_LIBRARIES MC::Geant4VMC MC::Geant4 FairRoot::Base O2::SimulationDataFormat O2::Generators O2::SimSetup O2::FastSim ) o2_add_library(FLUKASetup diff --git a/Detectors/gconfig/g4Config.C b/Detectors/gconfig/g4Config.C index 16374cf9fd4a3..77494c559ca51 100644 --- a/Detectors/gconfig/g4Config.C +++ b/Detectors/gconfig/g4Config.C @@ -61,6 +61,7 @@ R__LOAD_LIBRARY(libgeant4vmc) #include "TG4RunConfiguration.h" #include "SimConfig/G4Params.h" #include "SimConfig/FluenceWeightCalculator.h" +#include "FastSim/G4FastSimulation.h" #endif #include "commonConfig.C" @@ -114,8 +115,12 @@ void Config() LOG(fatal) << "Unsupported geometry navigation mode"; } - auto runConfiguration = new TG4RunConfiguration(geomNavStr, physicsSetup, "stepLimiter+specialCuts", - specialStacking, mtMode); + // o2::fastsim::G4RunConfiguration differs from TG4RunConfiguration only in + // providing the fast-simulation hook; with G4.fastSimModels empty it behaves + // identically. + auto runConfiguration = new o2::fastsim::G4RunConfiguration(geomNavStr, physicsSetup, + "stepLimiter+specialCuts", + specialStacking, mtMode); if (g4Params.g4scoring) { runConfiguration->SetUseOfG4Scoring(); if (g4Params.g4fluenceweight) { From aef62c38536da526a475772235674e8457caaffe Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sat, 19 Sep 2026 03:21:56 +0200 Subject: [PATCH 3/9] Add a simulation example for the absorber fast simulation This adds run/SimExamples/FastSim_Absorber, which runs the same five events through PIPE and ABSO twice, once with detailed transport and once with the toy model, and compares the number of tracks. - run.sh performs both simulations and the comparison. - countTracks.macro reports tracks per event of an o2-sim output. - README.md says how the feature is switched on, what the toy model does and why the region is named after a tracking medium. --- run/SimExamples/FastSim_Absorber/README.md | 57 ++++++++++++++++++ .../FastSim_Absorber/countTracks.macro | 20 +++++++ run/SimExamples/FastSim_Absorber/run.sh | 59 +++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 run/SimExamples/FastSim_Absorber/README.md create mode 100644 run/SimExamples/FastSim_Absorber/countTracks.macro create mode 100755 run/SimExamples/FastSim_Absorber/run.sh diff --git a/run/SimExamples/FastSim_Absorber/README.md b/run/SimExamples/FastSim_Absorber/README.md new file mode 100644 index 0000000000000..d38886977970e --- /dev/null +++ b/run/SimExamples/FastSim_Absorber/README.md @@ -0,0 +1,57 @@ +# Fast simulation of the front absorber + +Replaces the detailed transport through the ALICE front absorber by a model, and +compares the result against a full simulation of the same events. + +`run.sh` runs both: a reference `o2-sim` with PIPE and ABSO only, and the same +setup with the fast simulation switched on. + +| file | | +|---|---| +| `run.sh` | the two simulations and the comparison | +| `countTracks.macro` | tracks per event of an `o2-sim` output | + +## Switching it on + +The feature does nothing unless a model is named: + +``` +--configKeyValues "G4.fastSimModels=toyAbsorber;G4.fastSimRegions=ABSO_AIR_ENVELOPE" +``` + +`G4.fastSimMinEnergy` (GeV, default 1) is the threshold below which the detailed +transport still runs, because a surrogate below it would be extrapolating and +the transport there is cheap anyway. + +## What the model does + +`toyAbsorber` is a placeholder. It returns the incident particle continuing in +its direction with the energy attenuated exponentially over the path through the +envelope — a real absorber turns one incident hadron into a shower, so the +numbers this produces are not physics. It is here so that the machinery can be +exercised end to end before a trained model exists. + +A model implements one function, `sample()`, which maps the particle that +entered the region to the particles that leave it +(`Detectors/FastSim/include/FastSim/FastSimModel.h`). Everything around it — +measuring the distance to the envelope surface, killing the incident particle, +stacking what comes back, booking the energy difference as a deposit — is shared +and does not have to be reimplemented. + +## Why the region is named `ABSO_AIR_ENVELOPE` + +Regions are selected by tracking medium, which Geant4-VMC maps to that medium's +material, adding every volume of that material to the region. A volume can +therefore only be addressed on its own if its material is its own, which is why +`AFaM` — the mother volume of the whole absorber — carries a dedicated air +material (`Detectors/Passive/src/Absorber.cxx`). Selecting it means "the +absorber", with all of its daughters inside. + +Selection by volume is not available: the VMC special cuts already root every +logical volume in a per-material region, and Geant4 allows a logical volume in +exactly one region. + +## What is not measured here + +CPU. The step and track counts show that the transport is being replaced; a +timing number needs a realistic workload rather than five events of PIPE+ABSO. diff --git a/run/SimExamples/FastSim_Absorber/countTracks.macro b/run/SimExamples/FastSim_Absorber/countTracks.macro new file mode 100644 index 0000000000000..0c5e10106aec4 --- /dev/null +++ b/run/SimExamples/FastSim_Absorber/countTracks.macro @@ -0,0 +1,20 @@ +// Tracks per event of an o2-sim output, as a cheap proxy for how much transport +// happened. Usage: root -l -b -q 'countTracks.macro("o2sim")' +void countTracks(const char* prefix = "o2sim") +{ + TFile file(Form("%s_Kine.root", prefix)); + auto* tree = (TTree*)file.Get("o2sim"); + if (!tree) { + printf("no o2sim tree in %s_Kine.root\n", prefix); + return; + } + std::vector* tracks = nullptr; + tree->SetBranchAddress("MCTrack", &tracks); + Long64_t total = 0; + for (Long64_t entry = 0; entry < tree->GetEntries(); ++entry) { + tree->GetEntry(entry); + total += tracks->size(); + } + printf("%s: %lld events, %lld tracks, %.1f per event\n", prefix, tree->GetEntries(), total, + tree->GetEntries() ? double(total) / tree->GetEntries() : 0.); +} diff --git a/run/SimExamples/FastSim_Absorber/run.sh b/run/SimExamples/FastSim_Absorber/run.sh new file mode 100755 index 0000000000000..ce5e354859f12 --- /dev/null +++ b/run/SimExamples/FastSim_Absorber/run.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# This example exercises the Geant4 fast simulation route on the front absorber. +# +# A fast simulation model replaces the detailed transport through a region of +# the geometry by a function from the particle that enters it to the particles +# that leave it. The model shipped here, `toyAbsorber`, is a placeholder: it +# returns the incident particle carrying on in its direction with an +# exponentially attenuated energy. What the example demonstrates is the +# machinery, not the physics. +# +# The region is named by TRACKING MEDIUM. `ABSO_AIR_ENVELOPE` is the medium of +# AFaM, the mother volume of the whole absorber, and it exists for exactly this +# purpose: Geant4-VMC builds a region per MATERIAL and adds every volume of that +# material to it, so a volume can only be addressed on its own if its material +# is its own. +# +# The setup is PIPE and ABSO only, which keeps the run short and puts the +# absorber in the path of everything. + +set -x + +EVENTS=5 +MODULES="-m PIPE ABSO" +GEN="-g pythia8pp" +# Alignment is irrelevant here and switching it off keeps the example from +# needing a CCDB connection and an alien token. +COMMON="align-geom.mDetectors=none" + +# --------------------------------------------------------------- 1. reference +# Detailed transport, for comparison. +mkdir -p full && cd full +o2-sim-serial -n ${EVENTS} ${GEN} -e TGeant4 ${MODULES} -o full \ + --configKeyValues "${COMMON}" > logfull 2>&1 +cd .. + +# -------------------------------------------------------------------- 2. fast +# G4.fastSimModels is what switches the feature on; with it empty (the default) +# nothing about the simulation changes. +mkdir -p fast && cd fast +o2-sim-serial -n ${EVENTS} ${GEN} -e TGeant4 ${MODULES} -o fast \ + --configKeyValues "${COMMON};\ +G4.fastSimModels=toyAbsorber;\ +G4.fastSimRegions=ABSO_AIR_ENVELOPE;\ +G4.fastSimMinEnergy=1.0" > logfast 2>&1 +cd .. + +# ----------------------------------------------------------------- 3. compare +# The model prints once at setup; if this line is missing the region name did +# not resolve to a medium and nothing was applied. +grep -h "fast simulation" fast/logfast + +# Tracks written per event. The absorber normally turns one incident hadron into +# a shower, and the toy model returns a single particle instead, so the fast run +# has to produce far fewer. +for d in full fast; do + echo "=== ${d}" + root -l -b -q "$(dirname "$0")/countTracks.macro(\"${d}/${d}\")" +done From f0bd228a10ed781232491022b8c0a9bd146b4a09 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sat, 19 Sep 2026 04:18:02 +0200 Subject: [PATCH 4/9] Record what the absorber fast simulation example measures This adds the numbers from a first run of the example, and says what they do and do not show. - Five pp events with PIPE and ABSO: 3544 tracks per event with detailed transport, 3160 with the toy model. - Transport time is 29.8 s against 28.0 s, which is not a performance claim: only the forward cone of a minimum-bias pp event enters the absorber, so most of the transport in this setup happens outside the region. - The geant4_vmc line confirming that the tracking medium resolved to its material is quoted, because a wrong medium name fails silently. --- run/SimExamples/FastSim_Absorber/README.md | 30 +++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/run/SimExamples/FastSim_Absorber/README.md b/run/SimExamples/FastSim_Absorber/README.md index d38886977970e..c2fafe3fe0e48 100644 --- a/run/SimExamples/FastSim_Absorber/README.md +++ b/run/SimExamples/FastSim_Absorber/README.md @@ -51,7 +51,31 @@ Selection by volume is not available: the VMC special cuts already root every logical volume in a per-material region, and Geant4 allows a logical volume in exactly one region. -## What is not measured here +## Measured -CPU. The step and track counts show that the transport is being replaced; a -timing number needs a realistic workload rather than five events of PIPE+ABSO. +Five pp minimum-bias events, Pythia8 and Geant4, `-m PIPE ABSO`, on one EPN node +against `O2PDPSuite/daily-20260819-0000-1`: + +| | full | fast | +|---|---|---| +| tracks per event | 3544 | 3160 | +| transport real time | 29.8 s | 28.0 s | + +The model is demonstrably applied — geant4_vmc reports + +``` +fast simulation is ENABLED for regions 'ABSO_AIR_ENVELOPE' +fast simulation: registering model toyAbsorber above 1 GeV +Adding fast simulation model toyAbsorber to regions ABSO_AIR_ENVELOPE0$ +``` + +where the last line is the tracking medium having resolved to its material, which +is the step that silently does nothing if the medium name is wrong. + +**But the saving here is small, and the example should not be read as a +performance claim.** The absorber sits at z = −90 to −501 cm and only the forward +cone of a minimum-bias pp event ever enters it, so most of the transport this +setup does is not in the region at all; and `fastSimMinEnergy=1` leaves +everything below 1 GeV to the detailed transport. An honest CPU number needs a +workload where the absorber is on the critical path — a forward-biased generator, +or the full detector where the muon arm is what the absorber exists to protect. From 2b7e283cbb98d67fb40aa13a30b42dd05ab6020b Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sat, 19 Sep 2026 04:51:33 +0200 Subject: [PATCH 5/9] Record that the toy model does not fire on the muon-arm path This documents a limitation found by running the example, and withdraws the reading of its track counts. - ABSO_AIR_ENVELOPE selects a region containing AFaM and nothing else: the VMC special cuts make every logical volume a root of its own material's region, and Geant4 stops propagating a region at any such daughter. - AFaM's daughters touch its surface, so a track entering the absorber lands in a daughter and never has AFaM as its volume. - A 20 GeV muon with /tracking/verbose 1 steps through the absorber identically with and without the fast simulation, showing only muIoni, eIoni, Transportation and specialCutForElectron. - The two runs' random sequences diverge before the absorber, so the track counts previously recorded do not measure the model. --- run/SimExamples/FastSim_Absorber/README.md | 52 ++++++++++++++++++---- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/run/SimExamples/FastSim_Absorber/README.md b/run/SimExamples/FastSim_Absorber/README.md index c2fafe3fe0e48..418e58b28c1a0 100644 --- a/run/SimExamples/FastSim_Absorber/README.md +++ b/run/SimExamples/FastSim_Absorber/README.md @@ -51,6 +51,45 @@ Selection by volume is not available: the VMC special cuts already root every logical volume in a per-material region, and Geant4 allows a logical volume in exactly one region. +## Status: the model does not fire on the muon-arm path + +Measured on a real run, and it is a limitation of the region mechanism rather +than of the model. + +`ABSO_AIR_ENVELOPE` selects a region containing `AFaM` **and nothing else**. The +VMC special cuts create one `G4Region` per material and make every logical volume +a root of its own, and Geant4 stops propagating a region down the tree at any +daughter that is itself a region root — so `AFaMgRing` is in `ABSO_MAGNESIUM$`, +`AFaGraphiteConeO` in `ABSO_CARBON0$`, and the absorber's mother region covers +none of them. On top of that, `AFaM`'s daughters touch its surface, so a track +entering the absorber lands straight in a daughter and never has `AFaM` as its +volume at all. + +A 20 GeV muon fired into the muon-arm acceptance with `/tracking/verbose 1` +therefore steps through the absorber identically with and without the fast +simulation enabled: + +``` + 13 54.2 -0.238 -900 1.99e+04 0.0405 287 902 AFaMgRing Transportation + 14 55.5 -0.244 -920 1.99e+04 5.33 20 922 AFaGraphiteConeO Transportation + 15 56.7 -0.254 -940 1.99e+04 5.33 20.2 942 AFaGraphiteConeO muIoni +``` + +Only `muIoni`, `eIoni`, `Transportation` and `specialCutForElectron` appear; no +fast-simulation process does. + +So a region in O2 can be "every volume of a given material" but not "this volume +and its daughters", and a surrogate for a whole module is not expressible through +this interface as it stands. The two ways forward are to name the absorber's +constituent materials and accept a per-piece envelope, or to change +`TG4RegionsManager` upstream so that the cuts regions do not claim every volume +as a root. + +**Do not read the track counts below as a measurement of the model.** They come +from two runs whose random sequences diverge before the absorber is even reached +— visible in the beam pipe — so the difference is not attributable to the fast +simulation. + ## Measured Five pp minimum-bias events, Pythia8 and Geant4, `-m PIPE ABSO`, on one EPN node @@ -61,7 +100,8 @@ against `O2PDPSuite/daily-20260819-0000-1`: | tracks per event | 3544 | 3160 | | transport real time | 29.8 s | 28.0 s | -The model is demonstrably applied — geant4_vmc reports +geant4_vmc does report that the region resolved, which is necessary but, as +above, not sufficient: ``` fast simulation is ENABLED for regions 'ABSO_AIR_ENVELOPE' @@ -72,10 +112,6 @@ Adding fast simulation model toyAbsorber to regions ABSO_AIR_ENVELOPE0$ where the last line is the tracking medium having resolved to its material, which is the step that silently does nothing if the medium name is wrong. -**But the saving here is small, and the example should not be read as a -performance claim.** The absorber sits at z = −90 to −501 cm and only the forward -cone of a minimum-bias pp event ever enters it, so most of the transport this -setup does is not in the region at all; and `fastSimMinEnergy=1` leaves -everything below 1 GeV to the detailed transport. An honest CPU number needs a -workload where the absorber is on the critical path — a forward-biased generator, -or the full detector where the muon arm is what the absorber exists to protect. +**These numbers are not a performance result and not a measurement of the +model** — see the section above. They are recorded only to show what the example +currently produces. From 8be434d3e31ea717d355b61aa5dac0ecce801d8b Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sat, 19 Sep 2026 05:07:36 +0200 Subject: [PATCH 6/9] Trigger a fast simulation model on its envelope, not on its region This makes a model cover a whole module, which the region mechanism alone cannot express. - A Geant4 region in O2 is always "every volume of a given material": the VMC special cuts make every logical volume a root of its own material's region, and Geant4 stops propagating a region at any such daughter. - G4FastTrack::GetEnvelopeSolid() therefore returns one absorber piece, and FastSimModel no longer uses it. - G4.fastSimEnvelope names the volume a model stands in for. Containment and the exit distance are read from the track's own touchable, which already carries the full ancestry and the transform of every level. - ModelTrigger now requires geometric containment in that envelope, which also excludes volumes that merely share a material with it, such as the absorber's support cradle. - The regions needed for Geant4 to consult the model are derived by walking the envelope's subtree and collecting its media, so no list is maintained by hand. The walk happens in CreateUserPostDetConstruction, the one point after the geometry is built and before the media are turned into regions. - G4.fastSimRegions still overrides the walk with an explicit list. --- Common/SimConfig/include/SimConfig/G4Params.h | 4 +- Detectors/FastSim/CMakeLists.txt | 1 + .../FastSim/include/FastSim/FastSimModel.h | 23 +++- .../FastSim/include/FastSim/FastSimRegions.h | 65 ++++++++++ .../include/FastSim/G4FastSimulation.h | 21 ++-- Detectors/FastSim/src/FastSimModel.cxx | 71 ++++++++++- Detectors/FastSim/src/FastSimRegions.cxx | 117 ++++++++++++++++++ Detectors/FastSim/src/G4FastSimulation.cxx | 44 ++++--- 8 files changed, 315 insertions(+), 31 deletions(-) create mode 100644 Detectors/FastSim/include/FastSim/FastSimRegions.h create mode 100644 Detectors/FastSim/src/FastSimRegions.cxx diff --git a/Common/SimConfig/include/SimConfig/G4Params.h b/Common/SimConfig/include/SimConfig/G4Params.h index 2c44f97ab1c61..97c3cc4ddb412 100644 --- a/Common/SimConfig/include/SimConfig/G4Params.h +++ b/Common/SimConfig/include/SimConfig/G4Params.h @@ -58,7 +58,9 @@ struct G4Params : public o2::conf::ConfigurableParamHelper { // Fast simulation. Empty fastSimModels (the default) disables the feature // entirely; see Detectors/gconfig/include/SimSetup/G4FastSimulation.h. std::string fastSimModels = ""; // comma-separated model names to activate - std::string fastSimRegions = ""; // tracking media the models apply to (wildcards allowed) + std::string fastSimEnvelope = ""; // volume a model stands in for, e.g. AFaM; the media of its + // subtree are collected automatically + std::string fastSimRegions = ""; // optional explicit media, overriding the subtree walk float fastSimMinEnergy = 1.f; // GeV; below this the detailed transport runs O2ParamDef(G4Params, "G4"); }; diff --git a/Detectors/FastSim/CMakeLists.txt b/Detectors/FastSim/CMakeLists.txt index b721288809f0a..ae4de342e391b 100644 --- a/Detectors/FastSim/CMakeLists.txt +++ b/Detectors/FastSim/CMakeLists.txt @@ -11,6 +11,7 @@ o2_add_library(FastSim SOURCES src/FastSimModel.cxx + src/FastSimRegions.cxx src/ToyAbsorberFastSim.cxx src/G4FastSimulation.cxx PUBLIC_LINK_LIBRARIES MC::Geant4VMC MC::Geant4 O2::SimConfig diff --git a/Detectors/FastSim/include/FastSim/FastSimModel.h b/Detectors/FastSim/include/FastSim/FastSimModel.h index 86d7718ac772c..7c22e7f01a0fb 100644 --- a/Detectors/FastSim/include/FastSim/FastSimModel.h +++ b/Detectors/FastSim/include/FastSim/FastSimModel.h @@ -39,7 +39,7 @@ struct FastSimInput { double kineticEnergy = 0.; ///< GeV double mass = 0.; ///< GeV double time = 0.; ///< ns - double exitDistance = 0.; ///< cm from `position` to the envelope surface along `direction` + double exitDistance = 0.; ///< cm from `position` to the ENVELOPE surface along `direction` }; /// One particle leaving the envelope. @@ -55,10 +55,23 @@ struct FastSimOutput { /// zero-length steps before it gets out. Models should emit just beyond it. constexpr double kSurfaceEpsilonCm = 1e-5; +/// Base class for fast simulation models. +/// +/// The model is attached to regions (see G4FastSimulation.h) purely so that +/// Geant4 consults it; what it measures against is the ENVELOPE VOLUME named +/// below, which is normally the mother volume of a whole module. The two are +/// deliberately separate, because a Geant4 region in O2 can only ever be "every +/// volume of a given material" -- the VMC special cuts make every logical volume +/// a root of its own material's region, and Geant4 stops propagating a region at +/// any such daughter. So `G4FastTrack::GetEnvelopeSolid()` would hand back one +/// absorber piece rather than the absorber, and this class does not use it. +/// +/// Containment and the exit distance are taken from the track's own touchable, +/// which already carries the full ancestry and the transform of every level. class FastSimModel : public G4VFastSimulationModel { public: - FastSimModel(const G4String& name, double minEnergyGeV); + FastSimModel(const G4String& name, const G4String& envelopeVolume, double minEnergyGeV); G4bool IsApplicable(const G4ParticleDefinition& particle) override; G4bool ModelTrigger(const G4FastTrack& fastTrack) override; @@ -74,7 +87,13 @@ class FastSimModel : public G4VFastSimulationModel virtual std::vector sample(const FastSimInput& input) const = 0; private: + /// The track's ancestry level at which the envelope volume sits, or -1 when + /// the track is not inside it at all. + int envelopeDepth(const G4Track* track) const; + + G4String mEnvelope; ///< logical volume the model measures against double mMinEnergy = 0.; ///< internal Geant4 units; below this the detailed transport runs + mutable bool mWarned = false; }; } // namespace o2::fastsim diff --git a/Detectors/FastSim/include/FastSim/FastSimRegions.h b/Detectors/FastSim/include/FastSim/FastSimRegions.h new file mode 100644 index 0000000000000..a47a9319a18ba --- /dev/null +++ b/Detectors/FastSim/include/FastSim/FastSimRegions.h @@ -0,0 +1,65 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#ifndef O2_FASTSIM_REGIONS_H_ +#define O2_FASTSIM_REGIONS_H_ + +/// Deriving a model's regions from its envelope volume. +/// +/// Geant4-VMC attaches a fast simulation model to regions, and in O2 a region +/// can only be "every volume of a given material" (see FastSimModel.h). To have +/// the model consulted everywhere inside a module, it must therefore be attached +/// to every material that module is built from -- which is a list nobody should +/// maintain by hand, because it changes whenever the geometry does. +/// +/// So walk the envelope's subtree and collect the media as they actually are. + +#include "TG4VUserPostDetConstruction.h" + +#include +#include +#include + +class TGeoVolume; + +namespace o2::fastsim +{ + +/// Every tracking medium used by `volume` or anything below it. +/// Takes a non-const pointer because TGeo's accessors are not const. +std::set mediaInSubtree(TGeoVolume* volume); + +/// Same, looked up by volume name in the current TGeo geometry. Empty if there +/// is no such volume. +std::set mediaInSubtree(const std::string& volumeName); + +/// Sets each model's regions from its envelope, in the one window where that is +/// possible: after the geometry is built and before Geant4-VMC turns the media +/// into regions. +class FastSimRegionConstruction : public TG4VUserPostDetConstruction +{ + public: + struct ModelRegions { + std::string model; + std::string envelope; ///< volume whose subtree supplies the media + std::string regions; ///< explicit media, used instead of the walk if given + }; + + explicit FastSimRegionConstruction(std::vector models); + void Construct() override; + + private: + std::vector mModels; +}; + +} // namespace o2::fastsim + +#endif // O2_FASTSIM_REGIONS_H_ diff --git a/Detectors/FastSim/include/FastSim/G4FastSimulation.h b/Detectors/FastSim/include/FastSim/G4FastSimulation.h index 45d0e0a95cdb2..b6b5327db313c 100644 --- a/Detectors/FastSim/include/FastSim/G4FastSimulation.h +++ b/Detectors/FastSim/include/FastSim/G4FastSimulation.h @@ -18,17 +18,20 @@ /// /// o2-sim -n 10 -g pythia8pp -e TGeant4 -m PIPE ABSO /// --configKeyValues "G4.fastSimModels=toyAbsorber; -/// G4.fastSimRegions=ABSO_AIR_ENVELOPE" +/// G4.fastSimEnvelope=AFaM" /// -/// Regions are selected by TRACKING MEDIUM name (wildcards allowed), which -/// Geant4-VMC maps to the medium's MATERIAL; every volume of that material joins -/// the region. That is why the absorber mother volume AFaM carries a material of -/// its own (see Detectors/Passive/src/Absorber.cxx). Selection by volume is not -/// available: the VMC special cuts already root every logical volume in a -/// per-material region, and Geant4 allows a volume in only one region. +/// `G4.fastSimEnvelope` names the VOLUME the model stands in for. The regions +/// Geant4 needs in order to consult the model are derived from it by walking its +/// subtree and collecting the media (FastSimRegions.h) -- a region in O2 can only +/// be "every volume of a given material", so covering a module means naming all +/// of its materials, and that list should not be maintained by hand. +/// +/// `G4.fastSimRegions` overrides the walk with an explicit space-separated list +/// of media, for when a model should see less than a whole subtree. #include "TG4RunConfiguration.h" #include "TG4VUserFastSimulation.h" +#include "TG4VUserPostDetConstruction.h" #include #include @@ -40,12 +43,13 @@ namespace o2::fastsim class G4FastSimulation : public TG4VUserFastSimulation { public: - G4FastSimulation(std::vector models, const std::string& regions, + G4FastSimulation(std::vector models, const std::string& envelope, double minEnergyGeV); void Construct() override; private: std::vector mModels; + std::string mEnvelope; double mMinEnergy = 1.; }; @@ -56,6 +60,7 @@ class G4RunConfiguration : public TG4RunConfiguration public: using TG4RunConfiguration::TG4RunConfiguration; TG4VUserFastSimulation* CreateUserFastSimulation() override; + TG4VUserPostDetConstruction* CreateUserPostDetConstruction() override; }; } // namespace o2::fastsim diff --git a/Detectors/FastSim/src/FastSimModel.cxx b/Detectors/FastSim/src/FastSimModel.cxx index 828d5b1427fe9..e2d91291f5494 100644 --- a/Detectors/FastSim/src/FastSimModel.cxx +++ b/Detectors/FastSim/src/FastSimModel.cxx @@ -21,7 +21,11 @@ #include #include #include +#include +#include +#include #include +#include #include @@ -29,11 +33,39 @@ namespace o2::fastsim { //_____________________________________________________________________________ -FastSimModel::FastSimModel(const G4String& name, double minEnergyGeV) - : G4VFastSimulationModel(name), mMinEnergy(minEnergyGeV * CLHEP::GeV) +FastSimModel::FastSimModel(const G4String& name, const G4String& envelopeVolume, + double minEnergyGeV) + : G4VFastSimulationModel(name), + mEnvelope(envelopeVolume), + mMinEnergy(minEnergyGeV * CLHEP::GeV) { } +//_____________________________________________________________________________ +int FastSimModel::envelopeDepth(const G4Track* track) const +{ + /// Where the envelope volume sits in the track's ancestry, or -1 if the track + /// is not inside it. + /// + /// The touchable is the cheapest exact answer to "is this track inside the + /// module": it is the navigator's own record of the volume and every one of + /// its ancestors, so no geometry lookup, no cached transform and no name list + /// is needed, and it stays correct if the envelope is ever placed more than + /// once. + const G4VTouchable* touchable = track->GetTouchable(); + if (touchable == nullptr) { + return -1; + } + const G4int depth = touchable->GetHistoryDepth(); + for (G4int level = 0; level <= depth; ++level) { + const G4VPhysicalVolume* volume = touchable->GetVolume(level); + if (volume != nullptr && volume->GetLogicalVolume()->GetName() == mEnvelope) { + return level; + } + } + return -1; +} + //_____________________________________________________________________________ G4bool FastSimModel::IsApplicable(const G4ParticleDefinition&) { @@ -45,9 +77,28 @@ G4bool FastSimModel::IsApplicable(const G4ParticleDefinition&) //_____________________________________________________________________________ G4bool FastSimModel::ModelTrigger(const G4FastTrack& fastTrack) { + const G4Track* track = fastTrack.GetPrimaryTrack(); + // Below the threshold the detailed transport is cheap and a surrogate would // be extrapolating. - return fastTrack.GetPrimaryTrack()->GetKineticEnergy() > mMinEnergy; + if (track->GetKineticEnergy() <= mMinEnergy) { + return false; + } + + // Geometric containment rather than a name list. This is what excludes, for + // instance, the absorber's steel support cradle: it shares its material with + // parts of the absorber, so no selection by material can separate them, but + // it sits outside the envelope and so fails here. + if (envelopeDepth(track) < 0) { + if (!mWarned) { + mWarned = true; + LOG(warn) << "fast simulation: model " << GetName() << " was consulted for a track " + << "outside its envelope '" << mEnvelope << "'; the region selection is " + << "wider than the envelope, which is allowed but wasteful"; + } + return false; + } + return true; } //_____________________________________________________________________________ @@ -68,9 +119,17 @@ void FastSimModel::DoIt(const G4FastTrack& fastTrack, G4FastStep& fastStep) input.kineticEnergy = track->GetKineticEnergy() / CLHEP::GeV; input.mass = track->GetDefinition()->GetPDGMass() / CLHEP::GeV; input.time = track->GetGlobalTime() / CLHEP::ns; - input.exitDistance = fastTrack.GetEnvelopeSolid()->DistanceToOut( - fastTrack.GetPrimaryTrackLocalPosition(), - fastTrack.GetPrimaryTrackLocalDirection()) / + // Deliberately NOT GetEnvelopeSolid(): that is the region's root volume, i.e. + // one absorber piece. Measure against the envelope volume instead, using the + // transform the touchable already holds for that level. + const G4int level = envelopeDepth(track); + const G4VTouchable* touchable = track->GetTouchable(); + const G4AffineTransform& toLocal = + touchable->GetHistory()->GetTransform(touchable->GetHistoryDepth() - level); + const G4VSolid* envelopeSolid = touchable->GetVolume(level)->GetLogicalVolume()->GetSolid(); + + input.exitDistance = envelopeSolid->DistanceToOut(toLocal.TransformPoint(position), + toLocal.TransformAxis(direction)) / CLHEP::cm; const std::vector outgoing = sample(input); diff --git a/Detectors/FastSim/src/FastSimRegions.cxx b/Detectors/FastSim/src/FastSimRegions.cxx new file mode 100644 index 0000000000000..5179fd458c619 --- /dev/null +++ b/Detectors/FastSim/src/FastSimRegions.cxx @@ -0,0 +1,117 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +#include "FastSim/FastSimRegions.h" + +#include "TG4GeometryManager.h" +#include "TG4ModelConfigurationManager.h" + +#include +#include +#include +#include + +#include + +namespace o2::fastsim +{ + +namespace +{ +void collect(TGeoVolume* volume, std::set& media, std::set& seen) +{ + if (volume == nullptr || !seen.insert(volume).second) { + return; // a volume can be placed many times; visit it once + } + if (const TGeoMedium* medium = volume->GetMedium()) { + media.insert(medium->GetName()); + } + TObjArray* nodes = volume->GetNodes(); + if (nodes == nullptr) { + return; + } + for (int i = 0; i < nodes->GetEntriesFast(); ++i) { + auto* node = static_cast(nodes->UncheckedAt(i)); + if (node != nullptr) { + collect(node->GetVolume(), media, seen); + } + } +} +} // namespace + +//_____________________________________________________________________________ +std::set mediaInSubtree(TGeoVolume* volume) +{ + std::set media; + std::set seen; + collect(volume, media, seen); + return media; +} + +//_____________________________________________________________________________ +std::set mediaInSubtree(const std::string& volumeName) +{ + if (gGeoManager == nullptr) { + LOG(error) << "fast simulation: no TGeo geometry when resolving '" << volumeName << "'"; + return {}; + } + auto* volume = gGeoManager->GetVolume(volumeName.c_str()); + if (volume == nullptr) { + LOG(error) << "fast simulation: no volume named '" << volumeName << "' in the geometry"; + return {}; + } + return mediaInSubtree(volume); +} + +//_____________________________________________________________________________ +FastSimRegionConstruction::FastSimRegionConstruction(std::vector models) + : mModels(std::move(models)) +{ +} + +//_____________________________________________________________________________ +void FastSimRegionConstruction::Construct() +{ + /// Called by Geant4-VMC after the geometry is built and immediately before + /// the media are turned into regions, which is the only moment at which this + /// can be done: the geometry does not exist when the model is created, and + /// the regions are fixed once they are made. + auto* manager = TG4GeometryManager::Instance()->GetFastModelsManager(); + + for (const auto& model : mModels) { + std::string regions = model.regions; + if (regions.empty()) { + const auto media = mediaInSubtree(model.envelope); + for (const auto& medium : media) { + // The setter tokenizes on whitespace, so a medium whose name contains a + // space cannot go through it. O2 composes medium names as + // _ and none contain spaces, but say so if that changes + // rather than silently selecting the wrong thing. + if (medium.find(' ') != std::string::npos) { + LOG(warn) << "fast simulation: medium '" << medium << "' contains a space and cannot " + << "be selected; it is skipped"; + continue; + } + regions += (regions.empty() ? "" : " ") + medium; + } + LOG(info) << "fast simulation: model " << model.model << " covers " << media.size() + << " media found under '" << model.envelope << "'"; + } + if (regions.empty()) { + LOG(error) << "fast simulation: model " << model.model << " ended up with no regions"; + continue; + } + LOG(debug) << "fast simulation: regions for " << model.model << ": " << regions; + manager->SetModelRegions(model.model, regions); + } +} + +} // namespace o2::fastsim diff --git a/Detectors/FastSim/src/G4FastSimulation.cxx b/Detectors/FastSim/src/G4FastSimulation.cxx index 89127218ecc5e..c9024bc294da5 100644 --- a/Detectors/FastSim/src/G4FastSimulation.cxx +++ b/Detectors/FastSim/src/G4FastSimulation.cxx @@ -10,6 +10,7 @@ // or submit itself to any jurisdiction. #include "FastSim/G4FastSimulation.h" +#include "FastSim/FastSimRegions.h" #include "FastSim/ToyAbsorberFastSim.h" #include "SimConfig/G4Params.h" @@ -37,19 +38,18 @@ std::vector split(const std::string& value, char sep) } // namespace //_____________________________________________________________________________ -G4FastSimulation::G4FastSimulation(std::vector models, const std::string& regions, - double minEnergyGeV) - : TG4VUserFastSimulation(), mModels(std::move(models)), mMinEnergy(minEnergyGeV) +G4FastSimulation::G4FastSimulation(std::vector models, + const std::string& envelope, double minEnergyGeV) + : TG4VUserFastSimulation(), mModels(std::move(models)), mEnvelope(envelope), + mMinEnergy(minEnergyGeV) { + // Only the model itself can be declared here: this constructor runs before the + // geometry exists, so the regions cannot be derived yet. They are set later by + // FastSimRegionConstruction, in the window between geometry construction and + // region creation. for (const auto& model : mModels) { SetModel(model); SetModelParticles(model, "all"); - if (!regions.empty()) { - SetModelRegions(model, regions); - } else { - LOG(warn) << "fast simulation: model " << model - << " has no G4.fastSimRegions; it will not be applied anywhere"; - } } } @@ -58,9 +58,9 @@ void G4FastSimulation::Construct() { for (const auto& model : mModels) { if (model == "toyAbsorber") { - LOG(info) << "fast simulation: registering model " << model << " above " << mMinEnergy - << " GeV"; - Register(new ToyAbsorberFastSim(model, mMinEnergy)); + LOG(info) << "fast simulation: registering model " << model << " on envelope '" << mEnvelope + << "' above " << mMinEnergy << " GeV"; + Register(new ToyAbsorberFastSim(model, mEnvelope, mMinEnergy)); } else { LOG(error) << "fast simulation: unknown model " << model << "; ignored"; } @@ -75,8 +75,24 @@ TG4VUserFastSimulation* G4RunConfiguration::CreateUserFastSimulation() if (models.empty()) { return nullptr; // the default: no fast simulation, unchanged behaviour } - LOG(info) << "fast simulation is ENABLED for regions '" << params.fastSimRegions << "'"; - return new G4FastSimulation(std::move(models), params.fastSimRegions, params.fastSimMinEnergy); + LOG(info) << "fast simulation is ENABLED on envelope '" << params.fastSimEnvelope << "'"; + return new G4FastSimulation(std::move(models), params.fastSimEnvelope, params.fastSimMinEnergy); +} + +//_____________________________________________________________________________ +TG4VUserPostDetConstruction* G4RunConfiguration::CreateUserPostDetConstruction() +{ + const auto& params = o2::conf::G4Params::Instance(); + auto models = split(params.fastSimModels, ','); + if (models.empty()) { + return TG4RunConfiguration::CreateUserPostDetConstruction(); + } + std::vector wanted; + wanted.reserve(models.size()); + for (auto& model : models) { + wanted.push_back({model, params.fastSimEnvelope, params.fastSimRegions}); + } + return new FastSimRegionConstruction(std::move(wanted)); } } // namespace o2::fastsim From 70a96794e0e08818d530876f7efd4851519fb234 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sat, 19 Sep 2026 09:07:08 +0200 Subject: [PATCH 7/9] Address review: let a model override DoIt, and fix the example's config - DoIt is Geant4's own entry point and is no longer final, so a model that does not fit the common shape can replace it rather than work around it. - Its comment now says what it is: it wraps the logic common to every model and delegates the physics to sample(). - The example passed G4.fastSimRegions=ABSO_AIR_ENVELOPE, which overrides the envelope walk with a region containing only AFaM and reproduces exactly the behaviour this branch fixes. It now passes G4.fastSimEnvelope=AFaM. - Drops an unreachable guard in the toy model: ModelTrigger only calls it above its threshold and an exponential of a finite path cannot reach zero. - Comment wording throughout: say what a class is rather than what O2 used to lack, and "encloses" rather than "measures against". --- .../FastSim/include/FastSim/FastSimModel.h | 22 +++++++++++-------- .../include/FastSim/G4FastSimulation.h | 4 ++-- .../include/FastSim/ToyAbsorberFastSim.h | 6 ++--- Detectors/FastSim/src/FastSimModel.cxx | 4 ++-- Detectors/FastSim/src/ToyAbsorberFastSim.cxx | 5 ++--- run/SimExamples/FastSim_Absorber/run.sh | 11 +++++----- 6 files changed, 27 insertions(+), 25 deletions(-) diff --git a/Detectors/FastSim/include/FastSim/FastSimModel.h b/Detectors/FastSim/include/FastSim/FastSimModel.h index 7c22e7f01a0fb..3bcd5cbe52fd8 100644 --- a/Detectors/FastSim/include/FastSim/FastSimModel.h +++ b/Detectors/FastSim/include/FastSim/FastSimModel.h @@ -16,8 +16,10 @@ /// /// A fast simulation model replaces the detailed transport through a region of /// the geometry by a function from the particle that enters it to the particles -/// that leave it. `DoIt()` below is the plumbing and is the same for every -/// model; a model implements `sample()` and nothing else. +/// that leave it. `DoIt()` is Geant4's own entry point +/// (`G4VFastSimulationModel::DoIt`); the implementation here wraps the logic +/// common to every model and delegates the physics to `sample()`. A model that +/// needs a different shape can still override `DoIt()`. #include "G4VFastSimulationModel.hh" @@ -58,8 +60,8 @@ constexpr double kSurfaceEpsilonCm = 1e-5; /// Base class for fast simulation models. /// /// The model is attached to regions (see G4FastSimulation.h) purely so that -/// Geant4 consults it; what it measures against is the ENVELOPE VOLUME named -/// below, which is normally the mother volume of a whole module. The two are +/// Geant4 consults it; what it encloses is the ENVELOPE VOLUME named below, +/// which is normally the mother volume of a whole module. The two are /// deliberately separate, because a Geant4 region in O2 can only ever be "every /// volume of a given material" -- the VMC special cuts make every logical volume /// a root of its own material's region, and Geant4 stops propagating a region at @@ -76,10 +78,12 @@ class FastSimModel : public G4VFastSimulationModel G4bool IsApplicable(const G4ParticleDefinition& particle) override; G4bool ModelTrigger(const G4FastTrack& fastTrack) override; - /// Measures the distance to the envelope surface, asks `sample()` what comes - /// out, kills the incident particle, stacks the result and books the energy - /// difference as a deposit. - void DoIt(const G4FastTrack& fastTrack, G4FastStep& fastStep) final; + /// Wraps the common logic of a fast simulation action and delegates the + /// physics to `sample()`: it measures the distance to the envelope surface, + /// kills the incident particle, stacks what `sample()` returned and books the + /// energy difference as a deposit. Override it for a model that does not fit + /// that shape. + void DoIt(const G4FastTrack& fastTrack, G4FastStep& fastStep) override; protected: /// Given the particle that entered, return everything that leaves. This is @@ -91,7 +95,7 @@ class FastSimModel : public G4VFastSimulationModel /// the track is not inside it at all. int envelopeDepth(const G4Track* track) const; - G4String mEnvelope; ///< logical volume the model measures against + G4String mEnvelope; ///< logical volume the model encloses double mMinEnergy = 0.; ///< internal Geant4 units; below this the detailed transport runs mutable bool mWarned = false; }; diff --git a/Detectors/FastSim/include/FastSim/G4FastSimulation.h b/Detectors/FastSim/include/FastSim/G4FastSimulation.h index b6b5327db313c..900e5e2d01ef7 100644 --- a/Detectors/FastSim/include/FastSim/G4FastSimulation.h +++ b/Detectors/FastSim/include/FastSim/G4FastSimulation.h @@ -53,8 +53,8 @@ class G4FastSimulation : public TG4VUserFastSimulation double mMinEnergy = 1.; }; -/// The one hook O2 was missing. Returns nullptr when no model is configured, -/// which is exactly the behaviour before this file existed. +/// Supplies Geant4-VMC with the fast simulation models and their regions. +/// Returns nullptr when no model is configured, so nothing is set up. class G4RunConfiguration : public TG4RunConfiguration { public: diff --git a/Detectors/FastSim/include/FastSim/ToyAbsorberFastSim.h b/Detectors/FastSim/include/FastSim/ToyAbsorberFastSim.h index 88990288bd0d2..95ed180bb9527 100644 --- a/Detectors/FastSim/include/FastSim/ToyAbsorberFastSim.h +++ b/Detectors/FastSim/include/FastSim/ToyAbsorberFastSim.h @@ -17,9 +17,9 @@ namespace o2::fastsim { -/// A toy model: one particle out, continuing along the incident direction with -/// the energy exponentially attenuated over the path through the envelope. -/// It exists to exercise the machinery, not to describe an absorber. +/// A toy fast simulation model for the absorber: one particle out, continuing +/// along the incident direction with the energy exponentially attenuated over +/// the path through the envelope. class ToyAbsorberFastSim : public FastSimModel { public: diff --git a/Detectors/FastSim/src/FastSimModel.cxx b/Detectors/FastSim/src/FastSimModel.cxx index e2d91291f5494..56de788834a90 100644 --- a/Detectors/FastSim/src/FastSimModel.cxx +++ b/Detectors/FastSim/src/FastSimModel.cxx @@ -120,8 +120,8 @@ void FastSimModel::DoIt(const G4FastTrack& fastTrack, G4FastStep& fastStep) input.mass = track->GetDefinition()->GetPDGMass() / CLHEP::GeV; input.time = track->GetGlobalTime() / CLHEP::ns; // Deliberately NOT GetEnvelopeSolid(): that is the region's root volume, i.e. - // one absorber piece. Measure against the envelope volume instead, using the - // transform the touchable already holds for that level. + // one absorber piece. Use the envelope volume instead, with the transform the + // touchable already holds for that level. const G4int level = envelopeDepth(track); const G4VTouchable* touchable = track->GetTouchable(); const G4AffineTransform& toLocal = diff --git a/Detectors/FastSim/src/ToyAbsorberFastSim.cxx b/Detectors/FastSim/src/ToyAbsorberFastSim.cxx index bdb5b09cea59b..f91b85cc141f2 100644 --- a/Detectors/FastSim/src/ToyAbsorberFastSim.cxx +++ b/Detectors/FastSim/src/ToyAbsorberFastSim.cxx @@ -28,10 +28,9 @@ std::vector ToyAbsorberFastSim::sample(const FastSimInput& input) // A toy transformation, not a physics model: the incident particle carries on // in its direction with the energy attenuated over the path through the // envelope. A trained model returns a shower here instead. + // Always positive: ModelTrigger only calls a model above its threshold, and + // an exponential of a finite path cannot reach zero. const double kinetic = input.kineticEnergy * std::exp(-input.exitDistance / kAbsorptionLengthCm); - if (kinetic <= 0.) { - return {}; - } const double momentum = std::sqrt(kinetic * (kinetic + 2. * input.mass)); FastSimOutput out; diff --git a/run/SimExamples/FastSim_Absorber/run.sh b/run/SimExamples/FastSim_Absorber/run.sh index ce5e354859f12..ed25df919ddb1 100755 --- a/run/SimExamples/FastSim_Absorber/run.sh +++ b/run/SimExamples/FastSim_Absorber/run.sh @@ -9,11 +9,10 @@ # exponentially attenuated energy. What the example demonstrates is the # machinery, not the physics. # -# The region is named by TRACKING MEDIUM. `ABSO_AIR_ENVELOPE` is the medium of -# AFaM, the mother volume of the whole absorber, and it exists for exactly this -# purpose: Geant4-VMC builds a region per MATERIAL and adds every volume of that -# material to it, so a volume can only be addressed on its own if its material -# is its own. +# `G4.fastSimEnvelope` names the volume the model stands in for: AFaM, the +# mother of the whole absorber. The regions Geant4 needs in order to consult the +# model are derived from it by walking its subtree and collecting the media, +# because a region can only ever be "every volume of a given material". # # The setup is PIPE and ABSO only, which keeps the run short and puts the # absorber in the path of everything. @@ -41,7 +40,7 @@ mkdir -p fast && cd fast o2-sim-serial -n ${EVENTS} ${GEN} -e TGeant4 ${MODULES} -o fast \ --configKeyValues "${COMMON};\ G4.fastSimModels=toyAbsorber;\ -G4.fastSimRegions=ABSO_AIR_ENVELOPE;\ +G4.fastSimEnvelope=AFaM;\ G4.fastSimMinEnergy=1.0" > logfast 2>&1 cd .. From 679654c6fc0edd23c8b0813cfd572a62772c83d3 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sat, 19 Sep 2026 09:12:47 +0200 Subject: [PATCH 8/9] Rewrite the absorber fast simulation example The previous text described the behaviour of the branch before the envelope existed, so most of it was wrong rather than merely stale. - run.sh now fixes the seed in both runs. Without it o2-sim picks one per run, the two simulations see different primaries, and the difference between them is mostly different events: an unseeded pair read 2.25x faster, a seeded pair reads 5%. - Measured on five pp events with PIPE and ABSO: 1547 tracks per event with detailed transport against 1144 with the toy model, 14.9 s against 14.2 s. - Says why a quarter fewer tracks buys five percent of wall clock, and that neither number is a performance result or a physics validation. - Records the two properties that surprise a reader of the output: a fast step does not call the sensitive detector, and its secondaries carry kPNull. --- run/SimExamples/FastSim_Absorber/README.md | 135 ++++++++++----------- run/SimExamples/FastSim_Absorber/run.sh | 8 +- 2 files changed, 68 insertions(+), 75 deletions(-) diff --git a/run/SimExamples/FastSim_Absorber/README.md b/run/SimExamples/FastSim_Absorber/README.md index 418e58b28c1a0..3a7c51a717e8c 100644 --- a/run/SimExamples/FastSim_Absorber/README.md +++ b/run/SimExamples/FastSim_Absorber/README.md @@ -4,7 +4,8 @@ Replaces the detailed transport through the ALICE front absorber by a model, and compares the result against a full simulation of the same events. `run.sh` runs both: a reference `o2-sim` with PIPE and ABSO only, and the same -setup with the fast simulation switched on. +setup with the fast simulation switched on. Both use the same seed, so the two +simulations see the same primaries and the comparison is paired. | file | | |---|---| @@ -16,102 +17,90 @@ setup with the fast simulation switched on. The feature does nothing unless a model is named: ``` ---configKeyValues "G4.fastSimModels=toyAbsorber;G4.fastSimRegions=ABSO_AIR_ENVELOPE" +--configKeyValues "G4.fastSimModels=toyAbsorber;G4.fastSimEnvelope=AFaM" ``` -`G4.fastSimMinEnergy` (GeV, default 1) is the threshold below which the detailed -transport still runs, because a surrogate below it would be extrapolating and -the transport there is cheap anyway. +`G4.fastSimEnvelope` is the volume the model stands in for — here `AFaM`, the +mother of the whole absorber. `G4.fastSimMinEnergy` (GeV, default 1) is the +threshold below which the detailed transport still runs. ## What the model does -`toyAbsorber` is a placeholder. It returns the incident particle continuing in -its direction with the energy attenuated exponentially over the path through the -envelope — a real absorber turns one incident hadron into a shower, so the -numbers this produces are not physics. It is here so that the machinery can be -exercised end to end before a trained model exists. +`toyAbsorber` is a placeholder: it returns the incident particle continuing in +its direction with the energy attenuated exponentially over its path through the +envelope. A real absorber turns one incident hadron into a shower, so these are +not physics numbers. A model implements one function, `sample()`, which maps the particle that entered the region to the particles that leave it -(`Detectors/FastSim/include/FastSim/FastSimModel.h`). Everything around it — +(`Detectors/FastSim/include/FastSim/FastSimModel.h`). The surrounding work — measuring the distance to the envelope surface, killing the incident particle, -stacking what comes back, booking the energy difference as a deposit — is shared -and does not have to be reimplemented. +stacking what comes back, booking the energy difference as a deposit — is shared. -## Why the region is named `ABSO_AIR_ENVELOPE` +## How the envelope and the regions relate -Regions are selected by tracking medium, which Geant4-VMC maps to that medium's -material, adding every volume of that material to the region. A volume can -therefore only be addressed on its own if its material is its own, which is why -`AFaM` — the mother volume of the whole absorber — carries a dedicated air -material (`Detectors/Passive/src/Absorber.cxx`). Selecting it means "the -absorber", with all of its daughters inside. +Geant4 consults a fast simulation model through *regions*, and in O2 a region can +only ever be "every volume of a given material": the VMC special cuts make every +logical volume a root of its own material's region, and Geant4 stops propagating +a region at any such daughter. A region named after `AFaM` would therefore +contain `AFaM` alone, which tracks skip entirely because its daughters touch its +surface. -Selection by volume is not available: the VMC special cuts already root every -logical volume in a per-material region, and Geant4 allows a logical volume in -exactly one region. - -## Status: the model does not fire on the muon-arm path - -Measured on a real run, and it is a limitation of the region mechanism rather -than of the model. - -`ABSO_AIR_ENVELOPE` selects a region containing `AFaM` **and nothing else**. The -VMC special cuts create one `G4Region` per material and make every logical volume -a root of its own, and Geant4 stops propagating a region down the tree at any -daughter that is itself a region root — so `AFaMgRing` is in `ABSO_MAGNESIUM$`, -`AFaGraphiteConeO` in `ABSO_CARBON0$`, and the absorber's mother region covers -none of them. On top of that, `AFaM`'s daughters touch its surface, so a track -entering the absorber lands straight in a daughter and never has `AFaM` as its -volume at all. - -A 20 GeV muon fired into the muon-arm acceptance with `/tracking/verbose 1` -therefore steps through the absorber identically with and without the fast -simulation enabled: +So the two jobs are separated. The regions are derived by walking the envelope's +subtree and collecting its media, which for `AFaM` finds fourteen: ``` - 13 54.2 -0.238 -900 1.99e+04 0.0405 287 902 AFaMgRing Transportation - 14 55.5 -0.244 -920 1.99e+04 5.33 20 922 AFaGraphiteConeO Transportation - 15 56.7 -0.254 -940 1.99e+04 5.33 20.2 942 AFaGraphiteConeO muIoni +fast simulation: model toyAbsorber covers 14 media found under 'AFaM' +Adding fast simulation model toyAbsorber to regions ABSO_AIR0$ ABSO_AIR_ENVELOPE0$ + ABSO_CONCRETE2$ ABSO_POLYETHYLEN2$ ABSO_CARBON0$ ABSO_CARBON2$ ABSO_MAGNESIUM$ + ABSO_Ni-W-Cu0$ ABSO_Ni-W-Cu2$ ABSO_LEAD0$ ABSO_LEAD2$ ABSO_STAINLESS STEEL0$ + ABSO_STAINLESS STEEL2$ ``` -Only `muIoni`, `eIoni`, `Transportation` and `specialCutForElectron` appear; no -fast-simulation process does. - -So a region in O2 can be "every volume of a given material" but not "this volume -and its daughters", and a surrogate for a whole module is not expressible through -this interface as it stands. The two ways forward are to name the absorber's -constituent materials and accept a per-piece envelope, or to change -`TG4RegionsManager` upstream so that the cuts regions do not claim every volume -as a root. - -**Do not read the track counts below as a measurement of the model.** They come -from two runs whose random sequences diverge before the absorber is even reached -— visible in the beam pipe — so the difference is not attributable to the fast -simulation. +What the model measures against is the envelope itself, read from the track's own +touchable. `ModelTrigger` also requires geometric containment in it, which is +what excludes the steel support cradle — it shares its material with the end +plate, so no selection by material could separate them. ## Measured -Five pp minimum-bias events, Pythia8 and Geant4, `-m PIPE ABSO`, on one EPN node -against `O2PDPSuite/daily-20260819-0000-1`: +Five pp minimum-bias events, Pythia8 and Geant4, `-m PIPE ABSO`, same seed, one +EPN node against `O2PDPSuite/daily-20260819-0000-1`: | | full | fast | |---|---|---| -| tracks per event | 3544 | 3160 | -| transport real time | 29.8 s | 28.0 s | +| tracks per event | 1547 | 1144 | +| transport real time | 14.9 s | 14.2 s | -geant4_vmc does report that the region resolved, which is necessary but, as -above, not sufficient: +A 20 GeV muon fired into the muon-arm acceptance with `/tracking/verbose 1` +shows what the model does to a single track: ``` -fast simulation is ENABLED for regions 'ABSO_AIR_ENVELOPE' -fast simulation: registering model toyAbsorber above 1 GeV -Adding fast simulation model toyAbsorber to regions ABSO_AIR_ENVELOPE0$ + 10 54.4 -0.137 -900 1.99e+04 0.0749 287 902 AFaMgRing Transportation + 11 54.4 -0.137 -900 0 1.99e+04 4.1e+03 5e+03 AFaMgRing G4FastSimulationManagerProcess ``` -where the last line is the tracking medium having resolved to its material, which -is the step that silently does nothing if the medium name is wrong. - -**These numbers are not a performance result and not a measurement of the -model** — see the section above. They are recorded only to show what the example -currently produces. +One step of 4.1 m across the whole absorber, in place of roughly twenty `muIoni` +steps through graphite, concrete and steel. + +## What these numbers are not + +**Not a performance result.** A quarter fewer tracks buys only five percent of +wall clock, because the tracks the absorber's shower contributes are cheap +low-energy ones and most of the CPU in this setup is spent elsewhere. And with +minimum-bias pp only the forward cone reaches the absorber at all. A CPU number +worth quoting needs a workload where the absorber is on the critical path — a +forward-biased generator, or the full detector where the muon arm is the point. + +**Not a physics validation.** The toy returns one particle where a real absorber +returns a shower, so the track counts above say more about the placeholder than +about the absorber. Comparing the outgoing multiplicity and spectrum against a +full simulation is what a trained model has to pass, and that is the measurement +this example is scaffolding for. + +Two further caveats worth knowing when reading any output of this: a fast step +does not call the sensitive detector, so its steps disappear from MCStepLogger +(harmless for a passive envelope, which has no hits); and secondaries the model +creates carry `TMCProcess` `kPNull`, the code geant4_vmc gives to everything it +has no VMC equivalent for, so they cannot be told apart from other tracks by +process alone. diff --git a/run/SimExamples/FastSim_Absorber/run.sh b/run/SimExamples/FastSim_Absorber/run.sh index ed25df919ddb1..057b649fbc391 100755 --- a/run/SimExamples/FastSim_Absorber/run.sh +++ b/run/SimExamples/FastSim_Absorber/run.sh @@ -22,6 +22,10 @@ set -x EVENTS=5 MODULES="-m PIPE ABSO" GEN="-g pythia8pp" +# The same seed in both runs, so the two simulations see the SAME primaries and +# the comparison is paired. Without it o2-sim picks a seed per run and the +# difference between the two is mostly different events. +SEED="--seed 12345" # Alignment is irrelevant here and switching it off keeps the example from # needing a CCDB connection and an alien token. COMMON="align-geom.mDetectors=none" @@ -29,7 +33,7 @@ COMMON="align-geom.mDetectors=none" # --------------------------------------------------------------- 1. reference # Detailed transport, for comparison. mkdir -p full && cd full -o2-sim-serial -n ${EVENTS} ${GEN} -e TGeant4 ${MODULES} -o full \ +o2-sim-serial -n ${EVENTS} ${GEN} ${SEED} -e TGeant4 ${MODULES} -o full \ --configKeyValues "${COMMON}" > logfull 2>&1 cd .. @@ -37,7 +41,7 @@ cd .. # G4.fastSimModels is what switches the feature on; with it empty (the default) # nothing about the simulation changes. mkdir -p fast && cd fast -o2-sim-serial -n ${EVENTS} ${GEN} -e TGeant4 ${MODULES} -o fast \ +o2-sim-serial -n ${EVENTS} ${GEN} ${SEED} -e TGeant4 ${MODULES} -o fast \ --configKeyValues "${COMMON};\ G4.fastSimModels=toyAbsorber;\ G4.fastSimEnvelope=AFaM;\ From b643cdced76f2893064a797a88f3b13ed22231dc Mon Sep 17 00:00:00 2001 From: ALICE Action Bot Date: Thu, 20 Aug 2026 15:16:00 +0000 Subject: [PATCH 9/9] Please consider the following formatting changes --- Detectors/FastSim/src/FastSimModel.cxx | 2 +- Detectors/FastSim/src/G4FastSimulation.cxx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Detectors/FastSim/src/FastSimModel.cxx b/Detectors/FastSim/src/FastSimModel.cxx index 56de788834a90..2237d0cd5e69c 100644 --- a/Detectors/FastSim/src/FastSimModel.cxx +++ b/Detectors/FastSim/src/FastSimModel.cxx @@ -129,7 +129,7 @@ void FastSimModel::DoIt(const G4FastTrack& fastTrack, G4FastStep& fastStep) const G4VSolid* envelopeSolid = touchable->GetVolume(level)->GetLogicalVolume()->GetSolid(); input.exitDistance = envelopeSolid->DistanceToOut(toLocal.TransformPoint(position), - toLocal.TransformAxis(direction)) / + toLocal.TransformAxis(direction)) / CLHEP::cm; const std::vector outgoing = sample(input); diff --git a/Detectors/FastSim/src/G4FastSimulation.cxx b/Detectors/FastSim/src/G4FastSimulation.cxx index c9024bc294da5..6349084fdf04d 100644 --- a/Detectors/FastSim/src/G4FastSimulation.cxx +++ b/Detectors/FastSim/src/G4FastSimulation.cxx @@ -40,8 +40,7 @@ std::vector split(const std::string& value, char sep) //_____________________________________________________________________________ G4FastSimulation::G4FastSimulation(std::vector models, const std::string& envelope, double minEnergyGeV) - : TG4VUserFastSimulation(), mModels(std::move(models)), mEnvelope(envelope), - mMinEnergy(minEnergyGeV) + : TG4VUserFastSimulation(), mModels(std::move(models)), mEnvelope(envelope), mMinEnergy(minEnergyGeV) { // Only the model itself can be declared here: this constructor runs before the // geometry exists, so the regions cannot be derived yet. They are set later by