From feb5129fe18260b59b35b35ba4ef00e84af098a6 Mon Sep 17 00:00:00 2001 From: Sandro Wenzel Date: Wed, 10 Jun 2026 16:54:56 +0200 Subject: [PATCH] AOD: don't emit MC collisions borrowed from the previous timeframe With --orbitsEarly (the timeframe history effect in O2DPG), the per-TF collision context is prefixed with the tail collisions of the previous timeframe. The AOD producer emitted BC and McCollision rows (plus McParticles) for these borrowed collisions as well, so merging per-TF AODs produced duplicate, non-monotonic BC entries and double-counted MC events. We now apply an ownership cut at mStartIR (orbitFirstSampled): collisions with globalBC below the TF start are skipped in collectBCs, the McCollision table and the McParticles table. McCollision indices are compacted accordingly, and reconstructed-track MC labels pointing into a dropped collision are invalidated (-1). The cut is applied uniformly to every TF, including the first one of a job, so that AODs of neighbouring jobs can also be merged without duplication. Merged AO2Ds now have a sorted, duplicate-free BC table and exactly one McCollision row per generated event, making the post-hoc AODBcRewriter (in O2DPG) repair unnecessary for new productions. --- Detectors/AOD/src/AODProducerWorkflowSpec.cxx | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/Detectors/AOD/src/AODProducerWorkflowSpec.cxx b/Detectors/AOD/src/AODProducerWorkflowSpec.cxx index 8365628f1644b..ca7f18c141610 100644 --- a/Detectors/AOD/src/AODProducerWorkflowSpec.cxx +++ b/Detectors/AOD/src/AODProducerWorkflowSpec.cxx @@ -236,6 +236,12 @@ void AODProducerWorkflowDPL::collectBCs(const o2::globaltracking::RecoContainer& // collecting non-empty BCs and enumerating them for (auto& rec : mcRecords) { uint64_t globalBC = rec.toLong(); + if (globalBC < mStartIR.toLong()) { + // MC collision borrowed from the previous timeframe (due to collision-context + // overlap); it is owned and stored by that timeframe's AOD, skip it here to + // avoid duplicate BC/MCCollision rows after merging + continue; + } bcsMap[globalBC] = 1; } @@ -1198,6 +1204,16 @@ void AODProducerWorkflowDPL::fillMCParticlesTable(o2::steer::MCKinematicsReader& int event = colInfo.eventID; int source = colInfo.sourceID; int mcColId = colInfo.colIndex; + if (mcColId < 0) { + // belongs to an MC collision dropped by the producer (borrowed + // from the previous timeframe, see collectBCs); none of its tracks end up in the + // McParticles table, so invalidate any entries the marking phase above may have + // added for it (e.g. via dangling reconstructed-track MC labels) + for (auto& entry : mToStore[source][event]) { + entry.second = -1; + } + continue; + } std::vector const& mcParticles = mcReader.getTracks(source, event); LOG(debug) << "Event=" << event << " source=" << source << " collision=" << mcColId; auto& preselect = mToStore[source][event]; @@ -2220,6 +2236,22 @@ void AODProducerWorkflowDPL::run(ProcessingContext& pc) const auto& mcRecords = mcReader->getDigitizationContext()->getEventRecords(); const auto& mcParts = mcReader->getDigitizationContext()->getEventParts(); + // Collisions whose nominal BC predates the start of this timeframe were + // already produced (and stored) by the previous timeframe's AOD due to + // the collision-context overlap introduced by --orbitsEarly; drop them + // here to avoid duplicate BC/MCCollision rows after merging timeframes. + // Build a remap from the original (per-context) MC collision index to a + // compact index covering only the collisions owned by this timeframe. + std::vector mcCollOldToNew(nMCCollisions, -1); + { + int newIdx = 0; + for (int iCol = 0; iCol < nMCCollisions; iCol++) { + if (mcRecords[iCol].toLong() >= mStartIR.toLong()) { + mcCollOldToNew[iCol] = newIdx++; + } + } + } + // if signal filtering enabled, let's check if there are more than one source; otherwise fatalise if (mUseSigFiltMC) { std::vector sourceIDs{}; @@ -2252,6 +2284,19 @@ void AODProducerWorkflowDPL::run(ProcessingContext& pc) for (int iCol = 0; iCol < nMCCollisions; iCol++) { const auto time = mcRecords[iCol].getTimeOffsetWrtBC(); auto globalBC = mcRecords[iCol].toLong(); + auto& colParts = mcParts[iCol]; + auto nParts = colParts.size(); + if (mcCollOldToNew[iCol] < 0) { + // collision borrowed from the previous timeframe (see collectBCs and the + // mcCollOldToNew remap above): no BC/MCCollision row is stored for it, but + // its pairs still need to be registered with colIndex = -1 + // so that fillMCParticlesTable can size mToStore correctly and invalidate + // any track labels dangling into these MC events + for (auto colPart : colParts) { + mcColToEvSrc.emplace_back(MCColInfo{-1, colPart.sourceID, colPart.entryID, globalBC}); + } + continue; + } auto item = bcsMap.find(globalBC); int bcID = -1; if (item != bcsMap.end()) { @@ -2261,8 +2306,6 @@ void AODProducerWorkflowDPL::run(ProcessingContext& pc) << "for MC collision; BC = " << globalBC << ", mc collision = " << iCol; } - auto& colParts = mcParts[iCol]; - auto nParts = colParts.size(); for (auto colPart : colParts) { auto eventID = colPart.entryID; auto sourceID = colPart.sourceID; @@ -2277,13 +2320,13 @@ void AODProducerWorkflowDPL::run(ProcessingContext& pc) hepmcPdfInfosCursor.cursor, hepmcHeavyIonsCursor.cursor, header, - iCol, + mcCollOldToNew[iCol], bcID, time, 0, sourceID); } - mcColToEvSrc.emplace_back(MCColInfo{iCol, sourceID, eventID, globalBC}); // point background and injected signal events to one collision + mcColToEvSrc.emplace_back(MCColInfo{mcCollOldToNew[iCol], sourceID, eventID, globalBC}); // point background and injected signal events to one collision } } }