From 450326fafd78b82b33e6d67897b322393791ff55 Mon Sep 17 00:00:00 2001 From: MateuszBala Date: Sat, 29 Aug 2026 15:38:20 +0200 Subject: [PATCH 1/6] fix(source): rename setPromptPhotonProbabilites to setPromptPhotonProbabilities --- docs/source_and_particle_management.rst | 6 +++--- source/physics/include/GatePositroniumSourceMessenger.hh | 2 +- source/physics/src/GatePositroniumSourceMessenger.cc | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source_and_particle_management.rst b/docs/source_and_particle_management.rst index 35b2b45e1..b6f8e3670 100644 --- a/docs/source_and_particle_management.rst +++ b/docs/source_and_particle_management.rst @@ -822,7 +822,7 @@ Required commands ``/gate/source/NAME/setPositroniumLifetimes t1 t2 ... tn unit`` Lifetime of each component. The last token is a Geant4 time unit such as ``ns``. -``/gate/source/NAME/setPromptPhotonProbabilites p1 p2 ... pn`` +``/gate/source/NAME/setPromptPhotonProbabilities p1 p2 ... pn`` Probability of prompt-gamma emission for each component. Values are restricted to the ``[0, 1]`` range by the messenger. ``/gate/source/NAME/setPromptPhotonEnergies e1 e2 ... en unit`` @@ -855,7 +855,7 @@ Optional commands .. note:: - The current implementation requires both ``setPromptPhotonProbabilites`` and ``setPromptPhotonEnergies`` even when the prompt-gamma probability is zero for all components. A common convention is to set the corresponding energies to ``0``. + The current implementation requires both ``setPromptPhotonProbabilities`` and ``setPromptPhotonEnergies`` even when the prompt-gamma probability is zero for all components. A common convention is to set the corresponding energies to ``0``. Event timing and generated vertices ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -958,7 +958,7 @@ The example below defines a two-component source with explicit 2-gamma and 3-gam /gate/source/psSource/setPositroniumLifetimes 0.125 142.0 ns /gate/source/psSource/setDecayKinds k2Gamma k3Gamma /gate/source/psSource/setPositronInteractions kParaPs kOrthoPs - /gate/source/psSource/setPromptPhotonProbabilites 1.0 1.0 + /gate/source/psSource/setPromptPhotonProbabilities 1.0 1.0 /gate/source/psSource/setPromptPhotonEnergies 1.274 1.274 MeV /gate/source/psSource/setElectronCaptureProbabilities 0.0 0.0 /gate/source/psSource/setMeanPositronRange 0.2 0.2 mm diff --git a/source/physics/include/GatePositroniumSourceMessenger.hh b/source/physics/include/GatePositroniumSourceMessenger.hh index 3c3e77b1b..af7d07cc1 100644 --- a/source/physics/include/GatePositroniumSourceMessenger.hh +++ b/source/physics/include/GatePositroniumSourceMessenger.hh @@ -49,7 +49,7 @@ class GatePositroniumSourceMessenger: public GateVSourceMessenger std::unique_ptr upCmdSetPositroniumFractions; std::unique_ptr upCmdSetPositroniumLifetimes; - std::unique_ptr upCmdSetPromptPhotonProbabilites; + std::unique_ptr upCmdSetPromptPhotonProbabilities; std::unique_ptr upCmdSetPromptPhotonEnergies; std::unique_ptr upCmdSetElectronCaptureProbabilities; std::unique_ptr upCmdSetDecayKinds; diff --git a/source/physics/src/GatePositroniumSourceMessenger.cc b/source/physics/src/GatePositroniumSourceMessenger.cc index 0e3abb8bd..f30a4e1d4 100644 --- a/source/physics/src/GatePositroniumSourceMessenger.cc +++ b/source/physics/src/GatePositroniumSourceMessenger.cc @@ -79,7 +79,7 @@ void GatePositroniumSourceMessenger::InitCommands() upCmdSetPositroniumLifetimes.reset(GetStringCmd( "setPositroniumLifetimes", "\"t1, t2, t3 .., tn t_unit\" - where ti corresponds to lifetime constants and t_unit is one of the Geant4 time units e.g. ns" ) ); upCmdSetDecayKinds.reset(GetStringCmd( "setDecayKinds", "\"k1, k2, k3 .., kn\" - where ki is k2Gamma or k3Gamma" ) ); upCmdSetPositronInteractions.reset(GetStringCmd( "setPositronInteractions", "\"k1, k2, k3 .., kn\" - where ki is kParaPs, kDirect or kOrthoPs, of a given element in vector of components. Used to properly recalculate intensities of the components from the theory" ) ); - upCmdSetPromptPhotonProbabilites.reset(GetStringCmd( "setPromptPhotonProbabilites", "\"f1, f2, f3 .., fn\" - where fi in [0.0, 1.0] " ) ); + upCmdSetPromptPhotonProbabilities.reset(GetStringCmd( "setPromptPhotonProbabilities", "\"f1, f2, f3 .., fn\" - where fi in [0.0, 1.0] " ) ); upCmdSetPromptPhotonEnergies.reset(GetStringCmd( "setPromptPhotonEnergies", "\"e1, e2, e3 .., en e_unit\" - where ei are energies and e_unit is one of the Geant4 energy units e.g. MeV" ) ); upCmdSetMeanPositronRange.reset(GetStringCmd( "setMeanPositronRange", "\"r1, r2, r3 .., rn r_unit\" - where ri are mean positron range and r_unit is one of the Geant 4 distance units e.g. mm " ) ); upCmdSetElectronCaptureProbabilities.reset(GetStringCmd( "setElectronCaptureProbabilities", "\"f1, f2, f3 .., fn\" - where fi in [0.0, 1.0] " ) ); @@ -123,7 +123,7 @@ void GatePositroniumSourceMessenger::SetNewValue(G4UIcommand *command, G4String } else if (command == upCmdSetPositroniumLifetimes.get()) { auto lifetimes = parseListOfParamsWithUnit(new_value); fParamGenerator.SetPositroniumLifetimes(lifetimes); - } else if (command == upCmdSetPromptPhotonProbabilites.get()) { + } else if (command == upCmdSetPromptPhotonProbabilities.get()) { std::vector promptPhotonProb; std::stringstream ss(new_value); float prob; From fbc824fabef702dd7ee408b7d22ee6063582dba9 Mon Sep 17 00:00:00 2001 From: MateuszBala Date: Sat, 29 Aug 2026 09:54:35 +0200 Subject: [PATCH 2/6] fix(multiphoton): skip events without primary vertex quietly --- source/digits_hits/src/GateMultiPhotonAnalysis.cc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/source/digits_hits/src/GateMultiPhotonAnalysis.cc b/source/digits_hits/src/GateMultiPhotonAnalysis.cc index 36417e26c..72fcfc1f8 100644 --- a/source/digits_hits/src/GateMultiPhotonAnalysis.cc +++ b/source/digits_hits/src/GateMultiPhotonAnalysis.cc @@ -297,6 +297,15 @@ void GateMultiPhotonAnalysis::RecordEndOfEvent(const G4Event *event) { return; } + // An event without any primary vertex carries no track, hence no hit and no trajectory + // container. GateSourceMgr stops generating vertices once the time limit of the run is + // reached, so the last event of every run looks exactly like this. There is nothing to + // analyse and nothing anomalous about it, so it is skipped quietly instead of being + // reported as a missing trajectory container. + if (event->GetNumberOfPrimaryVertex() == 0) { + return; + } + GateRunManager *runManager = GateRunManager::GetRunManager(); GateSteppingAction *steppingAction = (GateSteppingAction *)(runManager->GetUserSteppingAction()); TrackingMode mode = steppingAction->GetMode(); From 9013f5eaf463d172225d6c285c7d12962e534b1c Mon Sep 17 00:00:00 2001 From: MateuszBala Date: Sat, 29 Aug 2026 15:54:07 +0200 Subject: [PATCH 3/6] style(source): correct a typo in the positronium parameters error message --- source/physics/src/GatePositroniumDecayParamsGenerator.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/physics/src/GatePositroniumDecayParamsGenerator.cc b/source/physics/src/GatePositroniumDecayParamsGenerator.cc index b30471506..1c08bf5ef 100644 --- a/source/physics/src/GatePositroniumDecayParamsGenerator.cc +++ b/source/physics/src/GatePositroniumDecayParamsGenerator.cc @@ -122,7 +122,7 @@ void GatePositroniumDecayParamsGenerator::validatePositroniumDecayParams(const P GateError( "GatePositroniumDecayParamsGenerator::generatePositroniumDecayParams: " "number of provided parameters in Fractions, PromptGamma, Lifetimes, " - "Gamma Energies, Electron Capture Probabilites are not the same"); + "Gamma Energies, Electron Capture Probabilities are not the same"); } if (params.fPositronInteractions.empty()) GateWarning("PositroniumSource: setPositronInteractions was not called. " From 4aeb6e541bc337358186d26bd99b76a1586f9f2e Mon Sep 17 00:00:00 2001 From: MateuszBala Date: Fri, 28 Aug 2026 17:14:53 +0200 Subject: [PATCH 4/6] fix(digitizer): keep sourceID when the interaction volume name is empty --- source/digits_hits/src/GateDigitizerInitializationModule.cc | 5 +++-- source/digits_hits/src/GateHit.cc | 1 + source/digits_hits/src/GateHitConvertor.cc | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/source/digits_hits/src/GateDigitizerInitializationModule.cc b/source/digits_hits/src/GateDigitizerInitializationModule.cc index 9815536d9..a61460a5c 100644 --- a/source/digits_hits/src/GateDigitizerInitializationModule.cc +++ b/source/digits_hits/src/GateDigitizerInitializationModule.cc @@ -135,14 +135,15 @@ void GateDigitizerInitializationModule::Digitize() //------------------------------------------------- + // An empty volume name only means that the output module in use did not + // fill it, so it is replaced by the "NULL" placeholder. It tells nothing + // about the source of the event, hence sourceID is left untouched. if ((*inHC)[i]->GetComptonVolumeName().empty()) { Digi->SetComptonVolumeName( "NULL" ); - Digi->SetSourceID( -1 ); } if ((*inHC)[i]->GetRayleighVolumeName().empty()) { Digi->SetRayleighVolumeName( "NULL" ); - Digi->SetSourceID( -1 ); } /* // if (nVerboseLevel>1) diff --git a/source/digits_hits/src/GateHit.cc b/source/digits_hits/src/GateHit.cc index 99d307d83..47183854f 100644 --- a/source/digits_hits/src/GateHit.cc +++ b/source/digits_hits/src/GateHit.cc @@ -27,6 +27,7 @@ GateHit::GateHit() m_PDGEncoding(0), m_trackID(0), m_parentID(0), + m_sourceID(-1), m_systemID(-1), m_sourceEnergy(-1), m_sourcePDG(0), diff --git a/source/digits_hits/src/GateHitConvertor.cc b/source/digits_hits/src/GateHitConvertor.cc index 818a99ef2..f76330d9e 100644 --- a/source/digits_hits/src/GateHitConvertor.cc +++ b/source/digits_hits/src/GateHitConvertor.cc @@ -179,14 +179,15 @@ void GateHitConvertor::ProcessOneHit(const GateHit* hit,GatePulseList* pulseList + // An empty volume name only means that the output module in use did not fill it, + // so it is replaced by the "NULL" placeholder. It tells nothing about the source + // of the event, hence sourceID is left untouched. if (hit->GetComptonVolumeName().empty()) { pulse->SetComptonVolumeName( "NULL" ); - pulse->SetSourceID( -1 ); } if (hit->GetRayleighVolumeName().empty()) { pulse->SetRayleighVolumeName( "NULL" ); - pulse->SetSourceID( -1 ); } if (nVerboseLevel>1) From 548e3460d640a2a1950a902ea7800ebadae3615a Mon Sep 17 00:00:00 2001 From: MateuszBala Date: Sat, 29 Aug 2026 10:05:52 +0200 Subject: [PATCH 5/6] fix(multiphoton): always run the digitizer at end of event --- .../src/GateMultiPhotonAnalysis.cc | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/source/digits_hits/src/GateMultiPhotonAnalysis.cc b/source/digits_hits/src/GateMultiPhotonAnalysis.cc index 72fcfc1f8..b94f7a1c2 100644 --- a/source/digits_hits/src/GateMultiPhotonAnalysis.cc +++ b/source/digits_hits/src/GateMultiPhotonAnalysis.cc @@ -226,6 +226,20 @@ void RunDigitizersIfNeeded() { } } +//! Runs the digitizer chain when the enclosing scope is left, whichever path is taken. +//! GateAnalysis calls the digitizer outside of its "no trajectory container" branch, so +//! whether Singles and Coincidences are produced for an event does not depend on the +//! analysis being able to process that event. The guard gives the same guarantee here, +//! where the event is abandoned in more than one place. +class ScopedDigitizerRunner { + public: + ScopedDigitizerRunner() = default; + ~ScopedDigitizerRunner() { RunDigitizersIfNeeded(); } + + ScopedDigitizerRunner(const ScopedDigitizerRunner &) = delete; + ScopedDigitizerRunner &operator=(const ScopedDigitizerRunner &) = delete; +}; + } // namespace GateMultiPhotonAnalysis::GateMultiPhotonAnalysis(const G4String &name, GateOutputMgr *outputMgr, DigiMode digiMode) @@ -297,15 +311,6 @@ void GateMultiPhotonAnalysis::RecordEndOfEvent(const G4Event *event) { return; } - // An event without any primary vertex carries no track, hence no hit and no trajectory - // container. GateSourceMgr stops generating vertices once the time limit of the run is - // reached, so the last event of every run looks exactly like this. There is nothing to - // analyse and nothing anomalous about it, so it is skipped quietly instead of being - // reported as a missing trajectory container. - if (event->GetNumberOfPrimaryVertex() == 0) { - return; - } - GateRunManager *runManager = GateRunManager::GetRunManager(); GateSteppingAction *steppingAction = (GateSteppingAction *)(runManager->GetUserSteppingAction()); TrackingMode mode = steppingAction->GetMode(); @@ -321,6 +326,18 @@ void GateMultiPhotonAnalysis::RecordEndOfEvent(const G4Event *event) { return; } + // From here on the event is digitized whatever happens to the analysis itself. + ScopedDigitizerRunner digitizerRunner; + + // An event without any primary vertex carries no track, hence no hit and no trajectory + // container. GateSourceMgr stops generating vertices once the time limit of the run is + // reached, so the last event of every run looks exactly like this. There is nothing to + // analyse and nothing anomalous about it, so it is skipped quietly instead of being + // reported as a missing trajectory container. + if (event->GetNumberOfPrimaryVertex() == 0) { + return; + } + std::vector CHC_vector = GetOutputMgr()->GetHitCollections(); GatePhantomHitsCollection *PHC = GetOutputMgr()->GetPhantomHitCollection(); @@ -384,8 +401,6 @@ void GateMultiPhotonAnalysis::RecordEndOfEvent(const G4Event *event) { m_trajectoryNavigator); ProcessTimeline(&timeline, m_trajectoryNavigator, context, legacyPhotonIDPolicy); } - - RunDigitizersIfNeeded(); } void GateMultiPhotonAnalysis::RecordStepWithVolume(const GateVVolume *, const G4Step *) { From 8ca6c252a7955d4f2e3eb1a6806836544f90a2cc Mon Sep 17 00:00:00 2001 From: MateuszBala Date: Fri, 28 Aug 2026 17:51:54 +0200 Subject: [PATCH 6/6] fix(output): do not index the hit tree vector after the run cleared it --- source/digits_hits/src/GateToRoot.cc | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/source/digits_hits/src/GateToRoot.cc b/source/digits_hits/src/GateToRoot.cc index 24c19de81..a3fa6a925 100644 --- a/source/digits_hits/src/GateToRoot.cc +++ b/source/digits_hits/src/GateToRoot.cc @@ -628,9 +628,17 @@ void GateToRoot::RecordEndOfAcquisition() { // m_hfile = m_treeHit->GetCurrentFile(); - // Get file only for the 1st tree as it is the same file for all trees - m_hfile = m_treesHit[0]->GetCurrentFile(); + // Get file only for the 1st tree as it is the same file for all trees. + // The vector may be empty here: RecordEndOfRun() drops the trees (it saves the + // current file beforehand) and a setup without any sensitive detector never fills + // it. In both cases m_hfile already points to the file to be written. + if (!m_treesHit.empty() && m_treesHit[0]) + m_hfile = m_treesHit[0]->GetCurrentFile(); + if (!m_hfile) { + G4cerr << "GateToRoot::RecordEndOfAcquisition(): no ROOT file to write !\n"; + return; + } if (nVerboseLevel > 0) G4cout << "GateToRoot: ROOT: files writing...\n"; @@ -686,6 +694,12 @@ void GateToRoot::RecordEndOfRun(const G4Run *) { nbPrimaries -= 1.; // Number of primaries increase too much at each end of run ! + // ROOT may have switched to another file while the run was written (files are split + // above 1.9 GBytes), so remember where the trees actually live before dropping them - + // RecordEndOfAcquisition() has no tree left to ask. + if (!m_treesHit.empty() && m_treesHit[0]) + m_hfile = m_treesHit[0]->GetCurrentFile(); + m_hitBuffers.clear(); m_treesHit.clear(); }