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/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 b6c3ab5386179..e9db31ec3222e 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,32 @@ 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"; + for (uint32_t iLayer = 0; iLayer < mLayers; ++iLayer) { + 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}, std::vector{}); + } + } + if (mUseCalib) { + pc.outputs().snapshot(Output{Origin, "GBTCALIB", 0}, std::vector{}); + } + if (mTriggerOut) { + pc.outputs().snapshot(Output{Origin, "PHYSTRIG", 0}, std::vector{}); + } + 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 +241,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()]); 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/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 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); 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; 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 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: {