Skip to content

Commit 19866a3

Browse files
sawenzelclaude
andcommitted
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<N>/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 <noreply@anthropic.com>
1 parent 77c2fc3 commit 19866a3

5 files changed

Lines changed: 228 additions & 18 deletions

File tree

DataFormats/simulation/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ o2_target_root_dictionary(
5555
# * src/SimulationDataLinkDef.h
5656
# * and not src/SimulationDataFormatLinkDef.h
5757

58+
o2_add_test(DigitizationContext
59+
SOURCES test/testDigitizationContext.cxx
60+
COMPONENT_NAME SimulationDataFormat
61+
PUBLIC_LINK_LIBRARIES O2::SimulationDataFormat)
62+
5863
o2_add_test(InteractionSampler
5964
SOURCES test/testInteractionSampler.cxx
6065
COMPONENT_NAME SimulationDataFormat

DataFormats/simulation/include/SimulationDataFormat/DigitizationContext.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ class DigitizationContext
135135
void applyMaxCollisionFilter(std::vector<std::tuple<int, int, int>>& timeframeindices, long startOrbit, long orbitsPerTF, int maxColl, double orbitsEarly = 0.);
136136

137137
/// get timeframe structure --> index markers where timeframe starts/ends/is_influenced_by
138-
std::vector<std::tuple<int, int, int>> calcTimeframeIndices(long startOrbit, long orbitsPerTF, double orbitsEarly = 0.) const;
138+
/// One entry is produced per timeframe, including timeframes which contain no collision at all.
139+
/// nTimeframes is the number of timeframes the caller asked for; when given, the result has exactly
140+
/// that many entries, so that a timeframe without collisions keeps its own slot instead of shifting
141+
/// all later timeframes down by one.
142+
std::vector<std::tuple<int, int, int>> calcTimeframeIndices(long startOrbit, long orbitsPerTF, double orbitsEarly = 0., long nTimeframes = -1) const;
139143

140144
// Sample and fix interaction vertices (according to some distribution). Makes sure that same event ids
141145
// have to have same vertex, as well as event ids associated to same collision.

DataFormats/simulation/src/DigitizationContext.cxx

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -389,20 +389,33 @@ void DigitizationContext::fillQED(std::string_view QEDprefix, std::vector<o2::In
389389
namespace
390390
{
391391
// a common helper for timeframe structure
392-
std::vector<std::pair<int, int>> getTimeFrameBoundaries(std::vector<o2::InteractionTimeRecord> const& irecords, long startOrbit, long orbitsPerTF)
392+
// One entry is produced per timeframe. A timeframe without collisions gets an empty range
393+
// (first > second) rather than being left out, so that entry i always describes the timeframe
394+
// covering orbits [startOrbit + i * orbitsPerTF, startOrbit + (i+1) * orbitsPerTF).
395+
// nTimeframes, when positive, is the number of timeframes the caller asked for; the result is
396+
// padded with empty timeframes (or truncated) to exactly that length.
397+
std::vector<std::pair<int, int>> getTimeFrameBoundaries(std::vector<o2::InteractionTimeRecord> const& irecords, long startOrbit, long orbitsPerTF, long nTimeframes = -1)
393398
{
394399
std::vector<std::pair<int, int>> result;
395400

401+
auto pad_and_return = [&result, nTimeframes](int index) {
402+
if (nTimeframes > 0) {
403+
while ((long)result.size() < nTimeframes) {
404+
result.emplace_back(std::pair<int, int>(index, index - 1)); // an empty timeframe
405+
}
406+
result.resize(nTimeframes);
407+
}
408+
return result;
409+
};
410+
396411
// the goal is to determine timeframe boundaries inside the interaction record vectors
397-
// determine if we can do anything
398412
if (irecords.size() == 0) {
399-
// nothing to do
400-
return result;
413+
return pad_and_return(0);
401414
}
402415

403416
if (irecords.back().orbit < startOrbit) {
404417
LOG(error) << "start orbit larger than last collision entry";
405-
return result;
418+
return pad_and_return((int)irecords.size());
406419
}
407420

408421
// skip to the first index falling within our constrained
@@ -413,10 +426,13 @@ std::vector<std::pair<int, int>> getTimeFrameBoundaries(std::vector<o2::Interact
413426

414427
// now we can start (2 pointer approach)
415428
auto right = left;
416-
int timeframe_count = 1;
429+
long timeframe_count = 1;
417430
while (right < irecords.size()) {
418-
if (irecords[right].orbit >= startOrbit + timeframe_count * orbitsPerTF) {
419-
// we finished one timeframe
431+
// a collision may lie several timeframes ahead of the previous one; close every timeframe it
432+
// skips over, as an empty one, so that the collision ends up in the timeframe it belongs to.
433+
// (A plain "if" here closed only one timeframe per collision, which both dropped the empty
434+
// timeframes and mis-assigned the collisions after them.)
435+
while (irecords[right].orbit >= startOrbit + timeframe_count * orbitsPerTF) {
420436
result.emplace_back(std::pair<int, int>(left, right - 1));
421437
timeframe_count++;
422438
left = right;
@@ -425,17 +441,18 @@ std::vector<std::pair<int, int>> getTimeFrameBoundaries(std::vector<o2::Interact
425441
}
426442
// finished last timeframe
427443
result.emplace_back(std::pair<int, int>(left, right - 1));
428-
return result;
444+
return pad_and_return((int)irecords.size());
429445
}
430446

431447
// a common helper for timeframe structure - includes indices for orbits-early (orbits from last timeframe still affecting current one)
432448
std::vector<std::tuple<int, int, int>> getTimeFrameBoundaries(std::vector<o2::InteractionTimeRecord> const& irecords,
433449
long startOrbit,
434450
long orbitsPerTF,
435-
float orbitsEarly)
451+
float orbitsEarly,
452+
long nTimeframes = -1)
436453
{
437454
// we could actually use the other method first ... then do another pass to fix the early-index ... or impact index
438-
auto true_indices = getTimeFrameBoundaries(irecords, startOrbit, orbitsPerTF);
455+
auto true_indices = getTimeFrameBoundaries(irecords, startOrbit, orbitsPerTF, nTimeframes);
439456

440457
std::vector<std::tuple<int, int, int>> indices_with_early{};
441458
for (int ti = 0; ti < true_indices.size(); ++ti) {
@@ -447,7 +464,7 @@ std::vector<std::tuple<int, int, int>> getTimeFrameBoundaries(std::vector<o2::In
447464

448465
// from the second timeframe on we can determine the index in the previous timeframe
449466
// which matches our criterion
450-
if (orbitsEarly > 0. && ti > 0) {
467+
if (orbitsEarly > 0. && ti > 0 && tf_range.first <= tf_range.second) {
451468
auto& prev_tf_range = true_indices[ti - 1];
452469
// in this range search the smallest index which precedes
453470
// timeframe ti by not more than "orbitsEarly" orbits
@@ -518,7 +535,8 @@ void DigitizationContext::applyMaxCollisionFilter(std::vector<std::tuple<int, in
518535

519536
LOG(info) << "timeframe indices " << previndex << " : " << firstindex << " : " << lastindex;
520537

521-
int collCount = 0; // counting collisions within timeframe
538+
int collCount = 0; // counting collisions within timeframe
539+
const size_t nrecords_before = newrecords.size(); // to detect a timeframe that stays empty
522540
// copy to new structure
523541
for (int index = previndex >= 0 ? previndex : firstindex; index <= lastindex; ++index) {
524542
if (collCount >= maxColl) {
@@ -571,6 +589,14 @@ void DigitizationContext::applyMaxCollisionFilter(std::vector<std::tuple<int, in
571589
} // ends one timeframe
572590

573591
// correct the timeframe indices
592+
if (newrecords.size() == nrecords_before) {
593+
// this timeframe received no collision at all; give it an empty range at the current
594+
// position so that it keeps its slot and the timeframes after it are not shifted
595+
std::get<0>(tf_indices) = (int)newrecords.size();
596+
std::get<1>(tf_indices) = (int)newrecords.size() - 1;
597+
std::get<2>(tf_indices) = -1;
598+
continue;
599+
}
574600
if (indices_old_to_new.find(firstindex) != indices_old_to_new.end()) {
575601
std::get<0>(tf_indices) = indices_old_to_new[firstindex]; // start
576602
}
@@ -588,9 +614,9 @@ void DigitizationContext::applyMaxCollisionFilter(std::vector<std::tuple<int, in
588614
mEventParts = newparts;
589615
}
590616

591-
std::vector<std::tuple<int, int, int>> DigitizationContext::calcTimeframeIndices(long startOrbit, long orbitsPerTF, double orbitsEarly) const
617+
std::vector<std::tuple<int, int, int>> DigitizationContext::calcTimeframeIndices(long startOrbit, long orbitsPerTF, double orbitsEarly, long nTimeframes) const
592618
{
593-
auto timeframeindices = getTimeFrameBoundaries(mEventRecords, startOrbit, orbitsPerTF, orbitsEarly);
619+
auto timeframeindices = getTimeFrameBoundaries(mEventRecords, startOrbit, orbitsPerTF, orbitsEarly, nTimeframes);
594620
return timeframeindices;
595621
}
596622

@@ -710,6 +736,11 @@ DigitizationContext DigitizationContext::extractSingleTimeframe(int timeframeid,
710736
if (earlyindex >= 0) {
711737
startindex = earlyindex;
712738
}
739+
if (endindex < startindex) {
740+
// a timeframe without any collision: return a valid but empty context rather than
741+
// copying a negative range
742+
endindex = startindex;
743+
}
713744
std::copy(mEventRecords.begin() + startindex, mEventRecords.begin() + endindex, std::back_inserter(r.mEventRecords));
714745
std::copy(mEventParts.begin() + startindex, mEventParts.begin() + endindex, std::back_inserter(r.mEventParts));
715746
if (mInteractionVertices.size() >= endindex) {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2019-2020 CERN and copyright holders of ALICE O2.
2+
// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3+
// All rights not expressly granted are reserved.
4+
//
5+
// This software is distributed under the terms of the GNU General Public
6+
// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7+
//
8+
// In applying this license CERN does not waive the privileges and immunities
9+
// granted to it by virtue of its status as an Intergovernmental Organization
10+
// or submit itself to any jurisdiction.
11+
12+
#define BOOST_TEST_MODULE Test DigitizationContext class
13+
#define BOOST_TEST_MAIN
14+
#define BOOST_TEST_DYN_LINK
15+
16+
#include <boost/test/unit_test.hpp>
17+
#include "SimulationDataFormat/DigitizationContext.h"
18+
#include <vector>
19+
20+
namespace o2
21+
{
22+
23+
// build a context whose collisions sit at the given orbits (one collision each, source 0)
24+
steer::DigitizationContext makeContext(std::vector<long> const& orbits)
25+
{
26+
steer::DigitizationContext ctx;
27+
auto& records = ctx.getEventRecords();
28+
auto& parts = ctx.getEventParts();
29+
int entry = 0;
30+
for (auto o : orbits) {
31+
records.emplace_back(o2::InteractionTimeRecord(o2::InteractionRecord(0, o), 0.));
32+
parts.push_back({steer::EventPart(0, entry++)});
33+
}
34+
ctx.setNCollisions(records.size());
35+
ctx.setMaxNumberParts(1);
36+
return ctx;
37+
}
38+
39+
// The timeframe index structure must have one entry per timeframe asked for, and entry i must
40+
// describe exactly the collisions falling into orbits [start + i*orbitsPerTF, start + (i+1)*orbitsPerTF).
41+
BOOST_AUTO_TEST_CASE(TimeframeIndicesAreSlotAligned)
42+
{
43+
long const orbitsPerTF = 6;
44+
long const start = 0;
45+
long const nTF = 5; // orbits 0..29
46+
47+
// timeframe 1 (orbits 6..11) and timeframe 4 (orbits 24..29) hold no collision
48+
std::vector<long> orbits{0, 3, 5, 12, 14, 17, 18, 21};
49+
auto ctx = makeContext(orbits);
50+
51+
auto indices = ctx.calcTimeframeIndices(start, orbitsPerTF, 0., nTF);
52+
BOOST_CHECK_EQUAL(indices.size(), (size_t)nTF);
53+
54+
for (int tf = 0; tf < nTF; ++tf) {
55+
auto first = std::get<0>(indices[tf]);
56+
auto last = std::get<1>(indices[tf]);
57+
long const lo = start + tf * orbitsPerTF;
58+
long const hi = lo + orbitsPerTF;
59+
// count what should be in this timeframe
60+
int expected = 0;
61+
for (auto o : orbits) {
62+
if (o >= lo && o < hi) {
63+
expected++;
64+
}
65+
}
66+
BOOST_CHECK_EQUAL(last - first + 1, expected);
67+
for (int i = first; i <= last; ++i) {
68+
BOOST_CHECK(orbits[i] >= lo);
69+
BOOST_CHECK(orbits[i] < hi);
70+
}
71+
}
72+
}
73+
74+
// A timeframe without collisions must survive extraction as a valid, empty context
75+
BOOST_AUTO_TEST_CASE(EmptyTimeframeExtracts)
76+
{
77+
long const orbitsPerTF = 6;
78+
long const nTF = 3;
79+
auto ctx = makeContext({0, 2, 13}); // timeframe 1 (orbits 6..11) is empty
80+
auto indices = ctx.calcTimeframeIndices(0, orbitsPerTF, 0., nTF);
81+
BOOST_CHECK_EQUAL(indices.size(), (size_t)nTF);
82+
83+
auto tf0 = ctx.extractSingleTimeframe(0, indices, {});
84+
auto tf1 = ctx.extractSingleTimeframe(1, indices, {});
85+
auto tf2 = ctx.extractSingleTimeframe(2, indices, {});
86+
BOOST_CHECK_EQUAL(tf0.getEventRecords().size(), (size_t)2);
87+
BOOST_CHECK_EQUAL(tf1.getEventRecords().size(), (size_t)0);
88+
BOOST_CHECK_EQUAL(tf2.getEventRecords().size(), (size_t)1);
89+
BOOST_CHECK_EQUAL(tf2.getEventRecords()[0].orbit, 13);
90+
}
91+
92+
// The trailing timeframes of the requested range must be present even when the last collision
93+
// falls well before the end of the range
94+
BOOST_AUTO_TEST_CASE(TrailingTimeframesArePresent)
95+
{
96+
long const orbitsPerTF = 6;
97+
long const nTF = 9; // this is what an 8-timeframe anchored MC job with orbitsEarly asks for
98+
auto ctx = makeContext({1, 2, 7});
99+
auto indices = ctx.calcTimeframeIndices(0, orbitsPerTF, 0., nTF);
100+
BOOST_CHECK_EQUAL(indices.size(), (size_t)nTF);
101+
for (int tf = 2; tf < nTF; ++tf) {
102+
BOOST_CHECK(std::get<0>(indices[tf]) > std::get<1>(indices[tf])); // empty, but present
103+
}
104+
}
105+
106+
// applyMaxCollisionFilter must not shift timeframes when one of them is empty
107+
BOOST_AUTO_TEST_CASE(MaxCollisionFilterKeepsSlots)
108+
{
109+
long const orbitsPerTF = 6;
110+
long const nTF = 4;
111+
// tf0: orbits 0,1,2 tf1: empty tf2: orbits 12,13 tf3: orbit 19
112+
auto ctx = makeContext({0, 1, 2, 12, 13, 19});
113+
auto indices = ctx.calcTimeframeIndices(0, orbitsPerTF, 0., nTF);
114+
ctx.applyMaxCollisionFilter(indices, 0, orbitsPerTF, 2, 0.); // keep at most 2 per timeframe
115+
116+
BOOST_CHECK_EQUAL(indices.size(), (size_t)nTF);
117+
BOOST_CHECK_EQUAL(std::get<1>(indices[0]) - std::get<0>(indices[0]) + 1, 2); // capped
118+
BOOST_CHECK(std::get<0>(indices[1]) > std::get<1>(indices[1])); // still empty
119+
BOOST_CHECK_EQUAL(std::get<1>(indices[2]) - std::get<0>(indices[2]) + 1, 2);
120+
BOOST_CHECK_EQUAL(std::get<1>(indices[3]) - std::get<0>(indices[3]) + 1, 1);
121+
122+
auto tf2 = ctx.extractSingleTimeframe(2, indices, {});
123+
BOOST_CHECK_EQUAL(tf2.getEventRecords().size(), (size_t)2);
124+
BOOST_CHECK_EQUAL(tf2.getEventRecords()[0].orbit, 12);
125+
}
126+
127+
} // namespace o2

Steer/src/CollisionContextTool.cxx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "SimulationDataFormat/DigitizationContext.h"
2121
#include "SimConfig/InteractionDiamondParam.h"
2222
#include "DataFormatsFT0/EventsPerBc.h"
23+
#include "CommonConstants/LHCConstants.h"
2324
#include <cmath>
2425
#include <TRandom.h>
2526
#include <numeric>
@@ -238,7 +239,7 @@ bool parseOptions(int argc, char* argv[], Options& optvalues)
238239
"timeframeID", bpo::value<int>(&optvalues.tfid)->default_value(0), "Timeframe id of the first timeframe int this context. Allows to generate contexts for different start orbits")(
239240
"first-orbit", bpo::value<double>(&optvalues.firstFractionalOrbit)->default_value(0), "First (fractional) orbit in the run (HBFUtils.firstOrbit + BC from decimal)")(
240241
"maxCollsPerTF", bpo::value<int>(&optvalues.maxCollsPerTF)->default_value(-1), "Maximal number of MC collisions to put into one timeframe. By default no constraint.")(
241-
"noEmptyTF", bpo::bool_switch(&optvalues.noEmptyTF), "Enforce to have at least one collision")(
242+
"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)")(
242243
"configKeyValues", bpo::value<std::string>(&optvalues.configKeyValues)->default_value(""), "Semicolon separated key=value strings (e.g.: 'TPC.gasDensity=1;...')")(
243244
"with-vertices", bpo::value<std::string>(&optvalues.vertexModeString)->default_value("kNoVertex"), "Assign vertices to collisions. Argument is the vertex mode. Defaults to no vertexing applied")(
244245
"timestamp", bpo::value<long>(&optvalues.timestamp)->default_value(-1L), "Timestamp for CCDB queries / anchoring")(
@@ -660,7 +661,10 @@ int main(int argc, char* argv[])
660661
}
661662
LOG(info) << "-------- DENSE CONTEXT ------->>";
662663

663-
auto timeframeindices = digicontext.calcTimeframeIndices(orbitstart, options.orbitsPerTF, options.orbitsEarly);
664+
// the number of timeframes we were asked for; passing it makes sure that a timeframe without
665+
// collisions keeps its own slot instead of shifting every later timeframe down by one
666+
long const num_timeframes_asked = usetimeframelength ? (orbits_total / options.orbitsPerTF) : -1;
667+
auto timeframeindices = digicontext.calcTimeframeIndices(orbitstart, options.orbitsPerTF, options.orbitsEarly, num_timeframes_asked);
664668
LOG(info) << "Fixed " << timeframeindices.size() << " timeframes ";
665669
for (auto p : timeframeindices) {
666670
LOG(info) << std::get<0>(p) << " " << std::get<1>(p) << " " << std::get<2>(p);
@@ -684,6 +688,45 @@ int main(int argc, char* argv[])
684688

685689
auto numTimeFrames = timeframeindices.size(); // digicontext.finalizeTimeframeStructure(orbitstart, options.orbitsPerTF, options.orbitsEarly);
686690

691+
// report - and, if asked, refuse - timeframes without a single collision. A timeframe with no
692+
// collision cannot be simulated, and the rest of the MC workflow expects one collision context
693+
// file per timeframe, so this has to be visible here and not five hours later in the simulation.
694+
{
695+
std::vector<int> empty_timeframes;
696+
auto const first_real_tf = options.orbitsEarly > 0. ? 1 : 0;
697+
for (int tf_id = first_real_tf; tf_id < (int)numTimeFrames; ++tf_id) {
698+
if (std::get<0>(timeframeindices[tf_id]) > std::get<1>(timeframeindices[tf_id])) {
699+
empty_timeframes.push_back(tf_id - first_real_tf + 1);
700+
}
701+
}
702+
if (!empty_timeframes.empty()) {
703+
std::stringstream tflist;
704+
for (auto tf : empty_timeframes) {
705+
tflist << " tf" << tf;
706+
}
707+
// the mean number of collisions in one timeframe, from the rate we were given
708+
auto const tf_length_s = options.orbitsPerTF * o2::constants::lhc::LHCOrbitMUS * 1e-6;
709+
double rate = 0.;
710+
for (auto& p : ispecs) {
711+
rate = std::max(rate, (double)p.interactionRate);
712+
}
713+
auto const mu_per_tf = rate * tf_length_s;
714+
LOG(warn) << empty_timeframes.size() << " of " << (numTimeFrames - first_real_tf)
715+
<< " timeframes contain no collision:" << tflist.str();
716+
LOG(warn) << "with interaction rate " << rate << " Hz and " << options.orbitsPerTF
717+
<< " orbits per timeframe there are only " << mu_per_tf
718+
<< " collisions per timeframe on average, so a fraction " << std::exp(-mu_per_tf)
719+
<< " of the timeframes comes out empty";
720+
if (mu_per_tf > 0.) {
721+
LOG(warn) << "use at least " << (int)std::ceil(8. / (rate * o2::constants::lhc::LHCOrbitMUS * 1e-6))
722+
<< " orbits per timeframe to keep that fraction below 1 per mille";
723+
}
724+
if (options.noEmptyTF) {
725+
LOG(fatal) << "--noEmptyTF was requested but timeframes without collisions were produced; refusing to continue";
726+
}
727+
}
728+
}
729+
687730
if (options.vertexMode != o2::conf::VertexMode::kNoVertex) {
688731
switch (options.vertexMode) {
689732
case o2::conf::VertexMode::kCCDB: {

0 commit comments

Comments
 (0)