From 19866a3d3daa4df6606ee68fd97e269757200c02 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sun, 20 Sep 2026 06:49:19 +0200 Subject: [PATCH 1/6] Give every timeframe its own slot in the collision context This fixes a problem in the timeframe index structure of the collision context and adds a unit test. - getTimeFrameBoundaries closed only one timeframe per collision, so a timeframe without collisions was left out of the index structure entirely and the collisions after it were assigned to the wrong timeframe. - The number of extracted per-timeframe contexts was therefore the number of non-empty timeframes, not the number of timeframes asked for, and the last tf/collisioncontext.root could be missing. - The scan now closes every timeframe a collision skips over and pads the result to the number of timeframes the caller asks for, so entry i always describes orbits [start + i*orbitsPerTF, start + (i+1)*orbitsPerTF). - applyMaxCollisionFilter keeps an empty timeframe empty when it re-indexes, and extractSingleTimeframe returns a valid empty context for it. - o2-steer-colcontexttool passes the number of timeframes it asked for, reports timeframes that came out empty together with the mean number of collisions per timeframe implied by the interaction rate, and refuses to continue when --noEmptyTF was requested. https://its.cern.ch/jira/browse/O2-7132 Co-Authored-By: Claude Opus 5 --- DataFormats/simulation/CMakeLists.txt | 5 + .../DigitizationContext.h | 6 +- .../simulation/src/DigitizationContext.cxx | 61 ++++++--- .../test/testDigitizationContext.cxx | 127 ++++++++++++++++++ Steer/src/CollisionContextTool.cxx | 47 ++++++- 5 files changed, 228 insertions(+), 18 deletions(-) create mode 100644 DataFormats/simulation/test/testDigitizationContext.cxx diff --git a/DataFormats/simulation/CMakeLists.txt b/DataFormats/simulation/CMakeLists.txt index 33c91337c77e9..f9001272b70df 100644 --- a/DataFormats/simulation/CMakeLists.txt +++ b/DataFormats/simulation/CMakeLists.txt @@ -55,6 +55,11 @@ o2_target_root_dictionary( # * src/SimulationDataLinkDef.h # * and not src/SimulationDataFormatLinkDef.h +o2_add_test(DigitizationContext + SOURCES test/testDigitizationContext.cxx + COMPONENT_NAME SimulationDataFormat + PUBLIC_LINK_LIBRARIES O2::SimulationDataFormat) + o2_add_test(InteractionSampler SOURCES test/testInteractionSampler.cxx COMPONENT_NAME SimulationDataFormat diff --git a/DataFormats/simulation/include/SimulationDataFormat/DigitizationContext.h b/DataFormats/simulation/include/SimulationDataFormat/DigitizationContext.h index 0dc3806e52cf2..54cf81d452cd3 100644 --- a/DataFormats/simulation/include/SimulationDataFormat/DigitizationContext.h +++ b/DataFormats/simulation/include/SimulationDataFormat/DigitizationContext.h @@ -135,7 +135,11 @@ class DigitizationContext void applyMaxCollisionFilter(std::vector>& timeframeindices, long startOrbit, long orbitsPerTF, int maxColl, double orbitsEarly = 0.); /// get timeframe structure --> index markers where timeframe starts/ends/is_influenced_by - std::vector> calcTimeframeIndices(long startOrbit, long orbitsPerTF, double orbitsEarly = 0.) const; + /// One entry is produced per timeframe, including timeframes which contain no collision at all. + /// nTimeframes is the number of timeframes the caller asked for; when given, the result has exactly + /// that many entries, so that a timeframe without collisions keeps its own slot instead of shifting + /// all later timeframes down by one. + std::vector> calcTimeframeIndices(long startOrbit, long orbitsPerTF, double orbitsEarly = 0., long nTimeframes = -1) const; // Sample and fix interaction vertices (according to some distribution). Makes sure that same event ids // have to have same vertex, as well as event ids associated to same collision. diff --git a/DataFormats/simulation/src/DigitizationContext.cxx b/DataFormats/simulation/src/DigitizationContext.cxx index 79e36aa9fa48b..1c41dce797cc4 100644 --- a/DataFormats/simulation/src/DigitizationContext.cxx +++ b/DataFormats/simulation/src/DigitizationContext.cxx @@ -389,20 +389,33 @@ void DigitizationContext::fillQED(std::string_view QEDprefix, std::vector> getTimeFrameBoundaries(std::vector const& irecords, long startOrbit, long orbitsPerTF) +// One entry is produced per timeframe. A timeframe without collisions gets an empty range +// (first > second) rather than being left out, so that entry i always describes the timeframe +// covering orbits [startOrbit + i * orbitsPerTF, startOrbit + (i+1) * orbitsPerTF). +// nTimeframes, when positive, is the number of timeframes the caller asked for; the result is +// padded with empty timeframes (or truncated) to exactly that length. +std::vector> getTimeFrameBoundaries(std::vector const& irecords, long startOrbit, long orbitsPerTF, long nTimeframes = -1) { std::vector> result; + auto pad_and_return = [&result, nTimeframes](int index) { + if (nTimeframes > 0) { + while ((long)result.size() < nTimeframes) { + result.emplace_back(std::pair(index, index - 1)); // an empty timeframe + } + result.resize(nTimeframes); + } + return result; + }; + // the goal is to determine timeframe boundaries inside the interaction record vectors - // determine if we can do anything if (irecords.size() == 0) { - // nothing to do - return result; + return pad_and_return(0); } if (irecords.back().orbit < startOrbit) { LOG(error) << "start orbit larger than last collision entry"; - return result; + return pad_and_return((int)irecords.size()); } // skip to the first index falling within our constrained @@ -413,10 +426,13 @@ std::vector> getTimeFrameBoundaries(std::vector= startOrbit + timeframe_count * orbitsPerTF) { - // we finished one timeframe + // a collision may lie several timeframes ahead of the previous one; close every timeframe it + // skips over, as an empty one, so that the collision ends up in the timeframe it belongs to. + // (A plain "if" here closed only one timeframe per collision, which both dropped the empty + // timeframes and mis-assigned the collisions after them.) + while (irecords[right].orbit >= startOrbit + timeframe_count * orbitsPerTF) { result.emplace_back(std::pair(left, right - 1)); timeframe_count++; left = right; @@ -425,17 +441,18 @@ std::vector> getTimeFrameBoundaries(std::vector(left, right - 1)); - return result; + return pad_and_return((int)irecords.size()); } // a common helper for timeframe structure - includes indices for orbits-early (orbits from last timeframe still affecting current one) std::vector> getTimeFrameBoundaries(std::vector const& irecords, long startOrbit, long orbitsPerTF, - float orbitsEarly) + float orbitsEarly, + long nTimeframes = -1) { // we could actually use the other method first ... then do another pass to fix the early-index ... or impact index - auto true_indices = getTimeFrameBoundaries(irecords, startOrbit, orbitsPerTF); + auto true_indices = getTimeFrameBoundaries(irecords, startOrbit, orbitsPerTF, nTimeframes); std::vector> indices_with_early{}; for (int ti = 0; ti < true_indices.size(); ++ti) { @@ -447,7 +464,7 @@ std::vector> getTimeFrameBoundaries(std::vector 0. && ti > 0) { + if (orbitsEarly > 0. && ti > 0 && tf_range.first <= tf_range.second) { auto& prev_tf_range = true_indices[ti - 1]; // in this range search the smallest index which precedes // timeframe ti by not more than "orbitsEarly" orbits @@ -518,7 +535,8 @@ void DigitizationContext::applyMaxCollisionFilter(std::vector= 0 ? previndex : firstindex; index <= lastindex; ++index) { if (collCount >= maxColl) { @@ -571,6 +589,14 @@ void DigitizationContext::applyMaxCollisionFilter(std::vector(tf_indices) = (int)newrecords.size(); + std::get<1>(tf_indices) = (int)newrecords.size() - 1; + std::get<2>(tf_indices) = -1; + continue; + } if (indices_old_to_new.find(firstindex) != indices_old_to_new.end()) { std::get<0>(tf_indices) = indices_old_to_new[firstindex]; // start } @@ -588,9 +614,9 @@ void DigitizationContext::applyMaxCollisionFilter(std::vector> DigitizationContext::calcTimeframeIndices(long startOrbit, long orbitsPerTF, double orbitsEarly) const +std::vector> DigitizationContext::calcTimeframeIndices(long startOrbit, long orbitsPerTF, double orbitsEarly, long nTimeframes) const { - auto timeframeindices = getTimeFrameBoundaries(mEventRecords, startOrbit, orbitsPerTF, orbitsEarly); + auto timeframeindices = getTimeFrameBoundaries(mEventRecords, startOrbit, orbitsPerTF, orbitsEarly, nTimeframes); return timeframeindices; } @@ -710,6 +736,11 @@ DigitizationContext DigitizationContext::extractSingleTimeframe(int timeframeid, if (earlyindex >= 0) { startindex = earlyindex; } + if (endindex < startindex) { + // a timeframe without any collision: return a valid but empty context rather than + // copying a negative range + endindex = startindex; + } std::copy(mEventRecords.begin() + startindex, mEventRecords.begin() + endindex, std::back_inserter(r.mEventRecords)); std::copy(mEventParts.begin() + startindex, mEventParts.begin() + endindex, std::back_inserter(r.mEventParts)); if (mInteractionVertices.size() >= endindex) { diff --git a/DataFormats/simulation/test/testDigitizationContext.cxx b/DataFormats/simulation/test/testDigitizationContext.cxx new file mode 100644 index 0000000000000..122c13cbcdd25 --- /dev/null +++ b/DataFormats/simulation/test/testDigitizationContext.cxx @@ -0,0 +1,127 @@ +// 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. + +#define BOOST_TEST_MODULE Test DigitizationContext class +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK + +#include +#include "SimulationDataFormat/DigitizationContext.h" +#include + +namespace o2 +{ + +// build a context whose collisions sit at the given orbits (one collision each, source 0) +steer::DigitizationContext makeContext(std::vector const& orbits) +{ + steer::DigitizationContext ctx; + auto& records = ctx.getEventRecords(); + auto& parts = ctx.getEventParts(); + int entry = 0; + for (auto o : orbits) { + records.emplace_back(o2::InteractionTimeRecord(o2::InteractionRecord(0, o), 0.)); + parts.push_back({steer::EventPart(0, entry++)}); + } + ctx.setNCollisions(records.size()); + ctx.setMaxNumberParts(1); + return ctx; +} + +// The timeframe index structure must have one entry per timeframe asked for, and entry i must +// describe exactly the collisions falling into orbits [start + i*orbitsPerTF, start + (i+1)*orbitsPerTF). +BOOST_AUTO_TEST_CASE(TimeframeIndicesAreSlotAligned) +{ + long const orbitsPerTF = 6; + long const start = 0; + long const nTF = 5; // orbits 0..29 + + // timeframe 1 (orbits 6..11) and timeframe 4 (orbits 24..29) hold no collision + std::vector orbits{0, 3, 5, 12, 14, 17, 18, 21}; + auto ctx = makeContext(orbits); + + auto indices = ctx.calcTimeframeIndices(start, orbitsPerTF, 0., nTF); + BOOST_CHECK_EQUAL(indices.size(), (size_t)nTF); + + for (int tf = 0; tf < nTF; ++tf) { + auto first = std::get<0>(indices[tf]); + auto last = std::get<1>(indices[tf]); + long const lo = start + tf * orbitsPerTF; + long const hi = lo + orbitsPerTF; + // count what should be in this timeframe + int expected = 0; + for (auto o : orbits) { + if (o >= lo && o < hi) { + expected++; + } + } + BOOST_CHECK_EQUAL(last - first + 1, expected); + for (int i = first; i <= last; ++i) { + BOOST_CHECK(orbits[i] >= lo); + BOOST_CHECK(orbits[i] < hi); + } + } +} + +// A timeframe without collisions must survive extraction as a valid, empty context +BOOST_AUTO_TEST_CASE(EmptyTimeframeExtracts) +{ + long const orbitsPerTF = 6; + long const nTF = 3; + auto ctx = makeContext({0, 2, 13}); // timeframe 1 (orbits 6..11) is empty + auto indices = ctx.calcTimeframeIndices(0, orbitsPerTF, 0., nTF); + BOOST_CHECK_EQUAL(indices.size(), (size_t)nTF); + + auto tf0 = ctx.extractSingleTimeframe(0, indices, {}); + auto tf1 = ctx.extractSingleTimeframe(1, indices, {}); + auto tf2 = ctx.extractSingleTimeframe(2, indices, {}); + BOOST_CHECK_EQUAL(tf0.getEventRecords().size(), (size_t)2); + BOOST_CHECK_EQUAL(tf1.getEventRecords().size(), (size_t)0); + BOOST_CHECK_EQUAL(tf2.getEventRecords().size(), (size_t)1); + BOOST_CHECK_EQUAL(tf2.getEventRecords()[0].orbit, 13); +} + +// The trailing timeframes of the requested range must be present even when the last collision +// falls well before the end of the range +BOOST_AUTO_TEST_CASE(TrailingTimeframesArePresent) +{ + long const orbitsPerTF = 6; + long const nTF = 9; // this is what an 8-timeframe anchored MC job with orbitsEarly asks for + auto ctx = makeContext({1, 2, 7}); + auto indices = ctx.calcTimeframeIndices(0, orbitsPerTF, 0., nTF); + BOOST_CHECK_EQUAL(indices.size(), (size_t)nTF); + for (int tf = 2; tf < nTF; ++tf) { + BOOST_CHECK(std::get<0>(indices[tf]) > std::get<1>(indices[tf])); // empty, but present + } +} + +// applyMaxCollisionFilter must not shift timeframes when one of them is empty +BOOST_AUTO_TEST_CASE(MaxCollisionFilterKeepsSlots) +{ + long const orbitsPerTF = 6; + long const nTF = 4; + // tf0: orbits 0,1,2 tf1: empty tf2: orbits 12,13 tf3: orbit 19 + auto ctx = makeContext({0, 1, 2, 12, 13, 19}); + auto indices = ctx.calcTimeframeIndices(0, orbitsPerTF, 0., nTF); + ctx.applyMaxCollisionFilter(indices, 0, orbitsPerTF, 2, 0.); // keep at most 2 per timeframe + + BOOST_CHECK_EQUAL(indices.size(), (size_t)nTF); + BOOST_CHECK_EQUAL(std::get<1>(indices[0]) - std::get<0>(indices[0]) + 1, 2); // capped + BOOST_CHECK(std::get<0>(indices[1]) > std::get<1>(indices[1])); // still empty + BOOST_CHECK_EQUAL(std::get<1>(indices[2]) - std::get<0>(indices[2]) + 1, 2); + BOOST_CHECK_EQUAL(std::get<1>(indices[3]) - std::get<0>(indices[3]) + 1, 1); + + auto tf2 = ctx.extractSingleTimeframe(2, indices, {}); + BOOST_CHECK_EQUAL(tf2.getEventRecords().size(), (size_t)2); + BOOST_CHECK_EQUAL(tf2.getEventRecords()[0].orbit, 12); +} + +} // namespace o2 diff --git a/Steer/src/CollisionContextTool.cxx b/Steer/src/CollisionContextTool.cxx index e97eeada3fd0c..3bf0516ee6525 100644 --- a/Steer/src/CollisionContextTool.cxx +++ b/Steer/src/CollisionContextTool.cxx @@ -20,6 +20,7 @@ #include "SimulationDataFormat/DigitizationContext.h" #include "SimConfig/InteractionDiamondParam.h" #include "DataFormatsFT0/EventsPerBc.h" +#include "CommonConstants/LHCConstants.h" #include #include #include @@ -238,7 +239,7 @@ bool parseOptions(int argc, char* argv[], Options& optvalues) "timeframeID", bpo::value(&optvalues.tfid)->default_value(0), "Timeframe id of the first timeframe int this context. Allows to generate contexts for different start orbits")( "first-orbit", bpo::value(&optvalues.firstFractionalOrbit)->default_value(0), "First (fractional) orbit in the run (HBFUtils.firstOrbit + BC from decimal)")( "maxCollsPerTF", bpo::value(&optvalues.maxCollsPerTF)->default_value(-1), "Maximal number of MC collisions to put into one timeframe. By default no constraint.")( - "noEmptyTF", bpo::bool_switch(&optvalues.noEmptyTF), "Enforce to have at least one collision")( + "noEmptyTF", bpo::bool_switch(&optvalues.noEmptyTF), "Fail if any of the timeframes asked for ends up without a collision (and shift the first collision into the sampled orbit range)")( "configKeyValues", bpo::value(&optvalues.configKeyValues)->default_value(""), "Semicolon separated key=value strings (e.g.: 'TPC.gasDensity=1;...')")( "with-vertices", bpo::value(&optvalues.vertexModeString)->default_value("kNoVertex"), "Assign vertices to collisions. Argument is the vertex mode. Defaults to no vertexing applied")( "timestamp", bpo::value(&optvalues.timestamp)->default_value(-1L), "Timestamp for CCDB queries / anchoring")( @@ -660,7 +661,10 @@ int main(int argc, char* argv[]) } LOG(info) << "-------- DENSE CONTEXT ------->>"; - auto timeframeindices = digicontext.calcTimeframeIndices(orbitstart, options.orbitsPerTF, options.orbitsEarly); + // the number of timeframes we were asked for; passing it makes sure that a timeframe without + // collisions keeps its own slot instead of shifting every later timeframe down by one + long const num_timeframes_asked = usetimeframelength ? (orbits_total / options.orbitsPerTF) : -1; + auto timeframeindices = digicontext.calcTimeframeIndices(orbitstart, options.orbitsPerTF, options.orbitsEarly, num_timeframes_asked); LOG(info) << "Fixed " << timeframeindices.size() << " timeframes "; for (auto p : timeframeindices) { LOG(info) << std::get<0>(p) << " " << std::get<1>(p) << " " << std::get<2>(p); @@ -684,6 +688,45 @@ int main(int argc, char* argv[]) auto numTimeFrames = timeframeindices.size(); // digicontext.finalizeTimeframeStructure(orbitstart, options.orbitsPerTF, options.orbitsEarly); + // report - and, if asked, refuse - timeframes without a single collision. A timeframe with no + // collision cannot be simulated, and the rest of the MC workflow expects one collision context + // file per timeframe, so this has to be visible here and not five hours later in the simulation. + { + std::vector empty_timeframes; + auto const first_real_tf = options.orbitsEarly > 0. ? 1 : 0; + for (int tf_id = first_real_tf; tf_id < (int)numTimeFrames; ++tf_id) { + if (std::get<0>(timeframeindices[tf_id]) > std::get<1>(timeframeindices[tf_id])) { + empty_timeframes.push_back(tf_id - first_real_tf + 1); + } + } + if (!empty_timeframes.empty()) { + std::stringstream tflist; + for (auto tf : empty_timeframes) { + tflist << " tf" << tf; + } + // the mean number of collisions in one timeframe, from the rate we were given + auto const tf_length_s = options.orbitsPerTF * o2::constants::lhc::LHCOrbitMUS * 1e-6; + double rate = 0.; + for (auto& p : ispecs) { + rate = std::max(rate, (double)p.interactionRate); + } + auto const mu_per_tf = rate * tf_length_s; + LOG(warn) << empty_timeframes.size() << " of " << (numTimeFrames - first_real_tf) + << " timeframes contain no collision:" << tflist.str(); + LOG(warn) << "with interaction rate " << rate << " Hz and " << options.orbitsPerTF + << " orbits per timeframe there are only " << mu_per_tf + << " collisions per timeframe on average, so a fraction " << std::exp(-mu_per_tf) + << " of the timeframes comes out empty"; + if (mu_per_tf > 0.) { + LOG(warn) << "use at least " << (int)std::ceil(8. / (rate * o2::constants::lhc::LHCOrbitMUS * 1e-6)) + << " orbits per timeframe to keep that fraction below 1 per mille"; + } + if (options.noEmptyTF) { + LOG(fatal) << "--noEmptyTF was requested but timeframes without collisions were produced; refusing to continue"; + } + } + } + if (options.vertexMode != o2::conf::VertexMode::kNoVertex) { switch (options.vertexMode) { case o2::conf::VertexMode::kCCDB: { From 98a5696f3125a12e3c8e09febcea01f23abf1472 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sun, 20 Sep 2026 06:49:19 +0200 Subject: [PATCH 2/6] Let the TPC looper generator accept a timeframe without collisions This fixes a problem in GenTPCLoopers::setFlatGas when the collision context of a timeframe is empty. - A timeframe holds no collision whenever the interaction rate is low enough, and the generator called exit(1) on it. - The extent of the timeframe now comes from HBFUtils in that case, which is where it is defined, instead of from the last collision. - With a single collision in the timeframe the mean interaction spacing was divided by zero; it is now taken from the interaction rate stored in the collision context. https://its.cern.ch/jira/browse/O2-7132 Co-Authored-By: Claude Opus 5 --- Generators/src/TPCLoopers.cxx | 36 +++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/Generators/src/TPCLoopers.cxx b/Generators/src/TPCLoopers.cxx index 6e5af7c0c84d8..28a66c1a27b7d 100644 --- a/Generators/src/TPCLoopers.cxx +++ b/Generators/src/TPCLoopers.cxx @@ -398,23 +398,35 @@ void GenTPCLoopers::setFlatGas(Bool_t flat, Int_t number, Int_t nloopers_orbit) mContextFile = std::filesystem::exists("collisioncontext.root") ? TFile::Open("collisioncontext.root") : nullptr; mCollisionContext = mContextFile ? (o2::steer::DigitizationContext*)mContextFile->Get("DigitizationContext") : nullptr; mInteractionTimeRecords = mCollisionContext ? mCollisionContext->getEventRecords() : std::vector{}; + const auto& hbfUtils = o2::raw::HBFUtils::Instance(); if (mInteractionTimeRecords.empty()) { - LOG(error) << "Error: No interaction time records found in the collision context!"; - exit(1); + // A timeframe can legitimately contain no collision at all when the interaction rate is + // low. No event is transported in that case, so nothing below is ever used; take the + // extent of the timeframe from HBFUtils rather than from the (absent) collisions. + LOG(warn) << "No interaction time records in the collision context; this timeframe holds no collision"; + o2::InteractionRecord tfEndIR(0, hbfUtils.orbitFirstSampled + hbfUtils.nHBFPerTF); + mTimeEnd = tfEndIR.bc2ns(); } else { LOG(info) << "Interaction Time records has " << mInteractionTimeRecords.size() << " entries."; mCollisionContext->printCollisionSummary(); + for (int c = 0; c < (int)mInteractionTimeRecords.size() - 1; c++) { + mIntTimeRecMean += mInteractionTimeRecords[c + 1].bc2ns() - mInteractionTimeRecords[c].bc2ns(); + } + if (mInteractionTimeRecords.size() > 1) { + mIntTimeRecMean /= (mInteractionTimeRecords.size() - 1); // Average interaction time record used as reference + } else { + // a single collision gives no spacing to average; use the one implied by the rate + auto rate = mCollisionContext->getDigitizerInteractionRate(); + mIntTimeRecMean = rate > 0. ? 1.e9 / rate : (double)o2::constants::lhc::LHCOrbitNS; + LOG(info) << "Only one collision in this timeframe; taking " << mIntTimeRecMean + << " ns as the mean interaction spacing from the interaction rate"; + } + // Get the start time of the second orbit after the last interaction record + const auto& lastIR = mInteractionTimeRecords.back(); + o2::InteractionRecord finalOrbitIR(0, lastIR.orbit + 2); // Final orbit, BC = 0 + mTimeEnd = finalOrbitIR.bc2ns(); + LOG(debug) << "Final orbit start time: " << mTimeEnd << " ns while last interaction record time is " << mInteractionTimeRecords.back().bc2ns() << " ns"; } - for (int c = 0; c < mInteractionTimeRecords.size() - 1; c++) { - mIntTimeRecMean += mInteractionTimeRecords[c + 1].bc2ns() - mInteractionTimeRecords[c].bc2ns(); - } - mIntTimeRecMean /= (mInteractionTimeRecords.size() - 1); // Average interaction time record used as reference - const auto& hbfUtils = o2::raw::HBFUtils::Instance(); - // Get the start time of the second orbit after the last interaction record - const auto& lastIR = mInteractionTimeRecords.back(); - o2::InteractionRecord finalOrbitIR(0, lastIR.orbit + 2); // Final orbit, BC = 0 - mTimeEnd = finalOrbitIR.bc2ns(); - LOG(debug) << "Final orbit start time: " << mTimeEnd << " ns while last interaction record time is " << mInteractionTimeRecords.back().bc2ns() << " ns"; } } else { mFlatGasNumber = -1; From b0cec484ed8a669749ab8235dcf7d5f6356beb8d Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sun, 20 Sep 2026 06:49:19 +0200 Subject: [PATCH 3/6] Let the MCH digitiser accept a timeframe without collisions This fixes a segmentation fault in MCHDPLDigitizerTask when the collision context of a timeframe is empty. - The noise-only signal range was taken from eventRecords.front() and eventRecords.back(), which is undefined behaviour on an empty vector. - A timeframe holds no collision whenever the interaction rate is low. - The range now comes from HBFUtils in that case, so the noise covers the timeframe that is actually being digitised. https://its.cern.ch/jira/browse/O2-7132 Co-Authored-By: Claude Opus 5 --- .../DigitizerWorkflow/src/MCHDigitizerSpec.cxx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Steer/DigitizerWorkflow/src/MCHDigitizerSpec.cxx b/Steer/DigitizerWorkflow/src/MCHDigitizerSpec.cxx index e2a6397f1a2cf..51102ce2421ca 100644 --- a/Steer/DigitizerWorkflow/src/MCHDigitizerSpec.cxx +++ b/Steer/DigitizerWorkflow/src/MCHDigitizerSpec.cxx @@ -15,6 +15,7 @@ #include "DataFormatsMCH/ROFRecord.h" #include "DataFormatsParameters/GRPObject.h" #include "DetectorsBase/BaseDPLDigitizer.h" +#include "DetectorsRaw/HBFUtils.h" #include "Framework/ConfigParamRegistry.h" #include "Framework/ControlService.h" #include "Framework/DataProcessorSpec.h" @@ -102,9 +103,20 @@ class MCHDPLDigitizerTask : public o2::base::BaseDPLDigitizer } } - // generate noise-only signals between first and last collisions ± 100 BC (= 25 ADC samples) - auto firstIR = InteractionRecord::long2IR(std::max(int64_t(0), eventRecords.front().toLong() - timeOffset - 100)); - auto lastIR = InteractionRecord::long2IR(std::max(int64_t(0), eventRecords.back().toLong() - timeOffset + 100)); + // generate noise-only signals between first and last collisions ± 100 BC (= 25 ADC samples). + // A timeframe can hold no collision at all when the interaction rate is low; take the range + // from the timeframe itself in that case, since there are no collisions to take it from. + int64_t firstLong, lastLong; + if (eventRecords.empty()) { + const auto& hbf = o2::raw::HBFUtils::Instance(); + firstLong = InteractionRecord(0, hbf.orbitFirstSampled).toLong(); + lastLong = InteractionRecord(0, hbf.orbitFirstSampled + hbf.nHBFPerTF).toLong(); + } else { + firstLong = eventRecords.front().toLong(); + lastLong = eventRecords.back().toLong(); + } + auto firstIR = InteractionRecord::long2IR(std::max(int64_t(0), firstLong - timeOffset - 100)); + auto lastIR = InteractionRecord::long2IR(std::max(int64_t(0), lastLong - timeOffset + 100)); mDigitizer->addNoise(firstIR, lastIR); // digitize From b16654d88ba8e925d485c90984913d573284ce9d Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sun, 20 Sep 2026 08:40:00 +0200 Subject: [PATCH 4/6] Let the digit readers accept a timeframe without collisions This fixes a crash in the ITS, MFT, MCH and MID digit readers when the digit tree of a timeframe has no entry. - A timeframe holds no collision at all whenever the interaction rate is low enough, and the digitiser then writes a valid tree with zero entries. - The ITS/MFT reader guarded this with an assert, which is compiled out of every production build because ENABLE_CASSERT defaults to OFF, and then dereferenced branch addresses that GetEntry had not filled. - The MCH and MID readers threw on the failed TTreeReader::Next(). - All four now send empty output and end the stream. - The two asserts in the ITS/MFT connectTree become real errors for the same reason. https://its.cern.ch/jira/browse/O2-7132 Co-Authored-By: Claude Opus 5 --- .../common/workflow/src/DigitReaderSpec.cxx | 41 +++++++++++++++++-- Detectors/MUON/MCH/IO/src/DigitReaderSpec.cxx | 15 +++++++ .../MUON/MID/Workflow/src/DigitReaderSpec.cxx | 15 +++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/Detectors/ITSMFT/common/workflow/src/DigitReaderSpec.cxx b/Detectors/ITSMFT/common/workflow/src/DigitReaderSpec.cxx index b6c3ab5386179..313eefd6ef4d2 100644 --- a/Detectors/ITSMFT/common/workflow/src/DigitReaderSpec.cxx +++ b/Detectors/ITSMFT/common/workflow/src/DigitReaderSpec.cxx @@ -26,6 +26,7 @@ #include "ITSMFTReconstruction/ChipMappingMFT.h" #include "SimulationDataFormat/MCCompLabel.h" #include "SimulationDataFormat/ConstMCTruthContainer.h" +#include "SimulationDataFormat/MCTruthContainer.h" #include "DataFormatsITSMFT/PhysTrigger.h" #include "CommonUtils/NameConf.h" #include "CommonDataFormat/IRFrame.h" @@ -101,7 +102,37 @@ void DigitReader::run(ProcessingContext& pc) auto ent = mTree->GetReadEntry(); if (!mUseIRFrames) { ent++; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and the + // digit tree then has no entry to read. Send empty output rather than dereferencing the + // branch addresses, which GetEntry has not filled. (This used to be an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF.) + LOG(info) << mDetName << "DigitReader has no entry to read, sending empty output"; + static const std::vector noROFRecords; + static const std::vector noDigits; + static const std::vector noMC2ROF; + for (uint32_t iLayer = 0; iLayer < mLayers; ++iLayer) { + pc.outputs().snapshot(Output{Origin, "DIGITSROF", iLayer}, noROFRecords); + pc.outputs().snapshot(Output{Origin, "DIGITS", iLayer}, noDigits); + if (mUseMC) { + auto& sharedlabels = pc.outputs().make>(Output{Origin, "DIGITSMCTR", iLayer}); + o2::dataformats::MCTruthContainer noLabels; + noLabels.flatten_to(sharedlabels); + pc.outputs().snapshot(Output{Origin, "DIGITSMC2ROF", iLayer}, noMC2ROF); + } + } + if (mUseCalib) { + static const std::vector noCalib; + pc.outputs().snapshot(Output{Origin, "GBTCALIB", 0}, noCalib); + } + if (mTriggerOut) { + static const std::vector noTrigger; + pc.outputs().snapshot(Output{Origin, "PHYSTRIG", 0}, noTrigger); + } + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); for (uint32_t iLayer = 0; iLayer < mLayers; ++iLayer) { LOG(info) << mDetName << "DigitReader" << ((mDoStaggering) ? std::format(": {}", iLayer) : "") << " pushes " << mDigROFRec[iLayer]->size() << " ROFRecords, " << mDigits[iLayer]->size() << " digits at entry " << ent; @@ -215,9 +246,13 @@ void DigitReader::connectTree(const std::string& filename) { mTree.reset(nullptr); // in case it was already loaded mFile.reset(TFile::Open(filename.c_str())); - assert(mFile && !mFile->IsZombie()); + if (!mFile || mFile->IsZombie()) { + throw std::runtime_error(std::format("Cannot open {}", filename)); + } mTree.reset((TTree*)mFile->Get(mDigTreeName.c_str())); - assert(mTree); + if (!mTree) { + throw std::runtime_error(std::format("Tree {} not found in {}", mDigTreeName, filename)); + } for (uint32_t iLayer = 0; iLayer < mLayers; ++iLayer) { setBranchAddress(mDigitROFBranchName, mDigROFRec[iLayer], iLayer); setBranchAddress(mDigitBranchName, mDigits[iLayer], iLayer); diff --git a/Detectors/MUON/MCH/IO/src/DigitReaderSpec.cxx b/Detectors/MUON/MCH/IO/src/DigitReaderSpec.cxx index 78a0022e07166..af13460a42dd0 100644 --- a/Detectors/MUON/MCH/IO/src/DigitReaderSpec.cxx +++ b/Detectors/MUON/MCH/IO/src/DigitReaderSpec.cxx @@ -37,6 +37,7 @@ #include "DataFormatsMCH/ROFRecord.h" #include "Framework/ConfigParamRegistry.h" #include "Framework/ControlService.h" +#include "Framework/Logger.h" #include "Framework/DataSpecUtils.h" #include "Framework/Task.h" #include "Framework/WorkflowSpec.h" @@ -109,6 +110,20 @@ class DigitsReaderDeviceDPL void sendNextTF(ProcessingContext& pc) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and the + // digit tree then has no entry. Send empty containers and finish, rather than throwing. + if (mTreeReader.GetEntries() == 0) { + LOG(info) << "digit tree has no entry, sending empty output"; + pc.outputs().snapshot(OutputRef{"rofs"}, std::vector{}); + pc.outputs().snapshot(OutputRef{"digits"}, std::vector{}); + if (mUseMC) { + pc.outputs().snapshot(OutputRef{"labels"}, dataformats::MCTruthContainer{}); + } + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } + // load the next TF and check its validity (missing branch, ...) if (!mTreeReader.Next()) { throw std::invalid_argument(mTreeReader.fgEntryStatusText[mTreeReader.GetEntryStatus()]); diff --git a/Detectors/MUON/MID/Workflow/src/DigitReaderSpec.cxx b/Detectors/MUON/MID/Workflow/src/DigitReaderSpec.cxx index f65415b8d701a..0479452bcd6f5 100644 --- a/Detectors/MUON/MID/Workflow/src/DigitReaderSpec.cxx +++ b/Detectors/MUON/MID/Workflow/src/DigitReaderSpec.cxx @@ -28,6 +28,7 @@ #include "Framework/ConfigParamRegistry.h" #include "Framework/ControlService.h" +#include "Framework/Logger.h" #include "Framework/DataSpecUtils.h" #include "Framework/Task.h" #include "Framework/WorkflowSpec.h" @@ -103,6 +104,20 @@ class DigitsReaderDeviceDPL void sendNextTF(ProcessingContext& pc) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and the + // digit tree then has no entry. Send empty containers and finish, rather than throwing. + if (mTreeReader.GetEntries() == 0) { + LOG(info) << "digit tree has no entry, sending empty output"; + pc.outputs().snapshot(OutputRef{"rofs"}, std::vector{}); + pc.outputs().snapshot(OutputRef{"digits"}, std::vector{}); + if (mUseMC) { + pc.outputs().snapshot(OutputRef{"labels"}, dataformats::MCTruthContainer{}); + } + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } + // load the next TF and check its validity (missing branch, ...) if (!mTreeReader.Next()) { throw std::invalid_argument(mTreeReader.fgEntryStatusText[mTreeReader.GetEntryStatus()]); From 3d409170450481288680b6fb951ebf3e4243e788 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sun, 20 Sep 2026 09:41:41 +0200 Subject: [PATCH 5/6] Check for the end of the tree where the reader specs asserted it This replaces a disabled assert with a real check in 36 ROOT-tree reader specs, so that a timeframe whose tree has no entry ends the stream instead of reading past the end. - Every one of them carried the same two lines: assert(ent < mTree->GetEntries()) with the comment "this should not happen", followed by mTree->GetEntry(ent). - ENABLE_CASSERT defaults to OFF, so the assert is compiled out of every production build and the reader then publishes branch addresses that GetEntry never filled. - A timeframe holds no collision whenever the interaction rate is low enough, which is when the trees come out empty. - The readers now end the stream, which the consumers downstream already handle. - Detectors/Upgrades/ALICE3/IOTOF is left alone: it has no ControlService. https://its.cern.ch/jira/browse/O2-7132 Co-Authored-By: Claude Opus 5 --- Detectors/CPV/workflow/src/ClusterReaderSpec.cxx | 11 ++++++++++- Detectors/CPV/workflow/src/DigitReaderSpec.cxx | 11 ++++++++++- Detectors/CTP/workflowIO/src/DigitReaderSpec.cxx | 11 ++++++++++- .../FIT/FDD/workflow/src/DigitReaderSpec.cxx | 11 ++++++++++- .../FIT/FDD/workflow/src/RecPointReaderSpec.cxx | 11 ++++++++++- .../FIT/FT0/workflow/src/DigitReaderSpec.cxx | 11 ++++++++++- .../FIT/FT0/workflow/src/RecPointReaderSpec.cxx | 11 ++++++++++- .../FIT/FV0/workflow/src/DigitReaderSpec.cxx | 11 ++++++++++- .../FIT/FV0/workflow/src/RecPointReaderSpec.cxx | 11 ++++++++++- Detectors/Filtering/src/FilteredTFReaderSpec.cxx | 11 ++++++++++- .../readers/src/GlobalFwdTrackReaderSpec.cxx | 11 ++++++++++- .../readers/src/IRFrameReaderSpec.cxx | 11 ++++++++++- .../readers/src/MatchedMCHMIDReaderSpec.cxx | 11 ++++++++++- .../readers/src/MatchedMFTMCHReaderSpec.cxx | 11 ++++++++++- .../readers/src/PrimaryVertexReaderSpec.cxx | 11 ++++++++++- .../readers/src/SecondaryVertexReaderSpec.cxx | 11 ++++++++++- .../readers/src/StrangenessTrackingReaderSpec.cxx | 11 ++++++++++- .../readers/src/TrackCosmicsReaderSpec.cxx | 11 ++++++++++- .../readers/src/TrackTPCITSReaderSpec.cxx | 11 ++++++++++- .../HMPID/workflow/src/ClustersReaderSpec.cxx | 11 ++++++++++- Detectors/HMPID/workflow/src/DigitsReaderSpec.cxx | 11 ++++++++++- .../ITSMFT/ITS/workflow/src/TrackReaderSpec.cxx | 11 ++++++++++- .../ITSMFT/ITS/workflow/src/VertexReaderSpec.cxx | 11 ++++++++++- .../ITSMFT/MFT/workflow/src/TrackReaderSpec.cxx | 11 ++++++++++- .../common/workflow/src/ClusterReaderSpec.cxx | 11 ++++++++++- .../common/workflow/src/DigitReaderSpec.cxx | 15 +++++---------- Detectors/PHOS/workflow/src/CellReaderSpec.cxx | 11 ++++++++++- Detectors/PHOS/workflow/src/DigitReaderSpec.cxx | 11 ++++++++++- .../TOF/workflowIO/src/CalibClusReaderSpec.cxx | 11 ++++++++++- .../TOF/workflowIO/src/ClusterReaderSpec.cxx | 11 ++++++++++- .../TPC/workflow/readers/src/TrackReaderSpec.cxx | 11 ++++++++++- .../TRD/workflow/io/src/TRDTrackReaderSpec.cxx | 11 ++++++++++- .../ALICE3/TRK/workflow/src/DigitReaderSpec.cxx | 11 ++++++++++- .../ITS3/workflow/src/DigitReaderSpec.cxx | 11 ++++++++++- Detectors/ZDC/workflow/src/DigitReaderSpec.cxx | 11 ++++++++++- Detectors/ZDC/workflow/src/RecEventReaderSpec.cxx | 11 ++++++++++- Detectors/ZDC/workflow/src/RecoReaderSpec.cxx | 11 ++++++++++- 37 files changed, 365 insertions(+), 46 deletions(-) diff --git a/Detectors/CPV/workflow/src/ClusterReaderSpec.cxx b/Detectors/CPV/workflow/src/ClusterReaderSpec.cxx index f9d0817325c36..dbcbd8a73510e 100644 --- a/Detectors/CPV/workflow/src/ClusterReaderSpec.cxx +++ b/Detectors/CPV/workflow/src/ClusterReaderSpec.cxx @@ -41,7 +41,16 @@ void ClusterReader::init(InitContext& ic) void ClusterReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mClusters.size() << " Clusters in " << mTRs.size() << " TriggerRecords at entry " << ent; pc.outputs().snapshot(Output{mOrigin, "CLUSTERS", 0}, mClusters); diff --git a/Detectors/CPV/workflow/src/DigitReaderSpec.cxx b/Detectors/CPV/workflow/src/DigitReaderSpec.cxx index 20fe497eb5d0c..29e88a8371319 100644 --- a/Detectors/CPV/workflow/src/DigitReaderSpec.cxx +++ b/Detectors/CPV/workflow/src/DigitReaderSpec.cxx @@ -41,7 +41,16 @@ void DigitReader::init(InitContext& ic) void DigitReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mDigits.size() << " Digits in " << mTRs.size() << " TriggerRecords at entry " << ent; pc.outputs().snapshot(Output{mOrigin, "DIGITS", 0}, mDigits); diff --git a/Detectors/CTP/workflowIO/src/DigitReaderSpec.cxx b/Detectors/CTP/workflowIO/src/DigitReaderSpec.cxx index 81e6f53f42dcc..2e49eb08ed7cf 100644 --- a/Detectors/CTP/workflowIO/src/DigitReaderSpec.cxx +++ b/Detectors/CTP/workflowIO/src/DigitReaderSpec.cxx @@ -86,7 +86,16 @@ void DigitReader::run(ProcessingContext& pc) auto ent = mTree->GetReadEntry(); if (!mUseIRFrames) { ent++; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "DigitReader pushes " << mDigits.size() << " digits at entry " << ent; pc.outputs().snapshot(Output{"CTP", "DIGITS", 0}, mDigits); diff --git a/Detectors/FIT/FDD/workflow/src/DigitReaderSpec.cxx b/Detectors/FIT/FDD/workflow/src/DigitReaderSpec.cxx index 628a2160c6d0c..08da00dd83051 100644 --- a/Detectors/FIT/FDD/workflow/src/DigitReaderSpec.cxx +++ b/Detectors/FIT/FDD/workflow/src/DigitReaderSpec.cxx @@ -77,7 +77,16 @@ void DigitReader::run(ProcessingContext& pc) } } auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "FDD DigitReader pushes " << digitsBC->size() << " digits"; diff --git a/Detectors/FIT/FDD/workflow/src/RecPointReaderSpec.cxx b/Detectors/FIT/FDD/workflow/src/RecPointReaderSpec.cxx index 3c4812c75b251..a8e78acd487d7 100644 --- a/Detectors/FIT/FDD/workflow/src/RecPointReaderSpec.cxx +++ b/Detectors/FIT/FDD/workflow/src/RecPointReaderSpec.cxx @@ -45,7 +45,16 @@ void RecPointReader::init(InitContext& ic) void RecPointReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "FDD RecPointReader pushes " << mRecPoints->size() << " recpoints with " << mChannelData->size() << " channels at entry " << ent; diff --git a/Detectors/FIT/FT0/workflow/src/DigitReaderSpec.cxx b/Detectors/FIT/FT0/workflow/src/DigitReaderSpec.cxx index 09586d778ac15..3f9e5c75b1aae 100644 --- a/Detectors/FIT/FT0/workflow/src/DigitReaderSpec.cxx +++ b/Detectors/FIT/FT0/workflow/src/DigitReaderSpec.cxx @@ -61,7 +61,16 @@ void DigitReader::run(ProcessingContext& pc) mTree->SetBranchAddress("FT0DIGITSMCTR", &plabels); } auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(debug) << "FT0DigitReader pushed " << channels.size() << " channels in " << digits.size() << " digits"; pc.outputs().snapshot(Output{"FT0", "DIGITSBC", 0}, digits); diff --git a/Detectors/FIT/FT0/workflow/src/RecPointReaderSpec.cxx b/Detectors/FIT/FT0/workflow/src/RecPointReaderSpec.cxx index ba5ae4aa1356c..f5404c23dfbd5 100644 --- a/Detectors/FIT/FT0/workflow/src/RecPointReaderSpec.cxx +++ b/Detectors/FIT/FT0/workflow/src/RecPointReaderSpec.cxx @@ -45,7 +45,16 @@ void RecPointReader::init(InitContext& ic) void RecPointReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(debug) << "FT0 RecPointReader pushes " << mRecPoints->size() << " recpoints with " << mChannelData->size() << " channels at entry " << ent; diff --git a/Detectors/FIT/FV0/workflow/src/DigitReaderSpec.cxx b/Detectors/FIT/FV0/workflow/src/DigitReaderSpec.cxx index a49bda2cec18b..491f98771f51b 100644 --- a/Detectors/FIT/FV0/workflow/src/DigitReaderSpec.cxx +++ b/Detectors/FIT/FV0/workflow/src/DigitReaderSpec.cxx @@ -62,7 +62,16 @@ void DigitReader::run(ProcessingContext& pc) mTree->SetBranchAddress("FV0DigitLabels", &plabels); } auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(debug) << "FV0DigitReader pushed " << channels.size() << " channels in " << digits.size() << " digits"; pc.outputs().snapshot(Output{"FV0", "DIGITSBC", 0}, digits); diff --git a/Detectors/FIT/FV0/workflow/src/RecPointReaderSpec.cxx b/Detectors/FIT/FV0/workflow/src/RecPointReaderSpec.cxx index 5997cac500ee6..ecf4796353c1c 100644 --- a/Detectors/FIT/FV0/workflow/src/RecPointReaderSpec.cxx +++ b/Detectors/FIT/FV0/workflow/src/RecPointReaderSpec.cxx @@ -45,7 +45,16 @@ void RecPointReader::init(InitContext& ic) void RecPointReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(debug) << "FV0 RecPointReader pushes " << mRecPoints->size() << " recpoints with " << mChannelData->size() << " channels at entry " << ent; diff --git a/Detectors/Filtering/src/FilteredTFReaderSpec.cxx b/Detectors/Filtering/src/FilteredTFReaderSpec.cxx index 22fe1370040db..0e2920532add8 100644 --- a/Detectors/Filtering/src/FilteredTFReaderSpec.cxx +++ b/Detectors/Filtering/src/FilteredTFReaderSpec.cxx @@ -40,7 +40,16 @@ void FilteredTFReader::run(ProcessingContext& pc) // FIXME: fill all output headers by TF specific info (extend findMessageHeaderStack) auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing filtered TF: " << mFiltTF.header.asString(); diff --git a/Detectors/GlobalTrackingWorkflow/readers/src/GlobalFwdTrackReaderSpec.cxx b/Detectors/GlobalTrackingWorkflow/readers/src/GlobalFwdTrackReaderSpec.cxx index 11fa58333f89b..465552259561d 100644 --- a/Detectors/GlobalTrackingWorkflow/readers/src/GlobalFwdTrackReaderSpec.cxx +++ b/Detectors/GlobalTrackingWorkflow/readers/src/GlobalFwdTrackReaderSpec.cxx @@ -61,7 +61,16 @@ void GlobalFwdTrackReader::init(InitContext& ic) void GlobalFwdTrackReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mTracks.size() << " Global Forward tracks at entry " << ent; diff --git a/Detectors/GlobalTrackingWorkflow/readers/src/IRFrameReaderSpec.cxx b/Detectors/GlobalTrackingWorkflow/readers/src/IRFrameReaderSpec.cxx index c1810a1deb743..e63c3ca327164 100644 --- a/Detectors/GlobalTrackingWorkflow/readers/src/IRFrameReaderSpec.cxx +++ b/Detectors/GlobalTrackingWorkflow/readers/src/IRFrameReaderSpec.cxx @@ -60,7 +60,16 @@ void IRFrameReaderSpec::init(InitContext& ic) void IRFrameReaderSpec::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(debug) << "Pushing " << mIRF.size() << " IR-frames in at entry " << ent; pc.outputs().snapshot(Output{mDataOrigin, "IRFRAMES", mSubSpec}, mIRF); diff --git a/Detectors/GlobalTrackingWorkflow/readers/src/MatchedMCHMIDReaderSpec.cxx b/Detectors/GlobalTrackingWorkflow/readers/src/MatchedMCHMIDReaderSpec.cxx index dc8cf71575787..a8e48f156181d 100644 --- a/Detectors/GlobalTrackingWorkflow/readers/src/MatchedMCHMIDReaderSpec.cxx +++ b/Detectors/GlobalTrackingWorkflow/readers/src/MatchedMCHMIDReaderSpec.cxx @@ -61,7 +61,16 @@ void MatchMCHMIDReader::init(InitContext& ic) void MatchMCHMIDReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mTracks.size() << " MCHMID matches at entry " << ent; diff --git a/Detectors/GlobalTrackingWorkflow/readers/src/MatchedMFTMCHReaderSpec.cxx b/Detectors/GlobalTrackingWorkflow/readers/src/MatchedMFTMCHReaderSpec.cxx index 5f02beebd1746..1e3c1015427f9 100644 --- a/Detectors/GlobalTrackingWorkflow/readers/src/MatchedMFTMCHReaderSpec.cxx +++ b/Detectors/GlobalTrackingWorkflow/readers/src/MatchedMFTMCHReaderSpec.cxx @@ -61,7 +61,16 @@ void MatchMFTMCHReader::init(InitContext& ic) void MatchMFTMCHReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mTracks.size() << " MFTMCH matches at entry " << ent; diff --git a/Detectors/GlobalTrackingWorkflow/readers/src/PrimaryVertexReaderSpec.cxx b/Detectors/GlobalTrackingWorkflow/readers/src/PrimaryVertexReaderSpec.cxx index 6e1aba8b2e1f3..0182c01f8d3f6 100644 --- a/Detectors/GlobalTrackingWorkflow/readers/src/PrimaryVertexReaderSpec.cxx +++ b/Detectors/GlobalTrackingWorkflow/readers/src/PrimaryVertexReaderSpec.cxx @@ -80,7 +80,16 @@ void PrimaryVertexReader::init(InitContext& ic) void PrimaryVertexReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mVerticesPtr->size() << " vertices at entry " << ent; diff --git a/Detectors/GlobalTrackingWorkflow/readers/src/SecondaryVertexReaderSpec.cxx b/Detectors/GlobalTrackingWorkflow/readers/src/SecondaryVertexReaderSpec.cxx index 9f252616c9d55..30476f20b4493 100644 --- a/Detectors/GlobalTrackingWorkflow/readers/src/SecondaryVertexReaderSpec.cxx +++ b/Detectors/GlobalTrackingWorkflow/readers/src/SecondaryVertexReaderSpec.cxx @@ -89,7 +89,16 @@ void SecondaryVertexReader::init(InitContext& ic) void SecondaryVertexReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOGP(info, "Pushing {} V0s ({} indices), {} cascades ({} indices) and {} 3-body ({} indices ) at entry {}", mV0s.size(), mV0sIdx.size(), mCascs.size(), mCascsIdx.size(), m3Bodys.size(), m3BodysIdx.size(), ent); diff --git a/Detectors/GlobalTrackingWorkflow/readers/src/StrangenessTrackingReaderSpec.cxx b/Detectors/GlobalTrackingWorkflow/readers/src/StrangenessTrackingReaderSpec.cxx index 8c7f87a720925..0ea73c1dc0dba 100644 --- a/Detectors/GlobalTrackingWorkflow/readers/src/StrangenessTrackingReaderSpec.cxx +++ b/Detectors/GlobalTrackingWorkflow/readers/src/StrangenessTrackingReaderSpec.cxx @@ -73,7 +73,16 @@ void StrangenessTrackingReader::init(InitContext& ic) void StrangenessTrackingReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mStrangeTrack.size() << " strange tracks at entry " << ent; pc.outputs().snapshot(Output{"GLO", "STRANGETRACKS", 0}, mStrangeTrack); diff --git a/Detectors/GlobalTrackingWorkflow/readers/src/TrackCosmicsReaderSpec.cxx b/Detectors/GlobalTrackingWorkflow/readers/src/TrackCosmicsReaderSpec.cxx index 7e3cdffd84a6d..e11cfa719f733 100644 --- a/Detectors/GlobalTrackingWorkflow/readers/src/TrackCosmicsReaderSpec.cxx +++ b/Detectors/GlobalTrackingWorkflow/readers/src/TrackCosmicsReaderSpec.cxx @@ -37,7 +37,16 @@ void TrackCosmicsReader::init(InitContext& ic) void TrackCosmicsReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mTracks.size() << " Cosmic Tracks at entry " << ent; diff --git a/Detectors/GlobalTrackingWorkflow/readers/src/TrackTPCITSReaderSpec.cxx b/Detectors/GlobalTrackingWorkflow/readers/src/TrackTPCITSReaderSpec.cxx index c7fd0d543ecf6..8d1c3dbc1e039 100644 --- a/Detectors/GlobalTrackingWorkflow/readers/src/TrackTPCITSReaderSpec.cxx +++ b/Detectors/GlobalTrackingWorkflow/readers/src/TrackTPCITSReaderSpec.cxx @@ -64,7 +64,16 @@ void TrackTPCITSReader::init(InitContext& ic) void TrackTPCITSReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mTracks.size() << " TPC-ITS matches at entry " << ent; diff --git a/Detectors/HMPID/workflow/src/ClustersReaderSpec.cxx b/Detectors/HMPID/workflow/src/ClustersReaderSpec.cxx index 9ac5074acb505..5fef823ae1138 100644 --- a/Detectors/HMPID/workflow/src/ClustersReaderSpec.cxx +++ b/Detectors/HMPID/workflow/src/ClustersReaderSpec.cxx @@ -68,7 +68,16 @@ void ClusterReaderTask::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); pc.outputs().snapshot(Output{"HMP", "CLUSTERS", 0}, mClustersFromFile); diff --git a/Detectors/HMPID/workflow/src/DigitsReaderSpec.cxx b/Detectors/HMPID/workflow/src/DigitsReaderSpec.cxx index 88f6df2bce2e7..df7910580558a 100644 --- a/Detectors/HMPID/workflow/src/DigitsReaderSpec.cxx +++ b/Detectors/HMPID/workflow/src/DigitsReaderSpec.cxx @@ -112,7 +112,16 @@ void DigitReader::run(ProcessingContext& pc) // mTree->Print("toponly"); auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); pc.outputs().snapshot(Output{"HMP", "DIGITS", 0}, mDigitsFromFile); diff --git a/Detectors/ITSMFT/ITS/workflow/src/TrackReaderSpec.cxx b/Detectors/ITSMFT/ITS/workflow/src/TrackReaderSpec.cxx index 2f081a11c28b9..1f7677d66c784 100644 --- a/Detectors/ITSMFT/ITS/workflow/src/TrackReaderSpec.cxx +++ b/Detectors/ITSMFT/ITS/workflow/src/TrackReaderSpec.cxx @@ -34,7 +34,16 @@ void TrackReader::init(InitContext& ic) void TrackReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mTracks.size() << " track at entry " << ent; pc.outputs().snapshot(Output{mOrigin, "ITSTrackROF", 0}, mROFRec); diff --git a/Detectors/ITSMFT/ITS/workflow/src/VertexReaderSpec.cxx b/Detectors/ITSMFT/ITS/workflow/src/VertexReaderSpec.cxx index e92f08af23c0d..d70f2e64e6970 100644 --- a/Detectors/ITSMFT/ITS/workflow/src/VertexReaderSpec.cxx +++ b/Detectors/ITSMFT/ITS/workflow/src/VertexReaderSpec.cxx @@ -37,7 +37,16 @@ void VertexReader::init(InitContext& ic) void VertexReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mVerticesPtr->size() << " vertices in " << mVerticesROFRecPtr->size() << " ROFs at entry " << ent; diff --git a/Detectors/ITSMFT/MFT/workflow/src/TrackReaderSpec.cxx b/Detectors/ITSMFT/MFT/workflow/src/TrackReaderSpec.cxx index 1a2ae573af536..3d0068febc6c2 100644 --- a/Detectors/ITSMFT/MFT/workflow/src/TrackReaderSpec.cxx +++ b/Detectors/ITSMFT/MFT/workflow/src/TrackReaderSpec.cxx @@ -42,7 +42,16 @@ void TrackReader::init(InitContext& ic) void TrackReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mTracks.size() << " track in " << mROFRec.size() << " ROFs at entry " << ent; pc.outputs().snapshot(Output{mOrigin, "MFTTrackROF", 0}, mROFRec); diff --git a/Detectors/ITSMFT/common/workflow/src/ClusterReaderSpec.cxx b/Detectors/ITSMFT/common/workflow/src/ClusterReaderSpec.cxx index 6174938171336..efe9376fb7d0a 100644 --- a/Detectors/ITSMFT/common/workflow/src/ClusterReaderSpec.cxx +++ b/Detectors/ITSMFT/common/workflow/src/ClusterReaderSpec.cxx @@ -57,7 +57,16 @@ template void ClusterReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); for (uint32_t iLayer = 0; iLayer < mLayers; ++iLayer) { diff --git a/Detectors/ITSMFT/common/workflow/src/DigitReaderSpec.cxx b/Detectors/ITSMFT/common/workflow/src/DigitReaderSpec.cxx index 313eefd6ef4d2..e9db31ec3222e 100644 --- a/Detectors/ITSMFT/common/workflow/src/DigitReaderSpec.cxx +++ b/Detectors/ITSMFT/common/workflow/src/DigitReaderSpec.cxx @@ -108,26 +108,21 @@ void DigitReader::run(ProcessingContext& pc) // branch addresses, which GetEntry has not filled. (This used to be an assert, which is // compiled out of every production build since ENABLE_CASSERT defaults to OFF.) LOG(info) << mDetName << "DigitReader has no entry to read, sending empty output"; - static const std::vector noROFRecords; - static const std::vector noDigits; - static const std::vector noMC2ROF; for (uint32_t iLayer = 0; iLayer < mLayers; ++iLayer) { - pc.outputs().snapshot(Output{Origin, "DIGITSROF", iLayer}, noROFRecords); - pc.outputs().snapshot(Output{Origin, "DIGITS", iLayer}, noDigits); + pc.outputs().snapshot(Output{Origin, "DIGITSROF", iLayer}, std::vector{}); + pc.outputs().snapshot(Output{Origin, "DIGITS", iLayer}, std::vector{}); if (mUseMC) { auto& sharedlabels = pc.outputs().make>(Output{Origin, "DIGITSMCTR", iLayer}); o2::dataformats::MCTruthContainer noLabels; noLabels.flatten_to(sharedlabels); - pc.outputs().snapshot(Output{Origin, "DIGITSMC2ROF", iLayer}, noMC2ROF); + pc.outputs().snapshot(Output{Origin, "DIGITSMC2ROF", iLayer}, std::vector{}); } } if (mUseCalib) { - static const std::vector noCalib; - pc.outputs().snapshot(Output{Origin, "GBTCALIB", 0}, noCalib); + pc.outputs().snapshot(Output{Origin, "GBTCALIB", 0}, std::vector{}); } if (mTriggerOut) { - static const std::vector noTrigger; - pc.outputs().snapshot(Output{Origin, "PHYSTRIG", 0}, noTrigger); + pc.outputs().snapshot(Output{Origin, "PHYSTRIG", 0}, std::vector{}); } pc.services().get().endOfStream(); pc.services().get().readyToQuit(QuitRequest::Me); diff --git a/Detectors/PHOS/workflow/src/CellReaderSpec.cxx b/Detectors/PHOS/workflow/src/CellReaderSpec.cxx index c7d93fc20301f..aa7f5282679bd 100644 --- a/Detectors/PHOS/workflow/src/CellReaderSpec.cxx +++ b/Detectors/PHOS/workflow/src/CellReaderSpec.cxx @@ -41,7 +41,16 @@ void CellReader::init(InitContext& ic) void CellReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mCells.size() << " Cells in " << mTRs.size() << " TriggerRecords at entry " << ent; pc.outputs().snapshot(Output{mOrigin, "CELLS", 0}, mCells); diff --git a/Detectors/PHOS/workflow/src/DigitReaderSpec.cxx b/Detectors/PHOS/workflow/src/DigitReaderSpec.cxx index 70f5077b2f0c9..3df5b9a4d02e9 100644 --- a/Detectors/PHOS/workflow/src/DigitReaderSpec.cxx +++ b/Detectors/PHOS/workflow/src/DigitReaderSpec.cxx @@ -41,7 +41,16 @@ void DigitReader::init(InitContext& ic) void DigitReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mDigits.size() << " Digits in " << mTRs.size() << " TriggerRecords at entry " << ent; pc.outputs().snapshot(Output{mOrigin, "DIGITS", 0}, mDigits); diff --git a/Detectors/TOF/workflowIO/src/CalibClusReaderSpec.cxx b/Detectors/TOF/workflowIO/src/CalibClusReaderSpec.cxx index 116f93a06c208..983dd59b33699 100644 --- a/Detectors/TOF/workflowIO/src/CalibClusReaderSpec.cxx +++ b/Detectors/TOF/workflowIO/src/CalibClusReaderSpec.cxx @@ -36,7 +36,16 @@ void CalibClusReader::init(InitContext& ic) void CalibClusReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(debug) << "Pushing " << mPclusInfos->size() << " TOF clusters calib info at entry " << ent; pc.outputs().snapshot(Output{o2::header::gDataOriginTOF, "INFOCALCLUS", 0}, mClusInfos); diff --git a/Detectors/TOF/workflowIO/src/ClusterReaderSpec.cxx b/Detectors/TOF/workflowIO/src/ClusterReaderSpec.cxx index e2979a8fc0dbf..35ae8b4fa2851 100644 --- a/Detectors/TOF/workflowIO/src/ClusterReaderSpec.cxx +++ b/Detectors/TOF/workflowIO/src/ClusterReaderSpec.cxx @@ -40,7 +40,16 @@ void ClusterReader::init(InitContext& ic) void ClusterReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(debug) << "Pushing " << mClustersPtr->size() << " TOF clusters at entry " << ent; diff --git a/Detectors/TPC/workflow/readers/src/TrackReaderSpec.cxx b/Detectors/TPC/workflow/readers/src/TrackReaderSpec.cxx index d73da0cb0d33c..adc4ac4698634 100644 --- a/Detectors/TPC/workflow/readers/src/TrackReaderSpec.cxx +++ b/Detectors/TPC/workflow/readers/src/TrackReaderSpec.cxx @@ -42,7 +42,16 @@ void TrackReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; accumulate(ent, 1); // to really accumulate all, use accumulate(ent,mTree->GetEntries()); - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); using TrackTunePar = o2::globaltracking::TrackTuneParams; const auto& trackTune = TrackTunePar::Instance(); diff --git a/Detectors/TRD/workflow/io/src/TRDTrackReaderSpec.cxx b/Detectors/TRD/workflow/io/src/TRDTrackReaderSpec.cxx index cd9702a3d2385..75dd29b5d4645 100644 --- a/Detectors/TRD/workflow/io/src/TRDTrackReaderSpec.cxx +++ b/Detectors/TRD/workflow/io/src/TRDTrackReaderSpec.cxx @@ -38,7 +38,16 @@ void TRDTrackReader::init(InitContext& ic) void TRDTrackReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "Pushing " << mTracks.size() << " tracks and " << mTrigRec.size() << " trigger records at entry " << ent; if (mUseMC) { diff --git a/Detectors/Upgrades/ALICE3/TRK/workflow/src/DigitReaderSpec.cxx b/Detectors/Upgrades/ALICE3/TRK/workflow/src/DigitReaderSpec.cxx index ec2b6d4d66192..8a1f461ca8f26 100644 --- a/Detectors/Upgrades/ALICE3/TRK/workflow/src/DigitReaderSpec.cxx +++ b/Detectors/Upgrades/ALICE3/TRK/workflow/src/DigitReaderSpec.cxx @@ -60,7 +60,16 @@ void DigitReader::init(InitContext& ic) void DigitReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); for (int iLayer = 0; iLayer < mLayers; ++iLayer) { diff --git a/Detectors/Upgrades/ITS3/workflow/src/DigitReaderSpec.cxx b/Detectors/Upgrades/ITS3/workflow/src/DigitReaderSpec.cxx index 141457c319b9b..75038b2bb6440 100644 --- a/Detectors/Upgrades/ITS3/workflow/src/DigitReaderSpec.cxx +++ b/Detectors/Upgrades/ITS3/workflow/src/DigitReaderSpec.cxx @@ -47,7 +47,16 @@ void ITS3DigitReader::init(InitContext& ic) void ITS3DigitReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); for (uint32_t iLayer = 0; iLayer < (mDoStaggering ? NLayers : 1); ++iLayer) { diff --git a/Detectors/ZDC/workflow/src/DigitReaderSpec.cxx b/Detectors/ZDC/workflow/src/DigitReaderSpec.cxx index e952111e0c6c3..adc115030b2e9 100644 --- a/Detectors/ZDC/workflow/src/DigitReaderSpec.cxx +++ b/Detectors/ZDC/workflow/src/DigitReaderSpec.cxx @@ -66,7 +66,16 @@ void DigitReader::run(ProcessingContext& pc) } auto ent = mTree->GetReadEntry() < 0 ? mTree->GetReadEntry() + mFirstEntry + 1 : mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "ZDCDigitReader pushed " << zdcOrbitData.size() << " orbits with " << zdcBCData.size() << " bcs and " << zdcChData.size() << " digits"; pc.outputs().snapshot(Output{"ZDC", "DIGITSPD", 0}, zdcOrbitData); diff --git a/Detectors/ZDC/workflow/src/RecEventReaderSpec.cxx b/Detectors/ZDC/workflow/src/RecEventReaderSpec.cxx index 18c620e427569..c068209037893 100644 --- a/Detectors/ZDC/workflow/src/RecEventReaderSpec.cxx +++ b/Detectors/ZDC/workflow/src/RecEventReaderSpec.cxx @@ -45,7 +45,16 @@ void RecEventReader::init(InitContext& ic) void RecEventReader::run(ProcessingContext& pc) { auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "ZDC RecEventReader pushes " << mBCRecData->size() << " events with " << mBCRecData->size() << " energy, " << mZDCTDCData->size() << " TDC and " << mZDCInfo->size() << " info records at entry " << ent; diff --git a/Detectors/ZDC/workflow/src/RecoReaderSpec.cxx b/Detectors/ZDC/workflow/src/RecoReaderSpec.cxx index 33b2b59d8247b..672191e2a5e19 100644 --- a/Detectors/ZDC/workflow/src/RecoReaderSpec.cxx +++ b/Detectors/ZDC/workflow/src/RecoReaderSpec.cxx @@ -64,7 +64,16 @@ void RecoReader::run(ProcessingContext& pc) mTree->SetBranchAddress("ZDCWaveform", &WaveformDataPtr); auto ent = mTree->GetReadEntry() + 1; - assert(ent < mTree->GetEntries()); // this should not happen + if (ent >= mTree->GetEntries()) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // the tree then has no entry to read. End the stream instead of reading past the end and + // publishing branch addresses that GetEntry has not filled. This was an assert, which is + // compiled out of every production build since ENABLE_CASSERT defaults to OFF. + LOG(info) << "no entry to read, ending the stream"; + pc.services().get().endOfStream(); + pc.services().get().readyToQuit(QuitRequest::Me); + return; + } mTree->GetEntry(ent); LOG(info) << "ZDCRecoReader pushed " << RecBC.size() << " b.c. " << Energy.size() << " Energies " << TDCData.size() << " TDCs " << Info.size() << " Infos " << WaveformData.size() << " Waveform chunks"; pc.outputs().snapshot(Output{"ZDC", "BCREC", 0}, RecBC); From 6d91a5410d143c71afd49459887b9327e583afcf Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Sun, 20 Sep 2026 10:10:10 +0200 Subject: [PATCH 6/6] Write one empty TPC digit entry when a timeframe has no collision This fixes the TPC digit writer producing a file without a tree when a timeframe holds no collision. - The custom close callback only called TFile::Close inside "if (entries > 0)", and never called TFile::Write, so with nothing to write the tree never reached the file. - The result was a 942 byte file with no o2sim tree, and every reader of it failed on a missing branch rather than on an empty tree. - Each branch is now filled once with the empty default object it is bound to, so the file is an ordinary timeframe that happens to contain no digit and the readers downstream stay on their normal path. - The tree is written explicitly, the way RootTreeWriter's own close does. https://its.cern.ch/jira/browse/O2-7132 Co-Authored-By: Claude Opus 5 --- .../src/TPCDigitRootWriterSpec.cxx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Detectors/TPC/simworkflow/src/TPCDigitRootWriterSpec.cxx b/Detectors/TPC/simworkflow/src/TPCDigitRootWriterSpec.cxx index a907a73281884..75b420314142f 100644 --- a/Detectors/TPC/simworkflow/src/TPCDigitRootWriterSpec.cxx +++ b/Detectors/TPC/simworkflow/src/TPCDigitRootWriterSpec.cxx @@ -69,12 +69,24 @@ DataProcessorSpec getTPCDigitRootWriterSpec(std::vector const& laneConfigur LOG(warning) << "INCONSISTENT NUMBER OF ENTRIES IN BRANCH " << br->GetName() << ": " << entries << " vs " << brentries; } } - if (entries > 0) { - LOG(info) << "Setting entries to " << entries; - outputtree->SetEntries(entries); - // outputtree->Write("", TObject::kOverwrite); - outputfile->Close(); + if (entries <= 0) { + // A timeframe holds no collision at all whenever the interaction rate is low enough, and + // then no branch is filled. Write one empty entry in every branch instead of nothing, so + // that the file is an ordinary timeframe that happens to contain no digit and every reader + // downstream stays on its normal path. Each branch is bound to a default constructed object + // of its own type by RootTreeWriter, so Fill() writes exactly that. + LOG(info) << "No branch was filled, writing one empty entry per branch"; + for (TObject* entry : *brlist) { + static_cast(entry)->Fill(); + } + entries = 1; } + LOG(info) << "Setting entries to " << entries; + outputtree->SetEntries(entries); + // write the tree explicitly, the way RootTreeWriter's own close does. Closing the file alone + // leaves an empty tree without a key, so the file comes out with no tree in it at all. + outputfile->Write(); + outputfile->Close(); }; // branch definitions for RootTreeWriter spec