From 8a7398bea5e0fc1bae611dd2cf4e9bc23cb47262 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 9 Nov 2026 01:07:29 +0100 Subject: [PATCH 01/11] Pass per-digitizer workspaces down the TPC digitization output path This replaces two shared static workspaces of the TPC digitizer by members, in preparation for a multithreaded digitizer. - The shaped-signal buffer of Digitizer::process and the label sorting buffer of DigitGlobalPad::fillOutputContainer are now members. - The SAMPAProcessing used for the output is passed down from DigitContainer, which uses the singleton unless one is set. https://its.cern.ch/jira/browse/O2-7309 Co-Authored-By: Claude Opus 5.5 --- .../include/TPCSimulation/DigitContainer.h | 6 ++++++ .../include/TPCSimulation/DigitGlobalPad.h | 4 ++-- .../include/TPCSimulation/DigitTime.h | 4 +++- .../include/TPCSimulation/Digitizer.h | 1 + .../TPC/simulation/src/DigitContainer.cxx | 19 +++++++++++-------- Detectors/TPC/simulation/src/Digitizer.cxx | 7 +++---- 6 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Detectors/TPC/simulation/include/TPCSimulation/DigitContainer.h b/Detectors/TPC/simulation/include/TPCSimulation/DigitContainer.h index 472a3b2c74618..c44bababfb98e 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/DigitContainer.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/DigitContainer.h @@ -21,6 +21,7 @@ #include "TPCBase/CRU.h" #include "DataFormatsTPC/Defs.h" #include "TPCSimulation/DigitTime.h" +#include "TPCSimulation/SAMPAProcessing.h" #include "TPCBase/CalDet.h" #include "CommonUtils/DebugStreamer.h" #include "TPCBase/ParameterDetector.h" @@ -87,6 +88,9 @@ class DigitContainer /// Get the size of the container for one event size_t size() const { return mTimeBins.size(); } + /// Use this SAMPAProcessing instead of the singleton when filling the output + void setSAMPAProcessing(SAMPAProcessing* sampaProcessing) { mSAMPAProcessing = sampaProcessing; } + /// Fetch the calibration objects used by fillOutputContainer from the CDBInterface /// Must be called after the CDBInterface is configured; fillOutputContainer calls it lazily otherwise void updateCalibrations(); @@ -103,6 +107,8 @@ class DigitContainer const CalDet* mDeadMap = nullptr; //!< Dead channel map bool mCMCEnabled = false; //!< Common mode correction enabled in the FEE config (DigiMode Auto) bool mCalibrationsUpdated = false; //!< Whether updateCalibrations was called + SAMPAProcessing* mSAMPAProcessing = nullptr; //!< SAMPAProcessing used for the output, the singleton if not set + std::vector> mLabelCollector; //!< Workspace to sort the MC labels of a digit void reportSettings(); }; diff --git a/Detectors/TPC/simulation/include/TPCSimulation/DigitGlobalPad.h b/Detectors/TPC/simulation/include/TPCSimulation/DigitGlobalPad.h index 743dfc3bd6977..6c08abbfca799 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/DigitGlobalPad.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/DigitGlobalPad.h @@ -97,6 +97,7 @@ class DigitGlobalPad const CRU& cru, TimeBin timeBin, GlobalPadNumber globalPad, o2::dataformats::LabelContainer, false>& labelContainer, + SAMPAProcessing& sampaProcessing, std::vector>& labelCollector, float commonMode = 0.f, const PrevDigitInfo& prevDigit = PrevDigitInfo(), Streamer* debugStream = nullptr, const CalDet* deadMap = nullptr); private: @@ -210,12 +211,11 @@ inline void DigitGlobalPad::fillOutputContainer(std::vector& output, const CRU& cru, TimeBin timeBin, GlobalPadNumber globalPad, o2::dataformats::LabelContainer, false>& labels, + SAMPAProcessing& sampaProcessing, std::vector>& labelCollector, float commonMode, const PrevDigitInfo& prevDigit, Streamer* debugStream, const CalDet* deadMap) { const Mapper& mapper = Mapper::instance(); - SAMPAProcessing& sampaProcessing = SAMPAProcessing::instance(); const PadPos pad = mapper.padPos(globalPad); - static std::vector> labelCollector; // static workspace container for sorting /// The charge accumulated on that pad is converted into ADC counts, saturation of the SAMPA is applied and a Digit /// is created in written out diff --git a/Detectors/TPC/simulation/include/TPCSimulation/DigitTime.h b/Detectors/TPC/simulation/include/TPCSimulation/DigitTime.h index 9f951afc2442e..48a84b4fea004 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/DigitTime.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/DigitTime.h @@ -78,6 +78,7 @@ class DigitTime template void fillOutputContainer(std::vector& output, dataformats::MCTruthContainer& mcTruth, std::vector& commonModeOutput, const Sector& sector, TimeBin timeBin, + SAMPAProcessing& sampaProcessing, std::vector>& labelCollector, PrevDigitInfoArray* prevTime = nullptr, Streamer* debugStream = nullptr, const CalPad* padParams[3] = nullptr, const CalDet* deadMap = nullptr); @@ -129,6 +130,7 @@ inline float DigitTime::getCommonMode(const GEMstack& gemstack) const template inline void DigitTime::fillOutputContainer(std::vector& output, dataformats::MCTruthContainer& mcTruth, std::vector& commonModeOutput, const Sector& sector, TimeBin timeBin, + SAMPAProcessing& sampaProcessing, std::vector>& labelCollector, PrevDigitInfoArray* prevTime, Streamer* debugStream, const CalPad* padParams[3], const CalDet* deadMap) { @@ -167,7 +169,7 @@ inline void DigitTime::fillOutputContainer(std::vector& output, dataforma prevDigit = (*prevTime)[iPad]; } const CRU cru = mapper.getCRU(sector, iPad); - digit.fillOutputContainer(output, mcTruth, cru, timeBin, iPad, mLabels, getCommonMode(cru), prevDigit, debugStream, deadMap); + digit.fillOutputContainer(output, mcTruth, cru, timeBin, iPad, mLabels, sampaProcessing, labelCollector, getCommonMode(cru), prevDigit, debugStream, deadMap); } } } diff --git a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h index 1c76dcaaeedd3..4d88f558514d3 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h @@ -143,6 +143,7 @@ class Digitizer private: DigitContainer mDigitContainer; ///< Container for the Digits + std::vector mSignalArray; //!< Workspace for the shaped signal of one electron std::unique_ptr mSpaceCharge; ///< Handler of full distortions (static + IR dependant) std::unique_ptr mSpaceChargeDer; ///< Handler of reference static distortions Sector mSector = -1; ///< ID of the currently processed sector diff --git a/Detectors/TPC/simulation/src/DigitContainer.cxx b/Detectors/TPC/simulation/src/DigitContainer.cxx index 4c8ca8c2002db..b5103c9907574 100644 --- a/Detectors/TPC/simulation/src/DigitContainer.cxx +++ b/Detectors/TPC/simulation/src/DigitContainer.cxx @@ -48,6 +48,9 @@ void DigitContainer::fillOutputContainer(std::vector& output, updateCalibrations(); } + // after updateCalibrations, which may reload the FEE parameters that SAMPAProcessing picks up + auto& sampaProcessing = mSAMPAProcessing ? *mSAMPAProcessing : SAMPAProcessing::instance(); + const bool needsPrevDigArray = eleParam.doIonTail || eleParam.doIonTailPerPad || eleParam.doSaturationTail; const bool needsEmptyTimeBins = needsPrevDigArray || eleParam.doNoiseEmptyPads; @@ -91,34 +94,34 @@ void DigitContainer::fillOutputContainer(std::vector& output, if (time) { switch (digitizationMode) { case DigitzationMode::FullMode: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); break; } case DigitzationMode::ZeroSuppression: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); break; } case DigitzationMode::ZeroSuppressionCMCorr: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); break; } case DigitzationMode::SubtractPedestal: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); break; } case DigitzationMode::NoSaturation: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); break; } case DigitzationMode::PropagateADC: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); break; } case DigitzationMode::Auto: { if (mCMCEnabled) { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); } else { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); } break; } diff --git a/Detectors/TPC/simulation/src/Digitizer.cxx b/Detectors/TPC/simulation/src/Digitizer.cxx index 82d5c9f065b0e..c01da34158a30 100644 --- a/Detectors/TPC/simulation/src/Digitizer.cxx +++ b/Detectors/TPC/simulation/src/Digitizer.cxx @@ -65,8 +65,7 @@ void Digitizer::process(const std::vector& hits, const int nShapedPoints = eleParam.NShapedPoints; const auto amplificationMode = gemParam.AmplMode; - static std::vector signalArray; - signalArray.resize(nShapedPoints); + mSignalArray.resize(nShapedPoints); /// Reserve space in the digit container for the current event mDigitContainer.reserve(sampaProcessing.getTimeBinFromTime(mEventTime - mOutputDigitTimeOffset)); @@ -154,11 +153,11 @@ void Digitizer::process(const std::vector& hits, const GlobalPadNumber globalPad = mapper.globalPadNumber(digiPadPos.getGlobalPadPos()); const float ADCsignal = sampaProcessing.getADCvalue(static_cast(nElectronsGEM)); const MCCompLabel label(MCTrackID, eventID, sourceID, false); - sampaProcessing.getShapedSignal(ADCsignal, absoluteTime, signalArray); + sampaProcessing.getShapedSignal(ADCsignal, absoluteTime, mSignalArray); for (float i = 0; i < nShapedPoints; ++i) { const float time = absoluteTime + i * eleParam.ZbinWidth; mDigitContainer.addDigit(label, digiPadPos.getCRU(), sampaProcessing.getTimeBinFromTime(time), globalPad, - signalArray[i]); + mSignalArray[i]); } /// TODO: add ion backflow to space-charge density } From bc580961f455bc29cc41afa761ae433686ca9303 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 9 Nov 2026 10:42:58 +0100 Subject: [PATCH 02/11] Add optional per-sector seeding to the TPC digitizer This adds an option that makes the TPC digits independent of the lane assignment, as a preparation for MT digitization. - The random rings of ElectronTransport, GEMAmplification and SAMPAProcessing kept their positions from one sector to the next, so the digits depended on the number of TPC lanes. - With --tpc-sector-seeding, each Digitizer owns these three objects, fills their rings once from DigiParams.seed, and moves the rings to positions derived from the seed, the sector and the timeframe. - RandomRing can now be filled from a given generator and moved to a random position. - Without the option, the output is unchanged. https://its.cern.ch/jira/browse/O2-7309 Co-Authored-By: Claude Opus 5.5 --- .../MathUtils/include/MathUtils/RandomRing.h | 31 ++++---- .../include/TPCSimulation/Digitizer.h | 20 ++++++ .../include/TPCSimulation/ElectronTransport.h | 9 ++- .../include/TPCSimulation/GEMAmplification.h | 9 ++- .../include/TPCSimulation/SAMPAProcessing.h | 8 ++- Detectors/TPC/simulation/src/Digitizer.cxx | 72 ++++++++++++++++--- .../TPC/simulation/src/ElectronTransport.cxx | 8 ++- .../TPC/simulation/src/GEMAmplification.cxx | 27 +++++-- .../TPC/simulation/src/SAMPAProcessing.cxx | 2 +- .../src/TPCDigitizerSpec.cxx | 13 ++++ 10 files changed, 166 insertions(+), 33 deletions(-) diff --git a/Common/MathUtils/include/MathUtils/RandomRing.h b/Common/MathUtils/include/MathUtils/RandomRing.h index 59825f0a0ce09..a9915df430a37 100644 --- a/Common/MathUtils/include/MathUtils/RandomRing.h +++ b/Common/MathUtils/include/MathUtils/RandomRing.h @@ -54,7 +54,8 @@ class RandomRing /// constructor /// @param [in] randomType type of the random generator - RandomRing(const RandomType randomType = RandomType::Gaus); + /// @param [in] rng random generator to draw from, gRandom if null + RandomRing(const RandomType randomType = RandomType::Gaus, TRandom* rng = nullptr); /// constructor accepting TF1 /// @param [in] function TF1 function @@ -62,11 +63,13 @@ class RandomRing /// initialisation of the random ring /// @param [in] randomType type of the random generator - void initialize(const RandomType randomType = RandomType::Gaus); + /// @param [in] rng random generator to draw from, gRandom if null + void initialize(const RandomType randomType = RandomType::Gaus, TRandom* rng = nullptr); /// initialisation of the random ring - /// @param [in] randomType type of the random generator - void initialize(TF1& function); + /// @param [in] function TF1 function + /// @param [in] rng random generator to draw from, gRandom if null + void initialize(TF1& function, TRandom* rng = nullptr); /// initialisation of the random ring /// @param [in] randomType type of the random generator @@ -110,6 +113,10 @@ class RandomRing /// @return position in the ring buffer unsigned int getRingPosition() const { return mRingPosition; } + /// move to a random position in the ring buffer; not for rings read with getNextValueVc() + /// @param [in] rng random generator to draw the position from + void setRandomPosition(TRandom& rng) { mRingPosition = rng.Integer(N); } + private: // ========================================================================= // ===| members |=========================================================== @@ -123,12 +130,12 @@ class RandomRing //______________________________________________________________________________ template -inline RandomRing::RandomRing(const RandomType randomType) +inline RandomRing::RandomRing(const RandomType randomType, TRandom* rng) : mRandomType(randomType), mRandomNumbers() { - initialize(randomType); + initialize(randomType, rng); } //______________________________________________________________________________ @@ -142,18 +149,18 @@ inline RandomRing::RandomRing(TF1& function) //______________________________________________________________________________ template -inline void RandomRing::initialize(const RandomType randomType) +inline void RandomRing::initialize(const RandomType randomType, TRandom* rng) { - + TRandom* random = rng ? rng : gRandom; for (auto& v : mRandomNumbers) { // TODO: configurable mean and sigma switch (randomType) { case RandomType::Gaus: { - v = gRandom->Gaus(0, 1); + v = random->Gaus(0, 1); break; } case RandomType::Flat: { - v = gRandom->Rndm(); + v = random->Rndm(); break; } default: { @@ -166,11 +173,11 @@ inline void RandomRing::initialize(const RandomType randomType) //______________________________________________________________________________ template -inline void RandomRing::initialize(TF1& function) +inline void RandomRing::initialize(TF1& function, TRandom* rng) { mRandomType = RandomType::CustomTF1; for (auto& v : mRandomNumbers) { - v = function.GetRandom(); + v = function.GetRandom(rng); } } diff --git a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h index 4d88f558514d3..8b4d638904573 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h @@ -31,6 +31,8 @@ namespace tpc { class DigitContainer; +class ElectronTransport; +class GEMAmplification; template class SpaceCharge; @@ -141,7 +143,25 @@ class Digitizer /// in case of scaled distortions, the distortions can be recalculated to ensure consistent distortions and corrections void recalculateDistortions(); + /// Use digitizer-owned random rings filled from seed and positioned per sector and timeframe in init(), + /// so that the digits of a sector do not depend on which sectors were processed before + /// \param seed base seed, typically DigiParams.seed + void setUseSectorSeeding(ULong64_t seed); + + /// Set the timeframe identifier that enters the sector seeding + void setSeedingTimeframe(ULong64_t timeframe) { mSeedingTimeframe = timeframe; } + private: + ElectronTransport& electronTransport(); + GEMAmplification& gemAmplification(); + SAMPAProcessing& sampaProcessing(); + + std::unique_ptr mElectronTransport; //!< Digitizer-owned instance with sector seeding, else the singleton is used + std::unique_ptr mGEMAmplification; //!< Digitizer-owned instance with sector seeding, else the singleton is used + std::unique_ptr mSAMPAProcessing; //!< Digitizer-owned instance with sector seeding, else the singleton is used + ULong64_t mSeed = 0; //!< Base seed for sector seeding + ULong64_t mSeedingTimeframe = 0; //!< Timeframe identifier for sector seeding + bool mUseSectorSeeding = false; //!< Whether sector seeding is used DigitContainer mDigitContainer; ///< Container for the Digits std::vector mSignalArray; //!< Workspace for the shaped signal of one electron std::unique_ptr mSpaceCharge; ///< Handler of full distortions (static + IR dependant) diff --git a/Detectors/TPC/simulation/include/TPCSimulation/ElectronTransport.h b/Detectors/TPC/simulation/include/TPCSimulation/ElectronTransport.h index 46d2aaf625d81..06a7a58d01f53 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/ElectronTransport.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/ElectronTransport.h @@ -41,9 +41,16 @@ class ElectronTransport return electronTransport; } + /// Constructor + /// \param rng random generator to fill the random rings from, gRandom if null + explicit ElectronTransport(TRandom* rng = nullptr); + /// Destructor ~ElectronTransport() = default; + /// Move the random rings to new positions drawn from rng + void setRandomRingPositions(TRandom& rng); + /// Update the OCDB parameters cached in the class. To be called once per event void updateParameters(float vdrift = 0); @@ -72,8 +79,6 @@ class ElectronTransport float getDriftTime(float zPos, float signChange = 1.f) const; private: - ElectronTransport(); - /// Circular random buffer containing random values of the Gauss distribution to take into account diffusion of the /// electrons math_utils::RandomRing<> mRandomGaus; diff --git a/Detectors/TPC/simulation/include/TPCSimulation/GEMAmplification.h b/Detectors/TPC/simulation/include/TPCSimulation/GEMAmplification.h index 8dbfa21febc69..c30302644a07e 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/GEMAmplification.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/GEMAmplification.h @@ -43,9 +43,16 @@ class GEMAmplification return gemAmplification; } + /// Constructor + /// \param rng random generator to fill the random rings from, gRandom if null + explicit GEMAmplification(TRandom* rng = nullptr); + /// Destructor ~GEMAmplification() = default; + /// Move the random rings to new positions drawn from rng + void setRandomRingPositions(TRandom& rng); + /// Update the OCDB parameters cached in the class. To be called once per event void updateParameters(); @@ -90,8 +97,6 @@ class GEMAmplification int getGEMMultiplication(int nElectrons, int GEM); private: - GEMAmplification(); - /// Circular random buffer containing random Gaus values for gain fluctuation if the number of electrons is larger /// (central limit theorem) math_utils::RandomRing<> mRandomGaus; diff --git a/Detectors/TPC/simulation/include/TPCSimulation/SAMPAProcessing.h b/Detectors/TPC/simulation/include/TPCSimulation/SAMPAProcessing.h index 3f21b6723be14..8d6e7acf9d0fa 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/SAMPAProcessing.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/SAMPAProcessing.h @@ -45,9 +45,16 @@ class SAMPAProcessing static SAMPAProcessing sampaProcessing; return sampaProcessing; } + /// Constructor + /// \param rng random generator to fill the random ring from, gRandom if null + explicit SAMPAProcessing(TRandom* rng = nullptr); + /// Destructor ~SAMPAProcessing() = default; + /// Move the random ring to a new position drawn from rng + void setRandomRingPositions(TRandom& rng) { mRandomNoiseRing.setRandomPosition(rng); } + /// Update the OCDB parameters cached in the class. To be called once per event void updateParameters(float vdrift = 0); @@ -134,7 +141,6 @@ class SAMPAProcessing float getPedestalCRU(const int sector, const int globalPadInSector) const; private: - SAMPAProcessing(); const ParameterGas* mGasParam; ///< Caching of the parameter class to avoid multiple CDB calls const ParameterDetector* mDetParam; ///< Caching of the parameter class to avoid multiple CDB calls const ParameterElectronics* mEleParam; ///< Caching of the parameter class to avoid multiple CDB calls diff --git a/Detectors/TPC/simulation/src/Digitizer.cxx b/Detectors/TPC/simulation/src/Digitizer.cxx index c01da34158a30..2febcfc8abbbe 100644 --- a/Detectors/TPC/simulation/src/Digitizer.cxx +++ b/Detectors/TPC/simulation/src/Digitizer.cxx @@ -30,6 +30,7 @@ #include "TPCCalibration/CorrMapParam.h" #include +#include ClassImp(o2::tpc::Digitizer); @@ -39,13 +40,68 @@ Digitizer::~Digitizer() = default; Digitizer::Digitizer() = default; +namespace +{ +// splitmix64 finaliser, to derive well separated seeds from the base seed +ULong64_t mixSeed(ULong64_t x) +{ + x += 0x9e3779b97f4a7c15ULL; + x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL; + x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL; + return x ^ (x >> 31); +} + +// TRandom3 takes 32 bits of the seed and treats 0 as a request for a random seed +UInt_t toTRandom3Seed(ULong64_t x) +{ + const UInt_t seed = static_cast(x ^ (x >> 32)); + return seed ? seed : 1; +} +} // namespace + +ElectronTransport& Digitizer::electronTransport() +{ + return mElectronTransport ? *mElectronTransport : ElectronTransport::instance(); +} + +GEMAmplification& Digitizer::gemAmplification() +{ + return mGEMAmplification ? *mGEMAmplification : GEMAmplification::instance(); +} + +SAMPAProcessing& Digitizer::sampaProcessing() +{ + return mSAMPAProcessing ? *mSAMPAProcessing : SAMPAProcessing::instance(); +} + +void Digitizer::setUseSectorSeeding(ULong64_t seed) +{ + mUseSectorSeeding = true; + mSeed = seed; +} + void Digitizer::init() { - auto& gemAmplification = GEMAmplification::instance(); + if (mUseSectorSeeding) { + if (!mGEMAmplification) { + // the rings are filled once, in the construction order of the singletons + TRandom3 rng(toTRandom3Seed(mixSeed(mSeed))); + mGEMAmplification = std::make_unique(&rng); + mElectronTransport = std::make_unique(&rng); + mSAMPAProcessing = std::make_unique(&rng); + mDigitContainer.setSAMPAProcessing(mSAMPAProcessing.get()); + } + const ULong64_t sectorSeed = mixSeed(mSeed ^ mixSeed(static_cast(mSector) + 1)); + TRandom3 rng(toTRandom3Seed(mixSeed(sectorSeed ^ mixSeed(mSeedingTimeframe)))); + mGEMAmplification->setRandomRingPositions(rng); + mElectronTransport->setRandomRingPositions(rng); + mSAMPAProcessing->setRandomRingPositions(rng); + } + auto& gemAmplification = this->gemAmplification(); gemAmplification.updateParameters(); - auto& electronTransport = ElectronTransport::instance(); + auto& electronTransport = this->electronTransport(); electronTransport.updateParameters(mVDrift); - auto& sampaProcessing = SAMPAProcessing::instance(); + auto& sampaProcessing = this->sampaProcessing(); // the calibration update may reload FEE parameters from file, so SAMPAProcessing picks up its pointers afterwards mDigitContainer.updateCalibrations(); sampaProcessing.updateParameters(mVDrift); @@ -59,9 +115,9 @@ void Digitizer::process(const std::vector& hits, auto& eleParam = ParameterElectronics::Instance(); auto& gemParam = ParameterGEM::Instance(); - auto& gemAmplification = GEMAmplification::instance(); - auto& electronTransport = ElectronTransport::instance(); - auto& sampaProcessing = SAMPAProcessing::instance(); + auto& gemAmplification = this->gemAmplification(); + auto& electronTransport = this->electronTransport(); + auto& sampaProcessing = this->sampaProcessing(); const int nShapedPoints = eleParam.NShapedPoints; const auto amplificationMode = gemParam.AmplMode; @@ -171,7 +227,7 @@ void Digitizer::flush(std::vector& digits, std::vector& commonModeOutput, bool finalFlush) { - SAMPAProcessing& sampaProcessing = SAMPAProcessing::instance(); + SAMPAProcessing& sampaProcessing = this->sampaProcessing(); mDigitContainer.fillOutputContainer(digits, labels, commonModeOutput, mSector, sampaProcessing.getTimeBinFromTime(mEventTime - mOutputDigitTimeOffset), mIsContinuous, finalFlush); // flushing debug output to file if (((finalFlush && mIsContinuous) || (!mIsContinuous)) && mSpaceCharge) { @@ -227,7 +283,7 @@ void Digitizer::setStartTime(double time) // this is setting the first timebin index for the digit container // note that negative times w.r.t start of timeframe/data-taking == mOutputDigitTimeOffset // will yield the 0-th bin (due to casting logic in sampaProcessing) - SAMPAProcessing& sampaProcessing = SAMPAProcessing::instance(); + SAMPAProcessing& sampaProcessing = this->sampaProcessing(); sampaProcessing.updateParameters(mVDrift); const auto timediff = time - mOutputDigitTimeOffset; const auto starttimebin = sampaProcessing.getTimeBinFromTime(timediff); diff --git a/Detectors/TPC/simulation/src/ElectronTransport.cxx b/Detectors/TPC/simulation/src/ElectronTransport.cxx index f9e36aa642158..e9afbe81fba3e 100644 --- a/Detectors/TPC/simulation/src/ElectronTransport.cxx +++ b/Detectors/TPC/simulation/src/ElectronTransport.cxx @@ -21,11 +21,17 @@ using namespace o2::tpc; using namespace o2::math_utils; -ElectronTransport::ElectronTransport() : mRandomGaus(), mRandomFlat(RandomRing<>::RandomType::Flat) +ElectronTransport::ElectronTransport(TRandom* rng) : mRandomGaus(RandomRing<>::RandomType::Gaus, rng), mRandomFlat(RandomRing<>::RandomType::Flat, rng) { updateParameters(); } +void ElectronTransport::setRandomRingPositions(TRandom& rng) +{ + mRandomGaus.setRandomPosition(rng); + mRandomFlat.setRandomPosition(rng); +} + void ElectronTransport::updateParameters(float vdrift) { mGasParam = &(ParameterGas::Instance()); diff --git a/Detectors/TPC/simulation/src/GEMAmplification.cxx b/Detectors/TPC/simulation/src/GEMAmplification.cxx index 8d47464e9ef53..0dadc00ee1f95 100644 --- a/Detectors/TPC/simulation/src/GEMAmplification.cxx +++ b/Detectors/TPC/simulation/src/GEMAmplification.cxx @@ -26,10 +26,15 @@ using namespace o2::tpc; using namespace o2::math_utils; using boost::format; -GEMAmplification::GEMAmplification() - : mRandomGaus(), - mRandomFlat(RandomRing<>::RandomType::Flat), - mGain() +GEMAmplification::GEMAmplification(TRandom* rng) + : mRandomGaus(RandomRing<>::RandomType::Gaus, rng), + mRandomFlat(RandomRing<>::RandomType::Flat, rng), + // the gain rings are refilled from the Polya distributions below; with an explicit generator they start empty + mGain{RandomRing<>(rng ? RandomRing<>::RandomType::CustomTF1 : RandomRing<>::RandomType::Gaus), + RandomRing<>(rng ? RandomRing<>::RandomType::CustomTF1 : RandomRing<>::RandomType::Gaus), + RandomRing<>(rng ? RandomRing<>::RandomType::CustomTF1 : RandomRing<>::RandomType::Gaus), + RandomRing<>(rng ? RandomRing<>::RandomType::CustomTF1 : RandomRing<>::RandomType::Gaus)}, + mGainFullStack(rng ? RandomRing<>::RandomType::CustomTF1 : RandomRing<>::RandomType::Gaus) { updateParameters(); @@ -65,7 +70,7 @@ GEMAmplification::GEMAmplification() polyaDistribution = (o2::math_utils::CachingTF1*)outfile->Get(TString::Format("func%d", i).Data()); // FIXME: verify that distribution corresponds to the parameters used here } - mGain[i].initialize(*polyaDistribution); + mGain[i].initialize(*polyaDistribution, rng); if (!cacheexists) { outfile->WriteTObject(polyaDistribution, TString::Format("func%d", i).Data()); @@ -88,7 +93,7 @@ GEMAmplification::GEMAmplification() } else { polyaDistribution = (o2::math_utils::CachingTF1*)outfile->Get("polyaStack"); } - mGainFullStack.initialize(*polyaDistribution); + mGainFullStack.initialize(*polyaDistribution, rng); if (!cacheexists) { outfile->WriteTObject(polyaDistribution, "polyaStack"); @@ -102,6 +107,16 @@ GEMAmplification::GEMAmplification() LOG(info) << "TPC: GEM setup (polya) took " << watch.CpuTime(); } +void GEMAmplification::setRandomRingPositions(TRandom& rng) +{ + mRandomGaus.setRandomPosition(rng); + mRandomFlat.setRandomPosition(rng); + for (auto& gain : mGain) { + gain.setRandomPosition(rng); + } + mGainFullStack.setRandomPosition(rng); +} + void GEMAmplification::updateParameters() { auto& cdb = CDBInterface::instance(); diff --git a/Detectors/TPC/simulation/src/SAMPAProcessing.cxx b/Detectors/TPC/simulation/src/SAMPAProcessing.cxx index 462008846fa04..bee8b02f06e8f 100644 --- a/Detectors/TPC/simulation/src/SAMPAProcessing.cxx +++ b/Detectors/TPC/simulation/src/SAMPAProcessing.cxx @@ -24,7 +24,7 @@ using namespace o2::tpc; -SAMPAProcessing::SAMPAProcessing() : mRandomNoiseRing() +SAMPAProcessing::SAMPAProcessing(TRandom* rng) : mRandomNoiseRing(o2::math_utils::RandomRing<>::RandomType::Gaus, rng) { updateParameters(); } diff --git a/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx b/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx index 8b0df106d81cc..a5d345dceb49c 100644 --- a/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx +++ b/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx @@ -44,6 +44,7 @@ #include "TPCCalibration/VDriftHelper.h" #include "CommonDataFormat/RangeReference.h" #include "SimConfig/DigiParams.h" +#include #include #include "Framework/CCDBParamSpec.h" @@ -133,6 +134,16 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer mDigitizer.setContinuousReadout(!triggeredMode); mDigitizer.setDistortionScaleType(mDistortionType); + if (ic.options().get("tpc-sector-seeding")) { + const auto seed = o2::conf::DigiParams::Instance().seed; + if (seed == 0) { + LOG(warning) << "TPC sector seeding with DigiParams.seed=0 is not reproducible"; + } + // with seed 0, gRandom was seeded randomly in the base class + mDigitizer.setUseSectorSeeding(seed != 0 ? seed : gRandom->Integer(kMaxUInt)); + LOG(info) << "TPC: using sector seeding"; + } + // we send the GRP data once if the corresponding output channel is available // and set the flag to false after mWriteGRP = true; @@ -381,6 +392,7 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer } mDigitizer.setSector(sector); + mDigitizer.setSeedingTimeframe(dh->tfCounter); mDigitizer.init(); auto& eventParts = context->getEventParts(); @@ -536,6 +548,7 @@ o2::framework::DataProcessorSpec getTPCDigitizerSpec(int channel, bool writeGRP, {"meanLumiDistortionsDerivative", VariantType::Float, -1.f, {"override lumi of derivative distortion object if >=0"}}, {"do-not-recalculate-distortions", VariantType::Bool, false, {"Do not recalculate the distortions"}}, {"n-threads-distortions", VariantType::Int, 4, {"Number of threads used for the calculation of the distortions"}}, + {"tpc-sector-seeding", VariantType::Bool, false, {"Seed the random numbers of each sector from DigiParams.seed and the sector, so that the digits do not depend on the lane assignment"}}, }}; } From b7377ff3c846fb4d51fcc223377c722fbaed5b24 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 9 Nov 2026 10:48:43 +0100 Subject: [PATCH 03/11] Fix formatting of the TPC Digitizer members --- Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h index 8b4d638904573..0c38d6ff6d344 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h @@ -162,6 +162,7 @@ class Digitizer ULong64_t mSeed = 0; //!< Base seed for sector seeding ULong64_t mSeedingTimeframe = 0; //!< Timeframe identifier for sector seeding bool mUseSectorSeeding = false; //!< Whether sector seeding is used + DigitContainer mDigitContainer; ///< Container for the Digits std::vector mSignalArray; //!< Workspace for the shaped signal of one electron std::unique_ptr mSpaceCharge; ///< Handler of full distortions (static + IR dependant) From 335355cad8a8de47b667b02bf4784c818b5834e8 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 9 Nov 2026 11:13:30 +0100 Subject: [PATCH 04/11] Move the TPC per-sector digitization loop into a shared function This moves the per-sector loop of the TPC digitizer spec into a free function, as a preparation for MT digitization. - digitizeSector runs the collision loop and the flushes of one sector and returns the digit groups. - The output of each flush is handed to a callback, so the function makes no DPL calls. - The lane spec now calls it; the chunked writer and the DPL outputs stay in the spec. - The output is unchanged. https://its.cern.ch/jira/browse/O2-7309 Co-Authored-By: Claude Opus 5.5 --- Steer/DigitizerWorkflow/CMakeLists.txt | 1 + .../src/TPCDigitizerSpec.cxx | 108 ++-------------- .../src/TPCSectorDigitization.cxx | 120 ++++++++++++++++++ .../src/TPCSectorDigitization.h | 55 ++++++++ 4 files changed, 189 insertions(+), 95 deletions(-) create mode 100644 Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx create mode 100644 Steer/DigitizerWorkflow/src/TPCSectorDigitization.h diff --git a/Steer/DigitizerWorkflow/CMakeLists.txt b/Steer/DigitizerWorkflow/CMakeLists.txt index 9a4ac7c7fb8f4..0f272769f170c 100644 --- a/Steer/DigitizerWorkflow/CMakeLists.txt +++ b/Steer/DigitizerWorkflow/CMakeLists.txt @@ -25,6 +25,7 @@ o2_add_executable(digitizer-workflow src/SimReaderSpec.cxx src/SimpleDigitizerWorkflow.cxx src/TPCDigitizerSpec.cxx + src/TPCSectorDigitization.cxx src/ZDCDigitizerSpec.cxx src/TOFDigitizerSpec.cxx $<$:src/ITS3DigitizerSpec.cxx> diff --git a/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx b/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx index a5d345dceb49c..482e85598d34e 100644 --- a/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx +++ b/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx @@ -15,6 +15,7 @@ #include #include "Framework/RootSerializationSupport.h" #include "TPCDigitizerSpec.h" +#include "TPCSectorDigitization.h" #include "Framework/ControlService.h" #include "Framework/ParallelContext.h" #include "Framework/ConfigParamRegistry.h" @@ -22,7 +23,6 @@ #include "Framework/DataRefUtils.h" #include "Framework/Lifetime.h" #include "Framework/DeviceSpec.h" -#include "DetectorsRaw/HBFUtils.h" #include "Headers/DataHeader.h" #include "TStopwatch.h" #include "Steer/HitProcessingManager.h" // for DigitizationContext @@ -92,20 +92,6 @@ void writeToBranchHelper>(TTr targetbr->DropBaskets("all"); } -std::string getBranchNameLeft(int sector) -{ - std::stringstream branchnamestreamleft; - branchnamestreamleft << "TPCHitsShiftedSector" << int(o2::tpc::Sector::getLeft(o2::tpc::Sector(sector))); - return branchnamestreamleft.str(); -} - -std::string getBranchNameRight(int sector) -{ - std::stringstream branchnamestreamright; - branchnamestreamright << "TPCHitsShiftedSector" << sector; - return branchnamestreamright.str(); -} - using namespace o2::base; class TPCDPLDigitizerTask : public BaseDPLDigitizer { @@ -178,7 +164,7 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer { std::stringstream brname; brname << "TPCDigit_" << mSector; - auto br = o2::base::getOrMakeBranch(*mInternalROOTFlushTTree, brname.str().c_str(), &mDigits); + auto br = o2::base::getOrMakeBranch(*mInternalROOTFlushTTree, brname.str().c_str(), &mFlushBuffers.digits); br->Fill(); br->ResetAddress(); } @@ -186,7 +172,7 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer // labels std::stringstream brname; brname << "TPCDigitMCTruth_" << mSector; - auto br = o2::base::getOrMakeBranch(*mInternalROOTFlushTTree, brname.str().c_str(), &mLabels); + auto br = o2::base::getOrMakeBranch(*mInternalROOTFlushTTree, brname.str().c_str(), &mFlushBuffers.labels); br->Fill(); br->ResetAddress(); } @@ -194,7 +180,7 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer // common std::stringstream brname; brname << "TPCCommonMode_" << mSector; - auto br = o2::base::getOrMakeBranch(*mInternalROOTFlushTTree, brname.str().c_str(), &mCommonMode); + auto br = o2::base::getOrMakeBranch(*mInternalROOTFlushTTree, brname.str().c_str(), &mFlushBuffers.commonMode); br->Fill(); br->ResetAddress(); } @@ -286,7 +272,6 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer } // TODO: make generic reset method? mFlushCounter = 0; - mDigitCounter = 0; } } } @@ -391,88 +376,24 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer throw std::runtime_error("Digitizer can only work on single sectors"); } - mDigitizer.setSector(sector); - mDigitizer.setSeedingTimeframe(dh->tfCounter); - mDigitizer.init(); - - auto& eventParts = context->getEventParts(); + TStopwatch timer; + timer.Start(); - auto flushDigitsAndLabels = [this, digitsAccum, &labelAccum, &commonModeAccum](bool finalFlush = false) { + auto onFlush = [this, digitsAccum, &labelAccum, &commonModeAccum]() { mFlushCounter++; - // flush previous buffer - mDigits.clear(); - mLabels.clear(); - mCommonMode.clear(); - mDigitizer.flush(mDigits, mLabels, mCommonMode, finalFlush); - LOG(info) << "TPC: Flushed " << mDigits.size() << " digits, " << mLabels.getNElements() << " labels and " << mCommonMode.size() << " common mode entries"; - if (mInternalWriter) { // the natural place to write out this independent datachunk immediately ... writeToROOTFile(); } else { // ... or to accumulate and later forward to next DPL proc - std::copy(mDigits.begin(), mDigits.end(), std::back_inserter(*digitsAccum)); + std::copy(mFlushBuffers.digits.begin(), mFlushBuffers.digits.end(), std::back_inserter(*digitsAccum)); if (mWithMCTruth) { - labelAccum.mergeAtBack(mLabels); + labelAccum.mergeAtBack(mFlushBuffers.labels); } - std::copy(mCommonMode.begin(), mCommonMode.end(), std::back_inserter(commonModeAccum)); + std::copy(mFlushBuffers.commonMode.begin(), mFlushBuffers.commonMode.end(), std::back_inserter(commonModeAccum)); } - mDigitCounter += mDigits.size(); }; - - if (isContinuous) { - auto& hbfu = o2::raw::HBFUtils::Instance(); - double time = hbfu.getFirstIRofTF(o2::InteractionRecord(0, hbfu.orbitFirstSampled)).bc2ns() / 1000.; - mDigitizer.setOutputDigitTimeOffset(time); - if (!irecords.empty()) { - mDigitizer.setStartTime(irecords[0].getTimeNS() / 1000.f); - } - } - - TStopwatch timer; - timer.Start(); - - // loop over all composite collisions given from context - // (aka loop over all the interaction records) - for (int collID = 0; collID < irecords.size(); ++collID) { - const double eventTime = irecords[collID].getTimeNS() / 1000.f; - LOG(info) << "TPC: Event time " << eventTime << " us"; - mDigitizer.setEventTime(eventTime); - if (!isContinuous) { - mDigitizer.setStartTime(eventTime); - } - size_t startSize = mDigitCounter; // digitsAccum->size(); - - // for each collision, loop over the constituents event and source IDs - // (background signal merging is basically taking place here) - for (auto& part : eventParts[collID]) { - const int eventID = part.entryID; - const int sourceID = part.sourceID; - - // get the hits for this event and this source - std::vector hitsLeft; - std::vector hitsRight; - context->retrieveHits(mSimChains, getBranchNameLeft(sector).c_str(), part.sourceID, part.entryID, &hitsLeft); - context->retrieveHits(mSimChains, getBranchNameRight(sector).c_str(), part.sourceID, part.entryID, &hitsRight); - LOG(debug) << "TPC: Found " << hitsLeft.size() << " hit groups left and " << hitsRight.size() << " hit groups right in collision " << collID << " eventID " << part.entryID; - - mDigitizer.process(hitsLeft, eventID, sourceID); - mDigitizer.process(hitsRight, eventID, sourceID); - - flushDigitsAndLabels(); - - if (!isContinuous) { - eventAccum.emplace_back(startSize, mDigits.size()); - } - } - } - - // final flushing step; getting everything not yet written out - if (isContinuous) { - LOG(info) << "TPC: Final flush"; - flushDigitsAndLabels(true); - eventAccum.emplace_back(0, mDigitCounter); // all digits are grouped to 1 super-event pseudo-triggered mode - } + eventAccum = digitizeSector(mDigitizer, *context, mSimChains, sector, dh->tfCounter, mFlushBuffers, onFlush); if (!mInternalWriter) { // send out to next stage @@ -490,13 +411,10 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer o2::tpc::Digitizer mDigitizer; o2::tpc::VDriftHelper mTPCVDriftHelper{}; std::vector mSimChains; - std::vector mDigits; - o2::dataformats::MCTruthContainer mLabels; - std::vector mCommonMode; - std::vector mListOfSectors; // a list of sectors treated by this task + SectorFlushBuffers mFlushBuffers; // output of the latest flush + std::vector mListOfSectors; // a list of sectors treated by this task TFile* mInternalROOTFlushFile = nullptr; TTree* mInternalROOTFlushTTree = nullptr; - size_t mDigitCounter = 0; size_t mFlushCounter = 0; int mLaneId = 0; // the id of the current process within the parallel pipeline int mSector = 0; diff --git a/Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx new file mode 100644 index 0000000000000..487c9b89c0302 --- /dev/null +++ b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx @@ -0,0 +1,120 @@ +// Copyright 2019-2020 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 "TPCSectorDigitization.h" +#include +#include +#include +#include "DetectorsRaw/HBFUtils.h" +#include "Framework/Logger.h" +#include "SimulationDataFormat/DigitizationContext.h" +#include "TPCBase/Sector.h" +#include "TPCSimulation/Point.h" +#include "TPCSimulation/Digitizer.h" + +namespace o2::tpc +{ + +namespace +{ +std::string getBranchNameLeft(int sector) +{ + std::stringstream branchnamestreamleft; + branchnamestreamleft << "TPCHitsShiftedSector" << int(o2::tpc::Sector::getLeft(o2::tpc::Sector(sector))); + return branchnamestreamleft.str(); +} + +std::string getBranchNameRight(int sector) +{ + std::stringstream branchnamestreamright; + branchnamestreamright << "TPCHitsShiftedSector" << sector; + return branchnamestreamright.str(); +} +} // namespace + +std::vector> digitizeSector(Digitizer& digitizer, const steer::DigitizationContext& context, + const std::vector& simChains, int sector, ULong64_t timeframe, + SectorFlushBuffers& buffers, const std::function& onFlush) +{ + std::vector> events; + const bool isContinuous = digitizer.isContinuousReadout(); + const auto& irecords = context.getEventRecords(); + const auto& eventParts = context.getEventParts(); + + digitizer.setSector(sector); + digitizer.setSeedingTimeframe(timeframe); + digitizer.init(); + + size_t digitCounter = 0; + auto flush = [&](bool finalFlush = false) { + buffers.digits.clear(); + buffers.labels.clear(); + buffers.commonMode.clear(); + digitizer.flush(buffers.digits, buffers.labels, buffers.commonMode, finalFlush); + LOG(info) << "TPC: Flushed " << buffers.digits.size() << " digits, " << buffers.labels.getNElements() << " labels and " << buffers.commonMode.size() << " common mode entries"; + onFlush(); + digitCounter += buffers.digits.size(); + }; + + if (isContinuous) { + auto& hbfu = o2::raw::HBFUtils::Instance(); + double time = hbfu.getFirstIRofTF(o2::InteractionRecord(0, hbfu.orbitFirstSampled)).bc2ns() / 1000.; + digitizer.setOutputDigitTimeOffset(time); + if (!irecords.empty()) { + digitizer.setStartTime(irecords[0].getTimeNS() / 1000.f); + } + } + + // loop over all composite collisions given from context + // (aka loop over all the interaction records) + for (int collID = 0; collID < irecords.size(); ++collID) { + const double eventTime = irecords[collID].getTimeNS() / 1000.f; + LOG(info) << "TPC: Event time " << eventTime << " us"; + digitizer.setEventTime(eventTime); + if (!isContinuous) { + digitizer.setStartTime(eventTime); + } + size_t startSize = digitCounter; + + // for each collision, loop over the constituents event and source IDs + // (background signal merging is basically taking place here) + for (auto& part : eventParts[collID]) { + const int eventID = part.entryID; + const int sourceID = part.sourceID; + + // get the hits for this event and this source + std::vector hitsLeft; + std::vector hitsRight; + context.retrieveHits(simChains, getBranchNameLeft(sector).c_str(), part.sourceID, part.entryID, &hitsLeft); + context.retrieveHits(simChains, getBranchNameRight(sector).c_str(), part.sourceID, part.entryID, &hitsRight); + LOG(debug) << "TPC: Found " << hitsLeft.size() << " hit groups left and " << hitsRight.size() << " hit groups right in collision " << collID << " eventID " << part.entryID; + + digitizer.process(hitsLeft, eventID, sourceID); + digitizer.process(hitsRight, eventID, sourceID); + + flush(); + + if (!isContinuous) { + events.emplace_back(startSize, buffers.digits.size()); + } + } + } + + // final flushing step; getting everything not yet written out + if (isContinuous) { + LOG(info) << "TPC: Final flush"; + flush(true); + events.emplace_back(0, digitCounter); // all digits are grouped to 1 super-event pseudo-triggered mode + } + return events; +} + +} // namespace o2::tpc diff --git a/Steer/DigitizerWorkflow/src/TPCSectorDigitization.h b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.h new file mode 100644 index 0000000000000..dc8680526687e --- /dev/null +++ b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.h @@ -0,0 +1,55 @@ +// Copyright 2019-2020 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 STEER_DIGITIZERWORKFLOW_TPCSECTORDIGITIZATION_H_ +#define STEER_DIGITIZERWORKFLOW_TPCSECTORDIGITIZATION_H_ + +#include +#include +#include +#include "CommonDataFormat/RangeReference.h" +#include "DataFormatsTPC/Digit.h" +#include "SimulationDataFormat/MCCompLabel.h" +#include "SimulationDataFormat/MCTruthContainer.h" +#include "TPCSimulation/CommonMode.h" + +class TChain; + +namespace o2 +{ +namespace steer +{ +class DigitizationContext; +} +namespace tpc +{ +class Digitizer; + +/// Output buffers of one flush of a sector +struct SectorFlushBuffers { + std::vector digits; + dataformats::MCTruthContainer labels; + std::vector commonMode; +}; + +/// Digitize all collisions of one sector of a timeframe. +/// After each flush the new output is in buffers and onFlush is called on the calling thread. +/// The function makes no DPL calls. Concurrent calls need one Digitizer with sector seeding and one set of chains +/// per thread; Digitizer::init() is called here and is not thread-safe across Digitizer instances. +/// \return the digit groups, one per collision part in triggered mode, one for the timeframe in continuous mode +std::vector> digitizeSector(Digitizer& digitizer, const steer::DigitizationContext& context, + const std::vector& simChains, int sector, ULong64_t timeframe, + SectorFlushBuffers& buffers, const std::function& onFlush); + +} // namespace tpc +} // namespace o2 + +#endif From 560908a637a7fd5268cec6eb2555aa4f95878683 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 9 Nov 2026 01:07:29 +0100 Subject: [PATCH 05/11] Do not take ownership of CCDB distortion maps in the TPC digitizer This fixes a double ownership of the TPC space-charge maps and prepares sharing them between digitizers. - setUseSCDistortions(SC*) and setSCDistortionsDerivative(SC*) took ownership of objects that the DPL CCDB cache owns and deletes itself when a newer object replaces them. - The Digitizer now owns only the maps it creates itself. - The map members are transient for ROOT I/O. https://its.cern.ch/jira/browse/O2-7309 Co-Authored-By: Claude Opus 5.5 --- .../include/TPCSimulation/Digitizer.h | 35 ++++++++++--------- Detectors/TPC/simulation/src/Digitizer.cxx | 17 +++++---- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h index 0c38d6ff6d344..4d2fc765116c6 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h @@ -120,10 +120,10 @@ class Digitizer /// \param nRBins number of grid points in r, must be (2**N)+1 void setUseSCDistortions(const SCDistortionType& distortionType, const TH3* hisInitialSCDensity); /// Enable the use of space-charge distortions and provide SpaceCharge object as input - /// \param spaceCharge unique pointer to spaceCharge object + /// \param spaceCharge spaceCharge object, not owned: the caller (e.g. the DPL CCDB cache) keeps it alive void setUseSCDistortions(SC* spaceCharge); - /// \param spaceCharge unique pointer to spaceCharge object + /// \param spaceCharge spaceCharge object of the derivative map, not owned: the caller keeps it alive void setSCDistortionsDerivative(SC* spaceCharge); /// Enable the use of space-charge distortions by providing global distortions and global corrections stored in a ROOT file @@ -163,21 +163,22 @@ class Digitizer ULong64_t mSeedingTimeframe = 0; //!< Timeframe identifier for sector seeding bool mUseSectorSeeding = false; //!< Whether sector seeding is used - DigitContainer mDigitContainer; ///< Container for the Digits - std::vector mSignalArray; //!< Workspace for the shaped signal of one electron - std::unique_ptr mSpaceCharge; ///< Handler of full distortions (static + IR dependant) - std::unique_ptr mSpaceChargeDer; ///< Handler of reference static distortions - Sector mSector = -1; ///< ID of the currently processed sector - double mEventTime = 0.f; ///< Time of the currently processed event - double mOutputDigitTimeOffset = 0; ///< Time of the first IR sampled in the digitizer - float mVDrift = 0; ///< VDrift for current timestamp - float mTDriftOffset = 0; ///< drift time additive offset in \mus - bool mIsContinuous; ///< Switch for continuous readout - bool mUseSCDistortions = false; ///< Flag to switch on the use of space-charge distortions - int mDistortionScaleType = 0; ///< type=0: no scaling of distortions, type=1 distortions without any scaling, type=2 distortions scaling with lumi - float mLumiScaleFactor = 0; ///< value used to scale the derivative map - bool mUseScaledDistortions = false; ///< whether the distortions are already scaled - ClassDefNV(Digitizer, 3); + DigitContainer mDigitContainer; ///< Container for the Digits + std::vector mSignalArray; //!< Workspace for the shaped signal of one electron + std::unique_ptr mSpaceChargeOwned; //!< Distortions created by the digitizer itself + SC* mSpaceCharge = nullptr; //!< Handler of full distortions (static + IR dependant), owned by mSpaceChargeOwned or by the caller + SC* mSpaceChargeDer = nullptr; //!< Handler of reference static distortions, owned by the caller + Sector mSector = -1; ///< ID of the currently processed sector + double mEventTime = 0.f; ///< Time of the currently processed event + double mOutputDigitTimeOffset = 0; ///< Time of the first IR sampled in the digitizer + float mVDrift = 0; ///< VDrift for current timestamp + float mTDriftOffset = 0; ///< drift time additive offset in \mus + bool mIsContinuous; ///< Switch for continuous readout + bool mUseSCDistortions = false; ///< Flag to switch on the use of space-charge distortions + int mDistortionScaleType = 0; ///< type=0: no scaling of distortions, type=1 distortions without any scaling, type=2 distortions scaling with lumi + float mLumiScaleFactor = 0; ///< value used to scale the derivative map + bool mUseScaledDistortions = false; ///< whether the distortions are already scaled + ClassDefNV(Digitizer, 4); }; } // namespace tpc } // namespace o2 diff --git a/Detectors/TPC/simulation/src/Digitizer.cxx b/Detectors/TPC/simulation/src/Digitizer.cxx index 2febcfc8abbbe..7bd78c97b3c20 100644 --- a/Detectors/TPC/simulation/src/Digitizer.cxx +++ b/Detectors/TPC/simulation/src/Digitizer.cxx @@ -140,7 +140,7 @@ void Digitizer::process(const std::vector& hits, if (mDistortionScaleType == 1) { mSpaceCharge->distortElectron(posEle); } else if (mDistortionScaleType == 2) { - mSpaceCharge->distortElectron(posEle, (mUseScaledDistortions ? nullptr : mSpaceChargeDer.get()), mLumiScaleFactor); + mSpaceCharge->distortElectron(posEle, (mUseScaledDistortions ? nullptr : mSpaceChargeDer), mLumiScaleFactor); } /// Remove electrons that end up more than three sigma of the hit's average diffusion away from the current sector @@ -239,7 +239,8 @@ void Digitizer::setUseSCDistortions(const SCDistortionType& distortionType, cons { mUseSCDistortions = true; if (!mSpaceCharge) { - mSpaceCharge = std::make_unique(); + mSpaceChargeOwned = std::make_unique(); + mSpaceCharge = mSpaceChargeOwned.get(); } mSpaceCharge->setSCDistortionType(distortionType); if (hisInitialSCDensity) { @@ -251,14 +252,17 @@ void Digitizer::setUseSCDistortions(const SCDistortionType& distortionType, cons void Digitizer::setUseSCDistortions(SC* spaceCharge) { mUseSCDistortions = true; - mSpaceCharge.reset(spaceCharge); + if (spaceCharge != mSpaceChargeOwned.get()) { + mSpaceChargeOwned.reset(); + } + mSpaceCharge = spaceCharge; mSpaceCharge->initAfterReadingFromFile(); mSpaceCharge->printMetaData(); } void Digitizer::setSCDistortionsDerivative(SC* spaceCharge) { - mSpaceChargeDer.reset(spaceCharge); + mSpaceChargeDer = spaceCharge; mSpaceChargeDer->initAfterReadingFromFile(); mSpaceChargeDer->printMetaData(); } @@ -267,7 +271,8 @@ void Digitizer::setUseSCDistortions(std::string_view finp) { mUseSCDistortions = true; if (!mSpaceCharge) { - mSpaceCharge = std::make_unique(); + mSpaceChargeOwned = std::make_unique(); + mSpaceCharge = mSpaceChargeOwned.get(); } // in case analytical distortions are loaded from file they are applied @@ -325,7 +330,7 @@ void Digitizer::recalculateDistortions() mSpaceChargeDer->calcGlobalCorrWithGlobalDistIterative(side, nullptr, 0); LOGP(info, "Calculating scaled distortions with scaling factor {}", mLumiScaleFactor); - mSpaceCharge->calcGlobalDistWithGlobalCorrIterativeLinearCartesian(side, mSpaceChargeDer.get(), mLumiScaleFactor); + mSpaceCharge->calcGlobalDistWithGlobalCorrIterativeLinearCartesian(side, mSpaceChargeDer, mLumiScaleFactor); } // set new lumi of avg map mSpaceCharge->setMeanLumi(CorrMapParam::Instance().lumiInst); From afe6d97da28f89b00cf00fa5b6e66d5f5ff42e33 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 9 Nov 2026 12:25:54 +0100 Subject: [PATCH 06/11] Prepare the TPC digitizer for concurrent digitizers This makes several TPC digitizers usable side by side in one process, as a preparation for MT digitization. - Each DigitContainer keeps its own copy of the dead channel map, because CDBInterface::getDeadChannelMap() rebuilds the shared map on every call. - The one-time settings report moves from the flush path into updateCalibrations(). - Digitizer::setSCDistortionsFrom() shares the distortion maps and their scaling with another digitizer. - digitizeSector() takes an optional mutex for the calls that read calibrations through the CDBInterface. - The output is unchanged. https://its.cern.ch/jira/browse/O2-7309 Co-Authored-By: Claude Opus 5.5 --- .../include/TPCSimulation/DigitContainer.h | 3 ++- .../include/TPCSimulation/Digitizer.h | 3 +++ .../TPC/simulation/src/DigitContainer.cxx | 25 +++++++++++-------- Detectors/TPC/simulation/src/Digitizer.cxx | 10 ++++++++ .../src/TPCSectorDigitization.cxx | 13 ++++++++-- .../src/TPCSectorDigitization.h | 6 +++-- 6 files changed, 45 insertions(+), 15 deletions(-) diff --git a/Detectors/TPC/simulation/include/TPCSimulation/DigitContainer.h b/Detectors/TPC/simulation/include/TPCSimulation/DigitContainer.h index c44bababfb98e..ed47188602ac1 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/DigitContainer.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/DigitContainer.h @@ -104,7 +104,8 @@ class DigitContainer std::unique_ptr mPrevDigArr; ///< Keep track of ToT and ion tail cumul from last time bin o2::utils::DebugStreamer mStreamer; ///< Debug streamer const CalPad* mPadParams[3] = {nullptr, nullptr, nullptr}; //!< Ion tail fraction, ion tail slope and common mode k-values per pad - const CalDet* mDeadMap = nullptr; //!< Dead channel map + const CalDet* mDeadMap = nullptr; //!< Dead channel map, points to mDeadMapCopy if set + std::unique_ptr> mDeadMapCopy; //!< Own copy of the dead channel map, which the CDBInterface rebuilds on each access bool mCMCEnabled = false; //!< Common mode correction enabled in the FEE config (DigiMode Auto) bool mCalibrationsUpdated = false; //!< Whether updateCalibrations was called SAMPAProcessing* mSAMPAProcessing = nullptr; //!< SAMPAProcessing used for the output, the singleton if not set diff --git a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h index 4d2fc765116c6..1f1c68aac3c20 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h @@ -143,6 +143,9 @@ class Digitizer /// in case of scaled distortions, the distortions can be recalculated to ensure consistent distortions and corrections void recalculateDistortions(); + /// Use the distortion maps and scaling of another digitizer, which keeps ownership of the maps + void setSCDistortionsFrom(const Digitizer& other); + /// Use digitizer-owned random rings filled from seed and positioned per sector and timeframe in init(), /// so that the digits of a sector do not depend on which sectors were processed before /// \param seed base seed, typically DigiParams.seed diff --git a/Detectors/TPC/simulation/src/DigitContainer.cxx b/Detectors/TPC/simulation/src/DigitContainer.cxx index b5103c9907574..49e70bfb70004 100644 --- a/Detectors/TPC/simulation/src/DigitContainer.cxx +++ b/Detectors/TPC/simulation/src/DigitContainer.cxx @@ -14,6 +14,7 @@ /// \author Andi Mathis, TU München, andreas.mathis@ph.tum.de #include "TPCSimulation/DigitContainer.h" +#include #include #include #include "TPCBase/Mapper.h" @@ -59,15 +60,6 @@ void DigitContainer::fillOutputContainer(std::vector& output, mPrevDigArr = std::make_unique(); } - static bool reportedSettings = false; - if (!reportedSettings) { - reportSettings(); - if (mDeadMap) { - LOGP(info, "Using dead map with {} masked pads", mDeadMap->getSum()); - } - reportedSettings = true; - } - for (auto& time : mTimeBins) { /// the time bins between the last event and the timing of this event are uncorrelated and can be written out /// OR the readout is triggered (i.e. not continuous) and we can dump everything in any case, as long it is within one drift time interval @@ -162,12 +154,25 @@ void DigitContainer::updateCalibrations() } // dead channel map - mDeadMap = eleParam.applyDeadMap ? &cdb.getDeadChannelMap() : nullptr; + mDeadMap = nullptr; + mDeadMapCopy.reset(); + if (eleParam.applyDeadMap) { + mDeadMapCopy = std::make_unique>(cdb.getDeadChannelMap()); + mDeadMap = mDeadMapCopy.get(); + } // common mode correction in the FEE, needed to resolve DigiMode Auto mCMCEnabled = (eleParam.DigiMode == DigitzationMode::Auto) && cdb.getFEEConfig().isCMCEnabled(); mCalibrationsUpdated = true; + + static std::atomic reportedSettings{false}; + if (!reportedSettings.exchange(true)) { + reportSettings(); + if (mDeadMap) { + LOGP(info, "Using dead map with {} masked pads", mDeadMap->getSum()); + } + } } void DigitContainer::reportSettings() diff --git a/Detectors/TPC/simulation/src/Digitizer.cxx b/Detectors/TPC/simulation/src/Digitizer.cxx index 7bd78c97b3c20..c9d3669864cc6 100644 --- a/Detectors/TPC/simulation/src/Digitizer.cxx +++ b/Detectors/TPC/simulation/src/Digitizer.cxx @@ -295,6 +295,16 @@ void Digitizer::setStartTime(double time) mDigitContainer.setStartTime(starttimebin); } +void Digitizer::setSCDistortionsFrom(const Digitizer& other) +{ + mSpaceChargeOwned.reset(); + mSpaceCharge = other.mSpaceCharge; + mSpaceChargeDer = other.mSpaceChargeDer; + mUseSCDistortions = other.mUseSCDistortions; + mLumiScaleFactor = other.mLumiScaleFactor; + mUseScaledDistortions = other.mUseScaledDistortions; +} + void Digitizer::setLumiScaleFactor() { mLumiScaleFactor = (CorrMapParam::Instance().lumiInst - mSpaceCharge->getMeanLumi()) / mSpaceChargeDer->getMeanLumi(); diff --git a/Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx index 487c9b89c0302..00fe9f088efd8 100644 --- a/Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx +++ b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx @@ -42,8 +42,12 @@ std::string getBranchNameRight(int sector) std::vector> digitizeSector(Digitizer& digitizer, const steer::DigitizationContext& context, const std::vector& simChains, int sector, ULong64_t timeframe, - SectorFlushBuffers& buffers, const std::function& onFlush) + SectorFlushBuffers& buffers, const std::function& onFlush, + std::mutex* cdbMutex) { + // locks cdbMutex, if given, for calls that read calibrations through the CDBInterface + auto cdbLock = [cdbMutex]() { return cdbMutex ? std::unique_lock(*cdbMutex) : std::unique_lock(); }; + std::vector> events; const bool isContinuous = digitizer.isContinuousReadout(); const auto& irecords = context.getEventRecords(); @@ -51,7 +55,10 @@ std::vector> digitizeSector(Digitizer& dig digitizer.setSector(sector); digitizer.setSeedingTimeframe(timeframe); - digitizer.init(); + { + auto lock = cdbLock(); + digitizer.init(); + } size_t digitCounter = 0; auto flush = [&](bool finalFlush = false) { @@ -69,6 +76,7 @@ std::vector> digitizeSector(Digitizer& dig double time = hbfu.getFirstIRofTF(o2::InteractionRecord(0, hbfu.orbitFirstSampled)).bc2ns() / 1000.; digitizer.setOutputDigitTimeOffset(time); if (!irecords.empty()) { + auto lock = cdbLock(); digitizer.setStartTime(irecords[0].getTimeNS() / 1000.f); } } @@ -80,6 +88,7 @@ std::vector> digitizeSector(Digitizer& dig LOG(info) << "TPC: Event time " << eventTime << " us"; digitizer.setEventTime(eventTime); if (!isContinuous) { + auto lock = cdbLock(); digitizer.setStartTime(eventTime); } size_t startSize = digitCounter; diff --git a/Steer/DigitizerWorkflow/src/TPCSectorDigitization.h b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.h index dc8680526687e..ed22cd0999519 100644 --- a/Steer/DigitizerWorkflow/src/TPCSectorDigitization.h +++ b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.h @@ -13,6 +13,7 @@ #define STEER_DIGITIZERWORKFLOW_TPCSECTORDIGITIZATION_H_ #include +#include #include #include #include "CommonDataFormat/RangeReference.h" @@ -43,11 +44,12 @@ struct SectorFlushBuffers { /// Digitize all collisions of one sector of a timeframe. /// After each flush the new output is in buffers and onFlush is called on the calling thread. /// The function makes no DPL calls. Concurrent calls need one Digitizer with sector seeding and one set of chains -/// per thread; Digitizer::init() is called here and is not thread-safe across Digitizer instances. +/// per thread, and a common cdbMutex, which serialises the calls that read calibrations through the CDBInterface. /// \return the digit groups, one per collision part in triggered mode, one for the timeframe in continuous mode std::vector> digitizeSector(Digitizer& digitizer, const steer::DigitizationContext& context, const std::vector& simChains, int sector, ULong64_t timeframe, - SectorFlushBuffers& buffers, const std::function& onFlush); + SectorFlushBuffers& buffers, const std::function& onFlush, + std::mutex* cdbMutex = nullptr); } // namespace tpc } // namespace o2 From 45575fc9ad49b72cab3b6e7e8ff13c4c727a847c Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 9 Nov 2026 12:25:54 +0100 Subject: [PATCH 07/11] Add a multithreaded TPC digitizer device This adds an optional single TPC digitizer device that digitizes the sectors with several threads instead of lanes. - --tpc-digi-threads N replaces the TPC lanes by one device; it requires --tpc-chunked-writer. - Each thread owns a digitizer with sector seeding, its own hit chains and its own tpc_driftime_digits_lane file, so the existing merger is unchanged. - The CCDB objects are handled once on the device thread, and the distortion maps are recalculated once and shared. - The digits are identical to the lane spec with --tpc-sector-seeding. https://its.cern.ch/jira/browse/O2-7309 Co-Authored-By: Claude Opus 5.5 --- Steer/DigitizerWorkflow/CMakeLists.txt | 1 + .../src/SimpleDigitizerWorkflow.cxx | 13 +- .../src/TPCDigitizerMTSpec.cxx | 358 ++++++++++++++++++ .../DigitizerWorkflow/src/TPCDigitizerSpec.h | 3 + 4 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx diff --git a/Steer/DigitizerWorkflow/CMakeLists.txt b/Steer/DigitizerWorkflow/CMakeLists.txt index 0f272769f170c..ea0b30cdbc57d 100644 --- a/Steer/DigitizerWorkflow/CMakeLists.txt +++ b/Steer/DigitizerWorkflow/CMakeLists.txt @@ -26,6 +26,7 @@ o2_add_executable(digitizer-workflow src/SimpleDigitizerWorkflow.cxx src/TPCDigitizerSpec.cxx src/TPCSectorDigitization.cxx + src/TPCDigitizerMTSpec.cxx src/ZDCDigitizerSpec.cxx src/TOFDigitizerSpec.cxx $<$:src/ITS3DigitizerSpec.cxx> diff --git a/Steer/DigitizerWorkflow/src/SimpleDigitizerWorkflow.cxx b/Steer/DigitizerWorkflow/src/SimpleDigitizerWorkflow.cxx index e8ae6d6affb3d..a271ea0245cf4 100644 --- a/Steer/DigitizerWorkflow/src/SimpleDigitizerWorkflow.cxx +++ b/Steer/DigitizerWorkflow/src/SimpleDigitizerWorkflow.cxx @@ -186,6 +186,7 @@ void customize(std::vector& workflowOptions) // Option to write TPC digits internaly, without forwarding to a special writer instance. // This is useful in GRID productions with small available memory. workflowOptions.push_back(ConfigParamSpec{"tpc-chunked-writer", o2::framework::VariantType::Bool, false, {"Write independent TPC digit chunks as soon as they can be flushed."}}); + workflowOptions.push_back(ConfigParamSpec{"tpc-digi-threads", o2::framework::VariantType::Int, 0, {"Digitize TPC in a single device with this many threads instead of lanes (0 = use lanes); requires --tpc-chunked-writer"}}); workflowOptions.push_back(ConfigParamSpec{"tpc-distortion-type", o2::framework::VariantType::Int, 0, {"Simulate distortions in the TPC (0=no distortions, 1=distortions without scaling, 2=distortions with CTP scaling)"}}); std::string simhelp("Comma separated list of simulation prefixes (for background, signal productions)"); @@ -624,8 +625,16 @@ WorkflowSpec defineDataProcessing(ConfigContext const& configcontext) auto internalwrite = configcontext.options().get("tpc-chunked-writer"); auto distortionType = configcontext.options().get("tpc-distortion-type"); - WorkflowSpec tpcPipelines = o2::tpc::getTPCDigitizerSpec(lanes, tpcsectors, mctruth, internalwrite, distortionType); - specs.insert(specs.end(), tpcPipelines.begin(), tpcPipelines.end()); + auto digiThreads = configcontext.options().get("tpc-digi-threads"); + if (digiThreads > 0) { + if (!internalwrite) { + LOG(fatal) << "--tpc-digi-threads requires --tpc-chunked-writer"; + } + specs.emplace_back(o2::tpc::getTPCDigitizerMTSpec(digiThreads, tpcsectors, distortionType)); + } else { + WorkflowSpec tpcPipelines = o2::tpc::getTPCDigitizerSpec(lanes, tpcsectors, mctruth, internalwrite, distortionType); + specs.insert(specs.end(), tpcPipelines.begin(), tpcPipelines.end()); + } if (configcontext.options().get("tpc-reco-type").empty() == false) { throw std::runtime_error("option 'tpc-reco-type' is deprecated, please connect workflows on the command line by pipe"); diff --git a/Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx b/Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx new file mode 100644 index 0000000000000..1bff1a7063c12 --- /dev/null +++ b/Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx @@ -0,0 +1,358 @@ +// Copyright 2019-2020 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. + +/// \file TPCDigitizerMTSpec.cxx +/// \brief A single TPC digitizer device that digitizes the sectors with several threads + +#include "TPCDigitizerSpec.h" +#include "TPCSectorDigitization.h" +#include "Framework/ConfigParamRegistry.h" +#include "Framework/DataProcessorSpec.h" +#include "Framework/DataRefUtils.h" +#include "Framework/Lifetime.h" +#include "Framework/CCDBParamSpec.h" +#include "Framework/Task.h" +#include "Framework/Logger.h" +#include "Headers/DataHeader.h" +#include "DataFormatsParameters/GRPObject.h" +#include "DataFormatsTPC/TPCSectorHeader.h" +#include "DetectorsBase/BaseDPLDigitizer.h" +#include "DetectorsBase/Detector.h" +#include "SimConfig/DigiParams.h" +#include "SimulationDataFormat/DigitizationContext.h" +#include "TPCBaseRecSim/CDBInterface.h" +#include "TPCBase/ParameterGEM.h" +#include "TPCBase/ParameterGas.h" +#include "TPCCalibration/VDriftHelper.h" +#include "TPCSimulation/Digitizer.h" +#include "TPCSpaceCharge/SpaceCharge.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace o2::framework; +using SC = o2::tpc::SpaceCharge; + +namespace o2 +{ +namespace tpc +{ + +namespace +{ +/// The state of one worker thread: a digitizer, its own hit chains and its own digit file +struct DigitizerWorker { + int id = 0; + Digitizer digitizer; + std::vector chains; + SectorFlushBuffers buffers; + std::exception_ptr error; + + ~DigitizerWorker() + { + for (auto chain : chains) { + delete chain; + } + } + + std::string fileName() const { return "tpc_driftime_digits_lane" + std::to_string(id) + ".root"; } + + /// Digitize one sector and write it to the worker's digit file, in the format of the chunked writer of the lane spec + void processSector(const steer::DigitizationContext& context, int sector, ULong64_t timeframe, bool withMCTruth, std::mutex& cdbMutex) + { + std::unique_ptr file(TFile::Open(fileName().c_str(), "UPDATE")); + if (!file || file->IsZombie()) { + throw std::runtime_error("TPC: cannot open " + fileName()); + } + auto tree = new TTree(std::to_string(sector).c_str(), "o2sim"); + tree->SetDirectory(file.get()); // owned by the file + size_t flushCounter = 0; + auto onFlush = [&]() { + ++flushCounter; + auto fill = [&tree](const std::string& name, auto* data) { + auto br = o2::base::getOrMakeBranch(*tree, name.c_str(), data); + br->Fill(); + br->ResetAddress(); + }; + fill("TPCDigit_" + std::to_string(sector), &buffers.digits); + if (withMCTruth) { + fill("TPCDigitMCTruth_" + std::to_string(sector), &buffers.labels); + } + fill("TPCCommonMode_" + std::to_string(sector), &buffers.commonMode); + }; + digitizeSector(digitizer, context, chains, sector, timeframe, buffers, onFlush, &cdbMutex); + tree->SetEntries(flushCounter); + file->Write("", TObject::kOverwrite); + file->Close(); + } +}; +} // namespace + +class TPCDPLDigitizerMTTask : public o2::base::BaseDPLDigitizer +{ + public: + TPCDPLDigitizerMTTask(int nThreads, int distortionType) : BaseDPLDigitizer(o2::base::InitServices::FIELD | o2::base::InitServices::GEOM), mNThreads(nThreads), mDistortionType(distortionType) {} + + void initDigitizerTask(framework::InitContext& ic) override + { + LOG(info) << "Initializing multithreaded TPC digitization with " << mNThreads << " threads"; + + mWithMCTruth = o2::conf::DigiParams::Instance().mctruth; + const auto triggeredMode = ic.options().get("TPCtriggered"); + mRecalcDistortions = !(ic.options().get("do-not-recalculate-distortions")); + SC::setNThreads(ic.options().get("n-threads-distortions")); + mUseCalibrationsFromCCDB = ic.options().get("TPCuseCCDB"); + mMeanLumiDistortions = ic.options().get("meanLumiDistortions"); + mMeanLumiDistortionsDerivative = ic.options().get("meanLumiDistortionsDerivative"); + LOG(info) << "TPC calibrations from CCDB: " << mUseCalibrationsFromCCDB; + + // the workers share no random numbers: each digitizer seeds every sector from DigiParams.seed + const auto seed = o2::conf::DigiParams::Instance().seed; + if (seed == 0) { + LOG(warning) << "TPC sector seeding with DigiParams.seed=0 is not reproducible"; + } + // with seed 0, gRandom was seeded randomly in the base class + const ULong64_t sectorSeed = seed != 0 ? seed : gRandom->Integer(kMaxUInt); + + for (int i = 0; i < mNThreads; ++i) { + auto& worker = mWorkers.emplace_back(std::make_unique()); + worker->id = i; + worker->digitizer.setContinuousReadout(!triggeredMode); + worker->digitizer.setDistortionScaleType(mDistortionType); + worker->digitizer.setUseSectorSeeding(sectorSeed); + } + + // the merger picks up every lane file in the directory, so files of an earlier run must not survive + for (const auto& entry : std::filesystem::directory_iterator(".")) { + const auto name = entry.path().filename().string(); + if (name.rfind("tpc_driftime_digits_lane", 0) == 0 && entry.path().extension() == ".root") { + std::filesystem::remove(entry.path()); + } + } + + ROOT::EnableThreadSafety(); + } + + void finaliseCCDB(framework::ConcreteDataMatcher& matcher, void* obj) + { + if (mTPCVDriftHelper.accountCCDBInputs(matcher, obj)) { + return; + } + // the maps are set on the first digitizer; the others share them once they are final + auto& digitizer = mWorkers[0]->digitizer; + if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCDIST", 0)) { + LOGP(info, "Updating distortion map"); + digitizer.setUseSCDistortions(static_cast(obj)); + if (mMeanLumiDistortions >= 0) { + digitizer.setMeanLumiDistortions(mMeanLumiDistortions); + } + } + if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCDISTDERIV", 0)) { + LOGP(info, "Updating reference distortion map"); + digitizer.setSCDistortionsDerivative(static_cast(obj)); + if (mMeanLumiDistortionsDerivative >= 0) { + digitizer.setMeanLumiDistortionsDerivative(mMeanLumiDistortionsDerivative); + } + } + if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCGASPARAM", 0)) { + LOGP(info, "TPC gas param updated"); + ParameterGas::Instance().printKeyValues(true, true); + } + if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCGEMPARAM", 0)) { + LOGP(info, "TPC GEM param updated"); + ParameterGEM::Instance().printKeyValues(true, true); + } + } + + void run(framework::ProcessingContext& pc) + { + LOG(info) << "Processing multithreaded TPC digitization"; + + auto& cdb = o2::tpc::CDBInterface::instance(); + cdb.setUseDefaults(!mUseCalibrationsFromCCDB); + // whatever are global settings for CCDB usage, we have to extract the TPC vdrift from CCDB for anchored simulations + mTPCVDriftHelper.extractCCDBInputs(pc); + pc.inputs().get("gasparam"); + pc.inputs().get("gemparam"); + auto& primary = mWorkers[0]->digitizer; + if (mDistortionType) { + pc.inputs().get("tpcdistortions"); + if (mDistortionType == 2) { + pc.inputs().get("tpcdistortionsderiv"); + primary.setLumiScaleFactor(); + if (mRecalcDistortions) { + // done once, with the threads of SpaceCharge, for all workers + primary.recalculateDistortions(); + } + } + for (size_t i = 1; i < mWorkers.size(); ++i) { + mWorkers[i]->digitizer.setSCDistortionsFrom(primary); + } + } + + if (mTPCVDriftHelper.isUpdated()) { + const auto& vd = mTPCVDriftHelper.getVDriftObject(); + LOGP(info, "Updating TPC fast transform map with new VDrift factor of {} wrt reference {} and DriftTimeOffset correction {} wrt {} from source {}", + vd.corrFact, vd.refVDrift, vd.timeOffsetCorr, vd.refTimeOffset, mTPCVDriftHelper.getSourceName()); + for (auto& worker : mWorkers) { + worker->digitizer.setVDrift(vd.getVDrift()); + worker->digitizer.setTDriftOffset(vd.getTimeOffset()); + } + mTPCVDriftHelper.acknowledgeUpdate(); + } + + if (std::filesystem::exists("ThresholdMap.root")) { + LOG(info) << "TPC: Using zero suppression map from 'ThresholdMap.root'"; + cdb.setThresholdMapFromFile("ThresholdMap.root"); + } + + if (std::filesystem::exists("GainMap.root")) { + LOG(info) << "TPC: Using gain map from 'GainMap.root'"; + cdb.setGainMapFromFile("GainMap.root"); + } + + // the sectors of this timeframe; all inputs carry the same collision context + std::vector sectors; + std::optional contextRef; + for (auto it = pc.inputs().begin(), end = pc.inputs().end(); it != end; ++it) { + for (auto const& inputref : it.parts()) { + if (inputref.spec->lifetime == o2::framework::Lifetime::Condition) { + continue; + } + auto const* sectorHeader = DataRefUtils::getHeader(inputref); + if (sectorHeader == nullptr) { + LOG(error) << "TPC: Sector header missing, skipping input"; + continue; + } + const int sector = sectorHeader->sector(); + if (sector < 0 || sector >= TPCSectorHeader::NSectors) { + throw std::runtime_error("Digitizer can only work on single sectors"); + } + sectors.push_back(sector); + if (!contextRef) { + contextRef = inputref; + } + } + } + + if (mWriteGRP) { + auto roMode = primary.isContinuousReadout() ? o2::parameters::GRPObject::CONTINUOUS : o2::parameters::GRPObject::PRESENT; + LOG(info) << "TPC: Sending ROMode= " << (primary.isContinuousReadout() ? "Continuous" : "Triggered") << " to GRPUpdater"; + pc.outputs().snapshot(Output{"TPC", "ROMode", 0}, roMode); + mWriteGRP = false; + } + if (!contextRef) { + return; + } + auto context = pc.inputs().get(*contextRef); + const ULong64_t timeframe = DataRefUtils::getHeader(*contextRef)->tfCounter; + LOG(info) << "TPC: Processing " << sectors.size() << " sectors with " << context->getEventRecords().size() << " collisions"; + + TStopwatch timer; + timer.Start(); + + const size_t nWorkers = std::min(mWorkers.size(), sectors.size()); + for (size_t i = 0; i < nWorkers; ++i) { + context->initSimChains(o2::detectors::DetID::TPC, mWorkers[i]->chains); + } + + std::atomic nextSector{0}; + // serialises CDBInterface access; the CCDB timestamp must not change while the workers run + std::mutex cdbMutex; + std::vector threads; + for (size_t i = 0; i < nWorkers; ++i) { + threads.emplace_back([&, worker = mWorkers[i].get()]() { + try { + for (size_t is = nextSector++; is < sectors.size(); is = nextSector++) { + LOG(info) << "TPC: Worker " << worker->id << " processing sector " << sectors[is]; + worker->processSector(*context, sectors[is], timeframe, mWithMCTruth, cdbMutex); + } + } catch (...) { + worker->error = std::current_exception(); + nextSector = sectors.size(); // let the other workers stop early + } + }); + } + for (auto& thread : threads) { + thread.join(); + } + for (size_t i = 0; i < nWorkers; ++i) { + if (mWorkers[i]->error) { + std::rethrow_exception(mWorkers[i]->error); + } + } + + timer.Stop(); + LOG(info) << "TPC: Digitization took " << timer.CpuTime() << "s CPU, " << timer.RealTime() << "s wall"; + } + + private: + std::vector> mWorkers; + o2::tpc::VDriftHelper mTPCVDriftHelper{}; + int mNThreads = 1; + int mDistortionType = 0; + bool mWriteGRP = true; + bool mWithMCTruth = true; + bool mUseCalibrationsFromCCDB = false; + bool mRecalcDistortions = false; + float mMeanLumiDistortions = -1; + float mMeanLumiDistortionsDerivative = -1; +}; + +o2::framework::DataProcessorSpec getTPCDigitizerMTSpec(int nThreads, std::vector const& sectors, int distortionType) +{ + std::vector inputs; + for (auto sector : sectors) { + inputs.emplace_back("collisioncontext" + std::to_string(sector), "SIM", "COLLISIONCONTEXT", static_cast(sector), Lifetime::Timeframe); + } + o2::tpc::VDriftHelper::requestCCDBInputs(inputs); + inputs.emplace_back("gasparam", o2::header::gDataOriginTPC, "TPCGASPARAM", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::ParGas))); + inputs.emplace_back("gemparam", o2::header::gDataOriginTPC, "TPCGEMPARAM", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::ParGEM))); + if (distortionType) { + inputs.emplace_back("tpcdistortions", o2::header::gDataOriginTPC, "TPCDIST", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::DistortionMapMC), {}, 1)); // time-dependent + if (distortionType == 2) { + inputs.emplace_back("tpcdistortionsderiv", o2::header::gDataOriginTPC, "TPCDISTDERIV", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::DistortionMapDerivMC), {}, 1)); // time-dependent + } + } + std::vector outputs{{"TPC", "ROMode", 0, Lifetime::Timeframe}}; + + // the name must not match "TPCDigitizer.*", whose completion policy would process the sectors one by one + return DataProcessorSpec{ + "tpc-digitizer-mt", + inputs, + outputs, + AlgorithmSpec{adaptFromTask(nThreads, distortionType)}, + Options{ + {"TPCtriggered", VariantType::Bool, false, {"Impose triggered RO mode (default: continuous)"}}, + {"TPCuseCCDB", VariantType::Bool, false, {"true: load calibrations from CCDB; false: use random calibratoins"}}, + {"meanLumiDistortions", VariantType::Float, -1.f, {"override lumi of distortion object if >=0"}}, + {"meanLumiDistortionsDerivative", VariantType::Float, -1.f, {"override lumi of derivative distortion object if >=0"}}, + {"do-not-recalculate-distortions", VariantType::Bool, false, {"Do not recalculate the distortions"}}, + {"n-threads-distortions", VariantType::Int, 4, {"Number of threads used for the calculation of the distortions"}}, + }}; +} + +} // namespace tpc +} // namespace o2 diff --git a/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.h b/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.h index 65837f7974ba1..62c6e1def55d1 100644 --- a/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.h +++ b/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.h @@ -24,6 +24,9 @@ o2::framework::DataProcessorSpec getTPCDigitizerSpec(int channel, bool writeGRP, o2::framework::WorkflowSpec getTPCDigitizerSpec(int nLanes, std::vector const& sectors, bool mctruth, bool internalwriter, int distortionType); +/// A single TPC digitizer device that digitizes the sectors with nThreads threads and writes the digits in chunks +o2::framework::DataProcessorSpec getTPCDigitizerMTSpec(int nThreads, std::vector const& sectors, int distortionType); + } // end namespace tpc } // end namespace o2 From 02765f4610d58acb2e0a6a682b89a015313ff9f2 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 9 Nov 2026 17:21:39 +0100 Subject: [PATCH 08/11] Keep RandomRing vector reads inside the ring This addresses a review comment on the random ring repositioning. - setRandomPosition() can place the ring at any index. - getNextValueVc() now wraps to the start when a vector would run past the end. https://its.cern.ch/jira/browse/O2-7309 Co-Authored-By: Claude Opus 5.5 --- Common/MathUtils/include/MathUtils/RandomRing.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Common/MathUtils/include/MathUtils/RandomRing.h b/Common/MathUtils/include/MathUtils/RandomRing.h index a9915df430a37..d3e61696bedf7 100644 --- a/Common/MathUtils/include/MathUtils/RandomRing.h +++ b/Common/MathUtils/include/MathUtils/RandomRing.h @@ -101,6 +101,10 @@ class RandomRing // within this header file (to reduce memory problems during compilation). // The hope is that the calling user calls this with a // correct Vc type (Vc::float_v) in a source file. + // a vector must not run past the end of the ring, also after setRandomPosition() + if (mRingPosition + VcType::size() > mRandomNumbers.size()) { + mRingPosition = 0; + } const VcType value = VcType(&mRandomNumbers[mRingPosition]); mRingPosition += VcType::size(); if (mRingPosition >= mRandomNumbers.size()) { @@ -113,7 +117,7 @@ class RandomRing /// @return position in the ring buffer unsigned int getRingPosition() const { return mRingPosition; } - /// move to a random position in the ring buffer; not for rings read with getNextValueVc() + /// move to a random position in the ring buffer /// @param [in] rng random generator to draw the position from void setRandomPosition(TRandom& rng) { mRingPosition = rng.Integer(N); } From 128083b3146afe4b2008928f54892052e5502b20 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 9 Nov 2026 17:21:39 +0100 Subject: [PATCH 09/11] Use one call for all TPC digitization modes in DigitContainer This addresses a review comment on the repeated fillOutputContainer calls. - The eight cases now pass the mode as a type to one lambda. https://its.cern.ch/jira/browse/O2-7309 Co-Authored-By: Claude Opus 5.5 --- .../TPC/simulation/src/DigitContainer.cxx | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/Detectors/TPC/simulation/src/DigitContainer.cxx b/Detectors/TPC/simulation/src/DigitContainer.cxx index 49e70bfb70004..aeb6da355759f 100644 --- a/Detectors/TPC/simulation/src/DigitContainer.cxx +++ b/Detectors/TPC/simulation/src/DigitContainer.cxx @@ -16,6 +16,7 @@ #include "TPCSimulation/DigitContainer.h" #include #include +#include #include #include "TPCBase/Mapper.h" #include "TPCBaseRecSim/CDBInterface.h" @@ -84,39 +85,37 @@ void DigitContainer::fillOutputContainer(std::vector& output, // fmt::print("Processing secotor: {}, time bin: {}, mFirstTimeBin: {}, dgitTime: {}\n", sector.getSector(), timeBin, mFirstTimeBin, (void*)time); if (time) { + // the digitization mode is a template parameter of fillOutputContainer + auto fill = [&](auto mode) { + time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + }; + using M = DigitzationMode; switch (digitizationMode) { - case DigitzationMode::FullMode: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + case M::FullMode: + fill(std::integral_constant{}); break; - } - case DigitzationMode::ZeroSuppression: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + case M::ZeroSuppression: + fill(std::integral_constant{}); break; - } - case DigitzationMode::ZeroSuppressionCMCorr: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + case M::ZeroSuppressionCMCorr: + fill(std::integral_constant{}); break; - } - case DigitzationMode::SubtractPedestal: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + case M::SubtractPedestal: + fill(std::integral_constant{}); break; - } - case DigitzationMode::NoSaturation: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + case M::NoSaturation: + fill(std::integral_constant{}); break; - } - case DigitzationMode::PropagateADC: { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + case M::PropagateADC: + fill(std::integral_constant{}); break; - } - case DigitzationMode::Auto: { + case M::Auto: if (mCMCEnabled) { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + fill(std::integral_constant{}); } else { - time->fillOutputContainer(output, mcTruth, commonModeOutput, sector, timeBin, sampaProcessing, mLabelCollector, mPrevDigArr.get(), debugStream, mPadParams, mDeadMap); + fill(std::integral_constant{}); } break; - } } } From 48da7ab7d0f714fc3f1f5753ec4df43935abff95 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 9 Nov 2026 17:25:29 +0100 Subject: [PATCH 10/11] Address review comments on the multithreaded TPC digitizer This addresses the review of the multithreaded TPC digitizer device. - The workers copy the random rings of the first worker instead of filling their own. - The workers no longer flush the process-wide debug streamer; the device flushes it after the join. - The Digitizer serialises its calibration reads itself, so digitizeSector needs no mutex. - The CCDB handling, the calibration files and the common options of both devices are in TPCDigitizerConditions. - The device stops on a second timeframe, which the chunked digit files cannot hold. - Shorter comments in TPCSectorDigitization.h. https://its.cern.ch/jira/browse/O2-7309 Co-Authored-By: Claude Opus 5.5 --- .../include/TPCSimulation/Digitizer.h | 10 ++ Detectors/TPC/simulation/src/Digitizer.cxx | 49 +++++-- .../src/TPCDigitizerMTSpec.cxx | 128 +++++----------- .../src/TPCDigitizerSpec.cxx | 104 ++----------- .../src/TPCSectorDigitization.cxx | 138 ++++++++++++++++-- .../src/TPCSectorDigitization.h | 43 +++++- 6 files changed, 257 insertions(+), 215 deletions(-) diff --git a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h index 1f1c68aac3c20..4c60102109214 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h @@ -154,6 +154,15 @@ class Digitizer /// Set the timeframe identifier that enters the sector seeding void setSeedingTimeframe(ULong64_t timeframe) { mSeedingTimeframe = timeframe; } + /// Fill the random rings of the sector seeding, if not done yet + void initSectorSeeding(); + + /// Use copies of the sector-seeding random rings of another digitizer + void setSectorSeedingFrom(const Digitizer& other); + + /// Whether flush() also flushes the process-wide debug streamer + void setFlushDebugStreamer(bool flush) { mFlushDebugStreamer = flush; } + private: ElectronTransport& electronTransport(); GEMAmplification& gemAmplification(); @@ -165,6 +174,7 @@ class Digitizer ULong64_t mSeed = 0; //!< Base seed for sector seeding ULong64_t mSeedingTimeframe = 0; //!< Timeframe identifier for sector seeding bool mUseSectorSeeding = false; //!< Whether sector seeding is used + bool mFlushDebugStreamer = true; //!< Whether flush() flushes the debug streamer DigitContainer mDigitContainer; ///< Container for the Digits std::vector mSignalArray; //!< Workspace for the shaped signal of one electron diff --git a/Detectors/TPC/simulation/src/Digitizer.cxx b/Detectors/TPC/simulation/src/Digitizer.cxx index c9d3669864cc6..114afbbcffd6d 100644 --- a/Detectors/TPC/simulation/src/Digitizer.cxx +++ b/Detectors/TPC/simulation/src/Digitizer.cxx @@ -31,6 +31,7 @@ #include #include +#include ClassImp(o2::tpc::Digitizer); @@ -42,6 +43,13 @@ Digitizer::Digitizer() = default; namespace { +/// serialises the calls that read calibrations through the CDBInterface, which is not thread safe +std::mutex& calibrationMutex() +{ + static std::mutex mutex; + return mutex; +} + // splitmix64 finaliser, to derive well separated seeds from the base seed ULong64_t mixSeed(ULong64_t x) { @@ -80,17 +88,35 @@ void Digitizer::setUseSectorSeeding(ULong64_t seed) mSeed = seed; } +void Digitizer::initSectorSeeding() +{ + if (mUseSectorSeeding && !mGEMAmplification) { + // the rings are filled once, in the construction order of the singletons + TRandom3 rng(toTRandom3Seed(mixSeed(mSeed))); + mGEMAmplification = std::make_unique(&rng); + mElectronTransport = std::make_unique(&rng); + mSAMPAProcessing = std::make_unique(&rng); + mDigitContainer.setSAMPAProcessing(mSAMPAProcessing.get()); + } +} + +void Digitizer::setSectorSeedingFrom(const Digitizer& other) +{ + mUseSectorSeeding = other.mUseSectorSeeding; + mSeed = other.mSeed; + if (other.mGEMAmplification) { + mGEMAmplification = std::make_unique(*other.mGEMAmplification); + mElectronTransport = std::make_unique(*other.mElectronTransport); + mSAMPAProcessing = std::make_unique(*other.mSAMPAProcessing); + mDigitContainer.setSAMPAProcessing(mSAMPAProcessing.get()); + } +} + void Digitizer::init() { + std::lock_guard lock(calibrationMutex()); if (mUseSectorSeeding) { - if (!mGEMAmplification) { - // the rings are filled once, in the construction order of the singletons - TRandom3 rng(toTRandom3Seed(mixSeed(mSeed))); - mGEMAmplification = std::make_unique(&rng); - mElectronTransport = std::make_unique(&rng); - mSAMPAProcessing = std::make_unique(&rng); - mDigitContainer.setSAMPAProcessing(mSAMPAProcessing.get()); - } + initSectorSeeding(); const ULong64_t sectorSeed = mixSeed(mSeed ^ mixSeed(static_cast(mSector) + 1)); TRandom3 rng(toTRandom3Seed(mixSeed(sectorSeed ^ mixSeed(mSeedingTimeframe)))); mGEMAmplification->setRandomRingPositions(rng); @@ -230,7 +256,7 @@ void Digitizer::flush(std::vector& digits, SAMPAProcessing& sampaProcessing = this->sampaProcessing(); mDigitContainer.fillOutputContainer(digits, labels, commonModeOutput, mSector, sampaProcessing.getTimeBinFromTime(mEventTime - mOutputDigitTimeOffset), mIsContinuous, finalFlush); // flushing debug output to file - if (((finalFlush && mIsContinuous) || (!mIsContinuous)) && mSpaceCharge) { + if (mFlushDebugStreamer && ((finalFlush && mIsContinuous) || (!mIsContinuous)) && mSpaceCharge) { o2::utils::DebugStreamer::instance()->flush(); } } @@ -289,7 +315,10 @@ void Digitizer::setStartTime(double time) // note that negative times w.r.t start of timeframe/data-taking == mOutputDigitTimeOffset // will yield the 0-th bin (due to casting logic in sampaProcessing) SAMPAProcessing& sampaProcessing = this->sampaProcessing(); - sampaProcessing.updateParameters(mVDrift); + { + std::lock_guard lock(calibrationMutex()); + sampaProcessing.updateParameters(mVDrift); + } const auto timediff = time - mOutputDigitTimeOffset; const auto starttimebin = sampaProcessing.getTimeBinFromTime(timediff); mDigitContainer.setStartTime(starttimebin); diff --git a/Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx b/Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx index 1bff1a7063c12..1ee6595247b58 100644 --- a/Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx +++ b/Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx @@ -34,6 +34,7 @@ #include "TPCCalibration/VDriftHelper.h" #include "TPCSimulation/Digitizer.h" #include "TPCSpaceCharge/SpaceCharge.h" +#include "CommonUtils/DebugStreamer.h" #include #include #include @@ -45,7 +46,6 @@ #include #include #include -#include #include #include #include @@ -80,7 +80,7 @@ struct DigitizerWorker { std::string fileName() const { return "tpc_driftime_digits_lane" + std::to_string(id) + ".root"; } /// Digitize one sector and write it to the worker's digit file, in the format of the chunked writer of the lane spec - void processSector(const steer::DigitizationContext& context, int sector, ULong64_t timeframe, bool withMCTruth, std::mutex& cdbMutex) + void processSector(const steer::DigitizationContext& context, int sector, ULong64_t timeframe, bool withMCTruth) { std::unique_ptr file(TFile::Open(fileName().c_str(), "UPDATE")); if (!file || file->IsZombie()) { @@ -102,7 +102,7 @@ struct DigitizerWorker { } fill("TPCCommonMode_" + std::to_string(sector), &buffers.commonMode); }; - digitizeSector(digitizer, context, chains, sector, timeframe, buffers, onFlush, &cdbMutex); + digitizeSector(digitizer, context, chains, sector, timeframe, buffers, onFlush); tree->SetEntries(flushCounter); file->Write("", TObject::kOverwrite); file->Close(); @@ -121,12 +121,7 @@ class TPCDPLDigitizerMTTask : public o2::base::BaseDPLDigitizer mWithMCTruth = o2::conf::DigiParams::Instance().mctruth; const auto triggeredMode = ic.options().get("TPCtriggered"); - mRecalcDistortions = !(ic.options().get("do-not-recalculate-distortions")); - SC::setNThreads(ic.options().get("n-threads-distortions")); - mUseCalibrationsFromCCDB = ic.options().get("TPCuseCCDB"); - mMeanLumiDistortions = ic.options().get("meanLumiDistortions"); - mMeanLumiDistortionsDerivative = ic.options().get("meanLumiDistortionsDerivative"); - LOG(info) << "TPC calibrations from CCDB: " << mUseCalibrationsFromCCDB; + mConditions.init(ic, mDistortionType); // the workers share no random numbers: each digitizer seeds every sector from DigiParams.seed const auto seed = o2::conf::DigiParams::Instance().seed; @@ -142,6 +137,8 @@ class TPCDPLDigitizerMTTask : public o2::base::BaseDPLDigitizer worker->digitizer.setContinuousReadout(!triggeredMode); worker->digitizer.setDistortionScaleType(mDistortionType); worker->digitizer.setUseSectorSeeding(sectorSeed); + // the main thread flushes the process-wide debug streamer after the workers + worker->digitizer.setFlushDebugStreamer(false); } // the merger picks up every lane file in the directory, so files of an earlier run must not survive @@ -157,80 +154,33 @@ class TPCDPLDigitizerMTTask : public o2::base::BaseDPLDigitizer void finaliseCCDB(framework::ConcreteDataMatcher& matcher, void* obj) { - if (mTPCVDriftHelper.accountCCDBInputs(matcher, obj)) { - return; - } // the maps are set on the first digitizer; the others share them once they are final - auto& digitizer = mWorkers[0]->digitizer; - if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCDIST", 0)) { - LOGP(info, "Updating distortion map"); - digitizer.setUseSCDistortions(static_cast(obj)); - if (mMeanLumiDistortions >= 0) { - digitizer.setMeanLumiDistortions(mMeanLumiDistortions); - } - } - if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCDISTDERIV", 0)) { - LOGP(info, "Updating reference distortion map"); - digitizer.setSCDistortionsDerivative(static_cast(obj)); - if (mMeanLumiDistortionsDerivative >= 0) { - digitizer.setMeanLumiDistortionsDerivative(mMeanLumiDistortionsDerivative); - } - } - if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCGASPARAM", 0)) { - LOGP(info, "TPC gas param updated"); - ParameterGas::Instance().printKeyValues(true, true); - } - if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCGEMPARAM", 0)) { - LOGP(info, "TPC GEM param updated"); - ParameterGEM::Instance().printKeyValues(true, true); - } + mConditions.finaliseCCDB(matcher, obj, mWorkers[0]->digitizer); } void run(framework::ProcessingContext& pc) { LOG(info) << "Processing multithreaded TPC digitization"; - auto& cdb = o2::tpc::CDBInterface::instance(); - cdb.setUseDefaults(!mUseCalibrationsFromCCDB); - // whatever are global settings for CCDB usage, we have to extract the TPC vdrift from CCDB for anchored simulations - mTPCVDriftHelper.extractCCDBInputs(pc); - pc.inputs().get("gasparam"); - pc.inputs().get("gemparam"); auto& primary = mWorkers[0]->digitizer; - if (mDistortionType) { - pc.inputs().get("tpcdistortions"); - if (mDistortionType == 2) { - pc.inputs().get("tpcdistortionsderiv"); - primary.setLumiScaleFactor(); - if (mRecalcDistortions) { - // done once, with the threads of SpaceCharge, for all workers - primary.recalculateDistortions(); - } + mConditions.update(pc, primary, [this](float vdrift, float tdriftOffset) { + for (auto& worker : mWorkers) { + worker->digitizer.setVDrift(vdrift); + worker->digitizer.setTDriftOffset(tdriftOffset); } + }); + if (mDistortionType) { for (size_t i = 1; i < mWorkers.size(); ++i) { mWorkers[i]->digitizer.setSCDistortionsFrom(primary); } } - - if (mTPCVDriftHelper.isUpdated()) { - const auto& vd = mTPCVDriftHelper.getVDriftObject(); - LOGP(info, "Updating TPC fast transform map with new VDrift factor of {} wrt reference {} and DriftTimeOffset correction {} wrt {} from source {}", - vd.corrFact, vd.refVDrift, vd.timeOffsetCorr, vd.refTimeOffset, mTPCVDriftHelper.getSourceName()); - for (auto& worker : mWorkers) { - worker->digitizer.setVDrift(vd.getVDrift()); - worker->digitizer.setTDriftOffset(vd.getTimeOffset()); + if (!mSeedingShared) { + // the rings depend only on the seed, so they are filled once and copied + primary.initSectorSeeding(); + for (size_t i = 1; i < mWorkers.size(); ++i) { + mWorkers[i]->digitizer.setSectorSeedingFrom(primary); } - mTPCVDriftHelper.acknowledgeUpdate(); - } - - if (std::filesystem::exists("ThresholdMap.root")) { - LOG(info) << "TPC: Using zero suppression map from 'ThresholdMap.root'"; - cdb.setThresholdMapFromFile("ThresholdMap.root"); - } - - if (std::filesystem::exists("GainMap.root")) { - LOG(info) << "TPC: Using gain map from 'GainMap.root'"; - cdb.setGainMapFromFile("GainMap.root"); + mSeedingShared = true; } // the sectors of this timeframe; all inputs carry the same collision context @@ -266,6 +216,11 @@ class TPCDPLDigitizerMTTask : public o2::base::BaseDPLDigitizer if (!contextRef) { return; } + // the chunked digit files and the dynamic sector scheduling hold one timeframe per process + if (mProcessedTimeframe) { + LOG(fatal) << "TPC: the multithreaded digitizer supports only one timeframe per process"; + } + mProcessedTimeframe = true; auto context = pc.inputs().get(*contextRef); const ULong64_t timeframe = DataRefUtils::getHeader(*contextRef)->tfCounter; LOG(info) << "TPC: Processing " << sectors.size() << " sectors with " << context->getEventRecords().size() << " collisions"; @@ -278,16 +233,15 @@ class TPCDPLDigitizerMTTask : public o2::base::BaseDPLDigitizer context->initSimChains(o2::detectors::DetID::TPC, mWorkers[i]->chains); } + // the sectors go to the workers dynamically std::atomic nextSector{0}; - // serialises CDBInterface access; the CCDB timestamp must not change while the workers run - std::mutex cdbMutex; std::vector threads; for (size_t i = 0; i < nWorkers; ++i) { threads.emplace_back([&, worker = mWorkers[i].get()]() { try { for (size_t is = nextSector++; is < sectors.size(); is = nextSector++) { LOG(info) << "TPC: Worker " << worker->id << " processing sector " << sectors[is]; - worker->processSector(*context, sectors[is], timeframe, mWithMCTruth, cdbMutex); + worker->processSector(*context, sectors[is], timeframe, mWithMCTruth); } } catch (...) { worker->error = std::current_exception(); @@ -298,6 +252,9 @@ class TPCDPLDigitizerMTTask : public o2::base::BaseDPLDigitizer for (auto& thread : threads) { thread.join(); } + if (auto* streamer = o2::utils::DebugStreamer::instance()) { + streamer->flush(); + } for (size_t i = 0; i < nWorkers; ++i) { if (mWorkers[i]->error) { std::rethrow_exception(mWorkers[i]->error); @@ -310,15 +267,13 @@ class TPCDPLDigitizerMTTask : public o2::base::BaseDPLDigitizer private: std::vector> mWorkers; - o2::tpc::VDriftHelper mTPCVDriftHelper{}; + TPCDigitizerConditions mConditions; + bool mSeedingShared = false; ///< whether the workers got the random rings of the first one + bool mProcessedTimeframe = false; ///< whether a timeframe was digitized int mNThreads = 1; int mDistortionType = 0; bool mWriteGRP = true; bool mWithMCTruth = true; - bool mUseCalibrationsFromCCDB = false; - bool mRecalcDistortions = false; - float mMeanLumiDistortions = -1; - float mMeanLumiDistortionsDerivative = -1; }; o2::framework::DataProcessorSpec getTPCDigitizerMTSpec(int nThreads, std::vector const& sectors, int distortionType) @@ -327,15 +282,7 @@ o2::framework::DataProcessorSpec getTPCDigitizerMTSpec(int nThreads, std::vector for (auto sector : sectors) { inputs.emplace_back("collisioncontext" + std::to_string(sector), "SIM", "COLLISIONCONTEXT", static_cast(sector), Lifetime::Timeframe); } - o2::tpc::VDriftHelper::requestCCDBInputs(inputs); - inputs.emplace_back("gasparam", o2::header::gDataOriginTPC, "TPCGASPARAM", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::ParGas))); - inputs.emplace_back("gemparam", o2::header::gDataOriginTPC, "TPCGEMPARAM", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::ParGEM))); - if (distortionType) { - inputs.emplace_back("tpcdistortions", o2::header::gDataOriginTPC, "TPCDIST", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::DistortionMapMC), {}, 1)); // time-dependent - if (distortionType == 2) { - inputs.emplace_back("tpcdistortionsderiv", o2::header::gDataOriginTPC, "TPCDISTDERIV", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::DistortionMapDerivMC), {}, 1)); // time-dependent - } - } + TPCDigitizerConditions::addInputs(inputs, distortionType); std::vector outputs{{"TPC", "ROMode", 0, Lifetime::Timeframe}}; // the name must not match "TPCDigitizer.*", whose completion policy would process the sectors one by one @@ -344,14 +291,7 @@ o2::framework::DataProcessorSpec getTPCDigitizerMTSpec(int nThreads, std::vector inputs, outputs, AlgorithmSpec{adaptFromTask(nThreads, distortionType)}, - Options{ - {"TPCtriggered", VariantType::Bool, false, {"Impose triggered RO mode (default: continuous)"}}, - {"TPCuseCCDB", VariantType::Bool, false, {"true: load calibrations from CCDB; false: use random calibratoins"}}, - {"meanLumiDistortions", VariantType::Float, -1.f, {"override lumi of distortion object if >=0"}}, - {"meanLumiDistortionsDerivative", VariantType::Float, -1.f, {"override lumi of derivative distortion object if >=0"}}, - {"do-not-recalculate-distortions", VariantType::Bool, false, {"Do not recalculate the distortions"}}, - {"n-threads-distortions", VariantType::Int, 4, {"Number of threads used for the calculation of the distortions"}}, - }}; + TPCDigitizerConditions::options()}; } } // namespace tpc diff --git a/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx b/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx index 482e85598d34e..9bd499a5955c1 100644 --- a/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx +++ b/Steer/DigitizerWorkflow/src/TPCDigitizerSpec.cxx @@ -108,14 +108,7 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer mWithMCTruth = o2::conf::DigiParams::Instance().mctruth; auto triggeredMode = ic.options().get("TPCtriggered"); - mRecalcDistortions = !(ic.options().get("do-not-recalculate-distortions")); - const int nthreadsDist = ic.options().get("n-threads-distortions"); - SC::setNThreads(nthreadsDist); - mUseCalibrationsFromCCDB = ic.options().get("TPCuseCCDB"); - mMeanLumiDistortions = ic.options().get("meanLumiDistortions"); - mMeanLumiDistortionsDerivative = ic.options().get("meanLumiDistortionsDerivative"); - - LOG(info) << "TPC calibrations from CCDB: " << mUseCalibrationsFromCCDB; + mConditions.init(ic, mDistortionType); mDigitizer.setContinuousReadout(!triggeredMode); mDigitizer.setDistortionScaleType(mDistortionType); @@ -188,73 +181,17 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer void finaliseCCDB(framework::ConcreteDataMatcher& matcher, void* obj) { - if (mTPCVDriftHelper.accountCCDBInputs(matcher, obj)) { - return; - } - if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCDIST", 0)) { - LOGP(info, "Updating distortion map"); - mDigitizer.setUseSCDistortions(static_cast(obj)); - if (mMeanLumiDistortions >= 0) { - mDigitizer.setMeanLumiDistortions(mMeanLumiDistortions); - } - } - if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCDISTDERIV", 0)) { - LOGP(info, "Updating reference distortion map"); - mDigitizer.setSCDistortionsDerivative(static_cast(obj)); - if (mMeanLumiDistortionsDerivative >= 0) { - mDigitizer.setMeanLumiDistortionsDerivative(mMeanLumiDistortionsDerivative); - } - } - if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCGASPARAM", 0)) { - LOGP(info, "TPC gas param updated"); - ParameterGas::Instance().printKeyValues(true, true); - } - if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCGEMPARAM", 0)) { - LOGP(info, "TPC GEM param updated"); - ParameterGEM::Instance().printKeyValues(true, true); - } + mConditions.finaliseCCDB(matcher, obj, mDigitizer); } void run(framework::ProcessingContext& pc) { LOG(info) << "Processing TPC digitization"; - /// For the time being use the defaults for the CDB - auto& cdb = o2::tpc::CDBInterface::instance(); - cdb.setUseDefaults(!mUseCalibrationsFromCCDB); - // whatever are global settings for CCDB usage, we have to extract the TPC vdrift from CCDB for anchored simulations - mTPCVDriftHelper.extractCCDBInputs(pc); - pc.inputs().get("gasparam"); - pc.inputs().get("gemparam"); - if (mDistortionType) { - pc.inputs().get("tpcdistortions"); - if (mDistortionType == 2) { - pc.inputs().get("tpcdistortionsderiv"); - mDigitizer.setLumiScaleFactor(); - if (mRecalcDistortions) { - mDigitizer.recalculateDistortions(); - } - } - } - - if (mTPCVDriftHelper.isUpdated()) { - const auto& vd = mTPCVDriftHelper.getVDriftObject(); - LOGP(info, "Updating TPC fast transform map with new VDrift factor of {} wrt reference {} and DriftTimeOffset correction {} wrt {} from source {}", - vd.corrFact, vd.refVDrift, vd.timeOffsetCorr, vd.refTimeOffset, mTPCVDriftHelper.getSourceName()); - mDigitizer.setVDrift(vd.getVDrift()); - mDigitizer.setTDriftOffset(vd.getTimeOffset()); - mTPCVDriftHelper.acknowledgeUpdate(); - } - - if (std::filesystem::exists("ThresholdMap.root")) { - LOG(info) << "TPC: Using zero suppression map from 'ThresholdMap.root'"; - cdb.setThresholdMapFromFile("ThresholdMap.root"); - } - - if (std::filesystem::exists("GainMap.root")) { - LOG(info) << "TPC: Using gain map from 'GainMap.root'"; - cdb.setGainMapFromFile("GainMap.root"); - } + mConditions.update(pc, mDigitizer, [this](float vdrift, float tdriftOffset) { + mDigitizer.setVDrift(vdrift); + mDigitizer.setTDriftOffset(tdriftOffset); + }); for (auto it = pc.inputs().begin(), end = pc.inputs().end(); it != end; ++it) { for (auto const& inputref : it.parts()) { @@ -409,7 +346,7 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer private: o2::tpc::Digitizer mDigitizer; - o2::tpc::VDriftHelper mTPCVDriftHelper{}; + TPCDigitizerConditions mConditions; std::vector mSimChains; SectorFlushBuffers mFlushBuffers; // output of the latest flush std::vector mListOfSectors; // a list of sectors treated by this task @@ -421,11 +358,7 @@ class TPCDPLDigitizerTask : public BaseDPLDigitizer bool mWriteGRP = false; bool mWithMCTruth = true; bool mInternalWriter = false; - bool mUseCalibrationsFromCCDB = false; int mDistortionType = 0; - float mMeanLumiDistortions = -1; - float mMeanLumiDistortionsDerivative = -1; - bool mRecalcDistortions = false; }; o2::framework::DataProcessorSpec getTPCDigitizerSpec(int channel, bool writeGRP, bool mctruth, bool internalwriter, int distortionType) @@ -454,20 +387,14 @@ o2::framework::DataProcessorSpec getTPCDigitizerSpec(int channel, bool writeGRP, } std::vector inputs{InputSpec{"collisioncontext", "SIM", "COLLISIONCONTEXT", static_cast(channel), Lifetime::Timeframe}}; + auto options = TPCDigitizerConditions::options(); + options.push_back({"tpc-sector-seeding", VariantType::Bool, false, {"Seed the random numbers of each sector from DigiParams.seed and the sector, so that the digits do not depend on the lane assignment"}}); return DataProcessorSpec{ id.str().c_str(), inputs, outputs, AlgorithmSpec{adaptFromTask(internalwriter, distortionType)}, - Options{ - {"TPCtriggered", VariantType::Bool, false, {"Impose triggered RO mode (default: continuous)"}}, - {"TPCuseCCDB", VariantType::Bool, false, {"true: load calibrations from CCDB; false: use random calibratoins"}}, - {"meanLumiDistortions", VariantType::Float, -1.f, {"override lumi of distortion object if >=0"}}, - {"meanLumiDistortionsDerivative", VariantType::Float, -1.f, {"override lumi of derivative distortion object if >=0"}}, - {"do-not-recalculate-distortions", VariantType::Bool, false, {"Do not recalculate the distortions"}}, - {"n-threads-distortions", VariantType::Int, 4, {"Number of threads used for the calculation of the distortions"}}, - {"tpc-sector-seeding", VariantType::Bool, false, {"Seed the random numbers of each sector from DigiParams.seed and the sector, so that the digits do not depend on the lane assignment"}}, - }}; + options}; } o2::framework::WorkflowSpec getTPCDigitizerSpec(int nLanes, std::vector const& sectors, bool mctruth, bool internalwriter, int distortionType) @@ -483,16 +410,7 @@ o2::framework::WorkflowSpec getTPCDigitizerSpec(int nLanes, std::vector con pipelineTemplate, nLanes, [size = sectors.size()]() { return size; }, [§ors](size_t index) { return sectors[index]; }); // add the channel for the GRP information to the first processor for (auto& spec : pipelines) { - o2::tpc::VDriftHelper::requestCCDBInputs(spec.inputs); // add the same CCDB request to each pipeline - spec.inputs.emplace_back("gasparam", o2::header::gDataOriginTPC, "TPCGASPARAM", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::ParGas))); - spec.inputs.emplace_back("gemparam", o2::header::gDataOriginTPC, "TPCGEMPARAM", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::ParGEM))); - if (distortionType) { - spec.inputs.emplace_back("tpcdistortions", o2::header::gDataOriginTPC, "TPCDIST", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::DistortionMapMC), {}, 1)); // time-dependent - // load derivative map in case scaling was requested - if (distortionType == 2) { - spec.inputs.emplace_back("tpcdistortionsderiv", o2::header::gDataOriginTPC, "TPCDISTDERIV", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::DistortionMapDerivMC), {}, 1)); // time-dependent - } - } + TPCDigitizerConditions::addInputs(spec.inputs, distortionType); // the same CCDB request for each pipeline } pipelines[0].outputs.emplace_back("TPC", "ROMode", 0, Lifetime::Timeframe); return pipelines; diff --git a/Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx index 00fe9f088efd8..cd859672f9038 100644 --- a/Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx +++ b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.cxx @@ -19,6 +19,17 @@ #include "TPCBase/Sector.h" #include "TPCSimulation/Point.h" #include "TPCSimulation/Digitizer.h" +#include "TPCBaseRecSim/CDBTypes.h" +#include "TPCBase/ParameterGEM.h" +#include "TPCBase/ParameterGas.h" +#include "TPCBaseRecSim/CDBInterface.h" +#include "TPCSpaceCharge/SpaceCharge.h" +#include "Framework/CCDBParamSpec.h" +#include "Framework/ConfigParamRegistry.h" +#include "Framework/InitContext.h" +#include "Framework/InputRecord.h" +#include "Framework/ProcessingContext.h" +#include namespace o2::tpc { @@ -42,12 +53,8 @@ std::string getBranchNameRight(int sector) std::vector> digitizeSector(Digitizer& digitizer, const steer::DigitizationContext& context, const std::vector& simChains, int sector, ULong64_t timeframe, - SectorFlushBuffers& buffers, const std::function& onFlush, - std::mutex* cdbMutex) + SectorFlushBuffers& buffers, const std::function& onFlush) { - // locks cdbMutex, if given, for calls that read calibrations through the CDBInterface - auto cdbLock = [cdbMutex]() { return cdbMutex ? std::unique_lock(*cdbMutex) : std::unique_lock(); }; - std::vector> events; const bool isContinuous = digitizer.isContinuousReadout(); const auto& irecords = context.getEventRecords(); @@ -55,10 +62,7 @@ std::vector> digitizeSector(Digitizer& dig digitizer.setSector(sector); digitizer.setSeedingTimeframe(timeframe); - { - auto lock = cdbLock(); - digitizer.init(); - } + digitizer.init(); size_t digitCounter = 0; auto flush = [&](bool finalFlush = false) { @@ -76,7 +80,6 @@ std::vector> digitizeSector(Digitizer& dig double time = hbfu.getFirstIRofTF(o2::InteractionRecord(0, hbfu.orbitFirstSampled)).bc2ns() / 1000.; digitizer.setOutputDigitTimeOffset(time); if (!irecords.empty()) { - auto lock = cdbLock(); digitizer.setStartTime(irecords[0].getTimeNS() / 1000.f); } } @@ -88,7 +91,6 @@ std::vector> digitizeSector(Digitizer& dig LOG(info) << "TPC: Event time " << eventTime << " us"; digitizer.setEventTime(eventTime); if (!isContinuous) { - auto lock = cdbLock(); digitizer.setStartTime(eventTime); } size_t startSize = digitCounter; @@ -126,4 +128,118 @@ std::vector> digitizeSector(Digitizer& dig return events; } +std::vector TPCDigitizerConditions::options() +{ + using framework::VariantType; + return { + {"TPCtriggered", VariantType::Bool, false, {"Impose triggered RO mode (default: continuous)"}}, + {"TPCuseCCDB", VariantType::Bool, false, {"true: load calibrations from CCDB; false: use random calibratoins"}}, + {"meanLumiDistortions", VariantType::Float, -1.f, {"override lumi of distortion object if >=0"}}, + {"meanLumiDistortionsDerivative", VariantType::Float, -1.f, {"override lumi of derivative distortion object if >=0"}}, + {"do-not-recalculate-distortions", VariantType::Bool, false, {"Do not recalculate the distortions"}}, + {"n-threads-distortions", VariantType::Int, 4, {"Number of threads used for the calculation of the distortions"}}, + }; +} + +void TPCDigitizerConditions::addInputs(std::vector& inputs, int distortionType) +{ + using framework::Lifetime; + VDriftHelper::requestCCDBInputs(inputs); + inputs.emplace_back("gasparam", o2::header::gDataOriginTPC, "TPCGASPARAM", 0, Lifetime::Condition, framework::ccdbParamSpec(CDBTypeMap.at(CDBType::ParGas))); + inputs.emplace_back("gemparam", o2::header::gDataOriginTPC, "TPCGEMPARAM", 0, Lifetime::Condition, framework::ccdbParamSpec(CDBTypeMap.at(CDBType::ParGEM))); + if (distortionType) { + inputs.emplace_back("tpcdistortions", o2::header::gDataOriginTPC, "TPCDIST", 0, Lifetime::Condition, framework::ccdbParamSpec(CDBTypeMap.at(CDBType::DistortionMapMC), {}, 1)); // time-dependent + // load derivative map in case scaling was requested + if (distortionType == 2) { + inputs.emplace_back("tpcdistortionsderiv", o2::header::gDataOriginTPC, "TPCDISTDERIV", 0, Lifetime::Condition, framework::ccdbParamSpec(CDBTypeMap.at(CDBType::DistortionMapDerivMC), {}, 1)); // time-dependent + } + } +} + +void TPCDigitizerConditions::init(framework::InitContext& ic, int distortionType) +{ + mDistortionType = distortionType; + mRecalcDistortions = !(ic.options().get("do-not-recalculate-distortions")); + SpaceCharge::setNThreads(ic.options().get("n-threads-distortions")); + mUseCalibrationsFromCCDB = ic.options().get("TPCuseCCDB"); + mMeanLumiDistortions = ic.options().get("meanLumiDistortions"); + mMeanLumiDistortionsDerivative = ic.options().get("meanLumiDistortionsDerivative"); + LOG(info) << "TPC calibrations from CCDB: " << mUseCalibrationsFromCCDB; +} + +bool TPCDigitizerConditions::finaliseCCDB(framework::ConcreteDataMatcher& matcher, void* obj, Digitizer& digitizer) +{ + using framework::ConcreteDataMatcher; + using SC = SpaceCharge; + if (mVDriftHelper.accountCCDBInputs(matcher, obj)) { + return true; + } + if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCDIST", 0)) { + LOGP(info, "Updating distortion map"); + digitizer.setUseSCDistortions(static_cast(obj)); + if (mMeanLumiDistortions >= 0) { + digitizer.setMeanLumiDistortions(mMeanLumiDistortions); + } + return true; + } + if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCDISTDERIV", 0)) { + LOGP(info, "Updating reference distortion map"); + digitizer.setSCDistortionsDerivative(static_cast(obj)); + if (mMeanLumiDistortionsDerivative >= 0) { + digitizer.setMeanLumiDistortionsDerivative(mMeanLumiDistortionsDerivative); + } + return true; + } + if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCGASPARAM", 0)) { + LOGP(info, "TPC gas param updated"); + ParameterGas::Instance().printKeyValues(true, true); + return true; + } + if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCGEMPARAM", 0)) { + LOGP(info, "TPC GEM param updated"); + ParameterGEM::Instance().printKeyValues(true, true); + return true; + } + return false; +} + +void TPCDigitizerConditions::update(framework::ProcessingContext& pc, Digitizer& digitizer, const std::function& setVDrift) +{ + using SC = SpaceCharge; + auto& cdb = CDBInterface::instance(); + cdb.setUseDefaults(!mUseCalibrationsFromCCDB); + // whatever are global settings for CCDB usage, we have to extract the TPC vdrift from CCDB for anchored simulations + mVDriftHelper.extractCCDBInputs(pc); + pc.inputs().get("gasparam"); + pc.inputs().get("gemparam"); + if (mDistortionType) { + pc.inputs().get("tpcdistortions"); + if (mDistortionType == 2) { + pc.inputs().get("tpcdistortionsderiv"); + digitizer.setLumiScaleFactor(); + if (mRecalcDistortions) { + digitizer.recalculateDistortions(); + } + } + } + + if (mVDriftHelper.isUpdated()) { + const auto& vd = mVDriftHelper.getVDriftObject(); + LOGP(info, "Updating TPC fast transform map with new VDrift factor of {} wrt reference {} and DriftTimeOffset correction {} wrt {} from source {}", + vd.corrFact, vd.refVDrift, vd.timeOffsetCorr, vd.refTimeOffset, mVDriftHelper.getSourceName()); + setVDrift(vd.getVDrift(), vd.getTimeOffset()); + mVDriftHelper.acknowledgeUpdate(); + } + + if (std::filesystem::exists("ThresholdMap.root")) { + LOG(info) << "TPC: Using zero suppression map from 'ThresholdMap.root'"; + cdb.setThresholdMapFromFile("ThresholdMap.root"); + } + + if (std::filesystem::exists("GainMap.root")) { + LOG(info) << "TPC: Using gain map from 'GainMap.root'"; + cdb.setGainMapFromFile("GainMap.root"); + } +} + } // namespace o2::tpc diff --git a/Steer/DigitizerWorkflow/src/TPCSectorDigitization.h b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.h index ed22cd0999519..daf417e21ae73 100644 --- a/Steer/DigitizerWorkflow/src/TPCSectorDigitization.h +++ b/Steer/DigitizerWorkflow/src/TPCSectorDigitization.h @@ -13,7 +13,6 @@ #define STEER_DIGITIZERWORKFLOW_TPCSECTORDIGITIZATION_H_ #include -#include #include #include #include "CommonDataFormat/RangeReference.h" @@ -21,6 +20,10 @@ #include "SimulationDataFormat/MCCompLabel.h" #include "SimulationDataFormat/MCTruthContainer.h" #include "TPCSimulation/CommonMode.h" +#include "TPCCalibration/VDriftHelper.h" +#include "Framework/ConcreteDataMatcher.h" +#include "Framework/ConfigParamSpec.h" +#include "Framework/InputSpec.h" class TChain; @@ -30,6 +33,11 @@ namespace steer { class DigitizationContext; } +namespace framework +{ +class InitContext; +class ProcessingContext; +} // namespace framework namespace tpc { class Digitizer; @@ -41,15 +49,36 @@ struct SectorFlushBuffers { std::vector commonMode; }; -/// Digitize all collisions of one sector of a timeframe. -/// After each flush the new output is in buffers and onFlush is called on the calling thread. -/// The function makes no DPL calls. Concurrent calls need one Digitizer with sector seeding and one set of chains -/// per thread, and a common cdbMutex, which serialises the calls that read calibrations through the CDBInterface. +/// Digitize all collisions of one sector of a timeframe, calling onFlush after each flush /// \return the digit groups, one per collision part in triggered mode, one for the timeframe in continuous mode std::vector> digitizeSector(Digitizer& digitizer, const steer::DigitizationContext& context, const std::vector& simChains, int sector, ULong64_t timeframe, - SectorFlushBuffers& buffers, const std::function& onFlush, - std::mutex* cdbMutex = nullptr); + SectorFlushBuffers& buffers, const std::function& onFlush); + +/// CCDB conditions and calibration files of the TPC digitizer devices +class TPCDigitizerConditions +{ + public: + /// options common to the TPC digitizer devices + static std::vector options(); + /// CCDB inputs common to the TPC digitizer devices + static void addInputs(std::vector& inputs, int distortionType); + + void init(framework::InitContext& ic, int distortionType); + /// pass a CCDB object to the digitizer; true if it was a TPC condition + bool finaliseCCDB(framework::ConcreteDataMatcher& matcher, void* obj, Digitizer& digitizer); + /// fetch the conditions of a timeframe and prepare the digitizer, which then holds the distortions + /// \param setVDrift called with the drift velocity and time offset when they changed + void update(framework::ProcessingContext& pc, Digitizer& digitizer, const std::function& setVDrift); + + private: + VDriftHelper mVDriftHelper{}; + int mDistortionType = 0; + bool mUseCalibrationsFromCCDB = false; + bool mRecalcDistortions = false; + float mMeanLumiDistortions = -1; + float mMeanLumiDistortionsDerivative = -1; +}; } // namespace tpc } // namespace o2 From b25a4f3059b666ee837f086b3d40920193cb1ec9 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Mon, 9 Nov 2026 19:41:06 +0100 Subject: [PATCH 11/11] Simplify the multithreaded TPC digitizer after the second review This addresses the second review of the multithreaded TPC digitizer device. - Old lane files are found with o2::utils::listFiles, as in the chunked merger. - Unused includes and the SpaceCharge alias are removed from the device. - setSectorSeedingFrom fills the rings of the source digitizer, so initSectorSeeding is private. - The seeding documentation states that the sectors share the ring contents. https://its.cern.ch/jira/browse/O2-7309 Co-Authored-By: Claude Opus 5.5 --- .../simulation/include/TPCSimulation/Digitizer.h | 13 +++++++------ Detectors/TPC/simulation/src/Digitizer.cxx | 3 ++- .../DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx | 16 +++------------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h index 4c60102109214..63f7de331cc26 100644 --- a/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h +++ b/Detectors/TPC/simulation/include/TPCSimulation/Digitizer.h @@ -147,23 +147,24 @@ class Digitizer void setSCDistortionsFrom(const Digitizer& other); /// Use digitizer-owned random rings filled from seed and positioned per sector and timeframe in init(), - /// so that the digits of a sector do not depend on which sectors were processed before + /// so that the digits of a sector do not depend on which sectors were processed before. + /// All sectors share the ring contents and differ in the start positions, as with the singleton rings. /// \param seed base seed, typically DigiParams.seed void setUseSectorSeeding(ULong64_t seed); /// Set the timeframe identifier that enters the sector seeding void setSeedingTimeframe(ULong64_t timeframe) { mSeedingTimeframe = timeframe; } - /// Fill the random rings of the sector seeding, if not done yet - void initSectorSeeding(); - - /// Use copies of the sector-seeding random rings of another digitizer - void setSectorSeedingFrom(const Digitizer& other); + /// Use copies of the sector-seeding random rings of another digitizer, which fills them if needed + void setSectorSeedingFrom(Digitizer& other); /// Whether flush() also flushes the process-wide debug streamer void setFlushDebugStreamer(bool flush) { mFlushDebugStreamer = flush; } private: + /// Fill the random rings of the sector seeding, if not done yet + void initSectorSeeding(); + ElectronTransport& electronTransport(); GEMAmplification& gemAmplification(); SAMPAProcessing& sampaProcessing(); diff --git a/Detectors/TPC/simulation/src/Digitizer.cxx b/Detectors/TPC/simulation/src/Digitizer.cxx index 114afbbcffd6d..9c7cd1b3649cb 100644 --- a/Detectors/TPC/simulation/src/Digitizer.cxx +++ b/Detectors/TPC/simulation/src/Digitizer.cxx @@ -100,8 +100,9 @@ void Digitizer::initSectorSeeding() } } -void Digitizer::setSectorSeedingFrom(const Digitizer& other) +void Digitizer::setSectorSeedingFrom(Digitizer& other) { + other.initSectorSeeding(); mUseSectorSeeding = other.mUseSectorSeeding; mSeed = other.mSeed; if (other.mGEMAmplification) { diff --git a/Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx b/Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx index 1ee6595247b58..99075e4e1669b 100644 --- a/Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx +++ b/Steer/DigitizerWorkflow/src/TPCDigitizerMTSpec.cxx @@ -18,7 +18,6 @@ #include "Framework/DataProcessorSpec.h" #include "Framework/DataRefUtils.h" #include "Framework/Lifetime.h" -#include "Framework/CCDBParamSpec.h" #include "Framework/Task.h" #include "Framework/Logger.h" #include "Headers/DataHeader.h" @@ -28,13 +27,9 @@ #include "DetectorsBase/Detector.h" #include "SimConfig/DigiParams.h" #include "SimulationDataFormat/DigitizationContext.h" -#include "TPCBaseRecSim/CDBInterface.h" -#include "TPCBase/ParameterGEM.h" -#include "TPCBase/ParameterGas.h" -#include "TPCCalibration/VDriftHelper.h" #include "TPCSimulation/Digitizer.h" -#include "TPCSpaceCharge/SpaceCharge.h" #include "CommonUtils/DebugStreamer.h" +#include "CommonUtils/FileSystemUtils.h" #include #include #include @@ -53,7 +48,6 @@ #include using namespace o2::framework; -using SC = o2::tpc::SpaceCharge; namespace o2 { @@ -142,11 +136,8 @@ class TPCDPLDigitizerMTTask : public o2::base::BaseDPLDigitizer } // the merger picks up every lane file in the directory, so files of an earlier run must not survive - for (const auto& entry : std::filesystem::directory_iterator(".")) { - const auto name = entry.path().filename().string(); - if (name.rfind("tpc_driftime_digits_lane", 0) == 0 && entry.path().extension() == ".root") { - std::filesystem::remove(entry.path()); - } + for (const auto& name : o2::utils::listFiles("tpc_driftime_digits_lane.*.root$")) { + std::filesystem::remove(name); } ROOT::EnableThreadSafety(); @@ -176,7 +167,6 @@ class TPCDPLDigitizerMTTask : public o2::base::BaseDPLDigitizer } if (!mSeedingShared) { // the rings depend only on the seed, so they are filled once and copied - primary.initSectorSeeding(); for (size_t i = 1; i < mWorkers.size(); ++i) { mWorkers[i]->digitizer.setSectorSeedingFrom(primary); }