From 892cd46cec8d35eb4c5db1ee4934895621f6c539 Mon Sep 17 00:00:00 2001 From: Marco Giacalone Date: Tue, 18 Aug 2026 12:25:59 +0200 Subject: [PATCH] Implement pools chaining via TChain + example and test macro --- Generators/CMakeLists.txt | 6 + .../include/Generators/GeneratorFromFile.h | 82 ++- .../Generators/GeneratorFromO2KineParam.h | 18 +- Generators/src/GeneratorFromFile.cxx | 437 ++++++++---- Generators/test/test_EventPoolChaining.cxx | 663 ++++++++++++++++++ run/SimExamples/EventPool_Chaining/README.md | 127 ++++ run/SimExamples/EventPool_Chaining/run.sh | 76 ++ run/SimExamples/EventPool_Chaining/rundpl.sh | 57 ++ run/SimExamples/README.md | 1 + 9 files changed, 1318 insertions(+), 149 deletions(-) create mode 100644 Generators/test/test_EventPoolChaining.cxx create mode 100644 run/SimExamples/EventPool_Chaining/README.md create mode 100755 run/SimExamples/EventPool_Chaining/run.sh create mode 100755 run/SimExamples/EventPool_Chaining/rundpl.sh diff --git a/Generators/CMakeLists.txt b/Generators/CMakeLists.txt index 5624ce7df5f07..f041e75f53a92 100644 --- a/Generators/CMakeLists.txt +++ b/Generators/CMakeLists.txt @@ -139,6 +139,12 @@ if(doBuildSimulation) LABELS generator PUBLIC_LINK_LIBRARIES O2::Generators) + o2_add_test(EventPoolChaining NAME test_Generator_test_EventPoolChaining + SOURCES test/test_EventPoolChaining.cxx + COMPONENT_NAME Generator + LABELS generator + PUBLIC_LINK_LIBRARIES O2::Generators) + # o2_add_test(GeneratorPythia8Param NAME test_Generator_test_GeneratorPythia8Param # SOURCES test/test_GeneratorPythia8Param.cxx # COMPONENT_NAME Generator diff --git a/Generators/include/Generators/GeneratorFromFile.h b/Generators/include/Generators/GeneratorFromFile.h index ad93814467fc3..d2e764c3d001c 100644 --- a/Generators/include/Generators/GeneratorFromFile.h +++ b/Generators/include/Generators/GeneratorFromFile.h @@ -21,6 +21,8 @@ #include #include #include +#include +#include class TBranch; class TFile; @@ -66,13 +68,18 @@ class GeneratorFromFile : public FairGenerator }; /// This class implements a generic FairGenerator which -/// reads the particles from an external O2 sim kinematics file. +/// reads the particles from one or more external O2 sim kinematics files. class GeneratorFromO2Kine : public o2::eventgen::Generator { public: GeneratorFromO2Kine() = default; + /// name may be a single file or a comma-separated list of files to be read one after the other GeneratorFromO2Kine(const char* name); + GeneratorFromO2Kine(std::vector const& filenames); GeneratorFromO2Kine(O2KineGenConfig const& pars); + /// same as above but with an explicit list of files. Used for event pools + GeneratorFromO2Kine(O2KineGenConfig const& pars, std::vector const& filenames); + ~GeneratorFromO2Kine() override; bool Init() override; @@ -91,24 +98,58 @@ class GeneratorFromO2Kine : public o2::eventgen::Generator void updateHeader(o2::dataformats::MCEventHeader* eventHeader) override; const o2::dataformats::MCEventHeader* getOrigMCEventHeader() const { return mOrigMCEventHeader.get(); } + /// number of events available in the file that is currently open + int getEventsAvailable() const { return mEventsAvailable; } + /// number of input files this generator can go through + int getNumberOfFiles() const { return (int)mFileNames.size(); } + /// index (within the file list) of the file currently open, -1 if none + int getCurrentFileIndex() const { return mCurrentFileIndex; } + /// name of the file currently open, empty if none + std::string getCurrentFileName() const; + /// number of opened files so far (including the current one) + int getNumberOfFilesUsed() const { return mFilesUsed; } + /// total number of events delivered so far + int getEventsServed() const { return mEventsServed; } + + /// helper splitting a comma-separated list of file names into its components + static std::vector splitFileNames(std::string const& filenames); + private: - TFile* mEventFile = nullptr; //! the file containing the persistent events - TBranch* mEventBranch = nullptr; //! the branch containing the persistent events - TBranch* mMCHeaderBranch = nullptr; //! branch containing MC event headers - int mEventCounter = 0; - int mEventsAvailable = 0; - bool mSkipNonTrackable = true; //! whether to pass non-trackable (decayed particles) to the MC stack - bool mContinueMode = false; //! whether we want to continue simulation of previously inhibited tracks - bool mRoundRobin = false; //! whether we want to take events from file in a round robin fashion - bool mRandomize = false; //! whether we want to randomize the order of events in the input file - unsigned int mRngSeed = 0; //! randomizer seed, 0 for random value - bool mRandomPhi = false; //! whether we want to randomize the phi angle of the particles - TGrid* mAlienInstance = nullptr; // a cached connection to TGrid (needed for Alien locations) + /// closes the currently opened file currently + void closeCurrentFile(); + /// fixes the order in which the events of the current file are served + void establishEventOrder(); + /// opens the file at the given index of the file list. + /// Returns false in case the file cannot be used + bool openFile(int index); + /// moves on to the next usable file of the list; wraps around in round robin mode. + /// Returns false when no further file is available + bool openNextFile(bool wrapAround); + + std::vector mFileNames; //! the list of input files, read one after the other + int mCurrentFileIndex = -1; //! index of the file currently open + int mFilesUsed = 0; //! how many files have been opened so far + TFile* mCurrentFile = nullptr; //! the file currently open + TBranch* mEventBranch = nullptr; //! the branch containing the persistent events + TBranch* mMCHeaderBranch = nullptr; //! branch containing MC event headers + std::vector mEventOrder; //! order in which the entries of the current file are served + int mEventCounter = 0; //! events already delivered from the current file + int mEventsServed = 0; //! events delivered in total, across all files + int mEventsAvailable = 0; //! events contained in the current file + int mStartEvent = 0; //! event to start from in the very first file + int mLastEntryRead = -1; //! entry of the current event within the current file + bool mSkipNonTrackable = true; //! whether to pass non-trackable (decayed particles) to the MC stack + bool mContinueMode = false; //! whether we want to continue simulation of previously inhibited tracks + bool mRoundRobin = false; //! whether we want to take events from file in a round robin fashion + bool mRandomize = false; //! whether we want to randomize the order of events in the input file + unsigned int mRngSeed = 0; //! randomizer seed, 0 for random value + bool mRandomPhi = false; //! whether we want to randomize the phi angle of the particles + TGrid* mAlienInstance = nullptr; // a cached connection to TGrid (needed for Alien locations) std::unique_ptr mConfig; //! Configuration object std::unique_ptr mOrigMCEventHeader; //! the MC event header of the original file - ClassDefOverride(GeneratorFromO2Kine, 2); + ClassDefOverride(GeneratorFromO2Kine, 3); }; /// Special generator for event pools. @@ -167,15 +208,24 @@ class GeneratorFromEventPool : public o2::eventgen::Generator std::vector const& getFileUniverse() const { return mPoolFilesAvailable; } + /// the file universe, in the order this generator instance will go through it + std::vector const& getChosenFiles() const { return mFilesChosen; } + + /// shuffles the given universe of pool files into the order this instance will use + std::vector selectFiles(std::vector const& universe); + + /// access to the underlying kinematics generator + o2::eventgen::GeneratorFromO2Kine const* getO2KineGenerator() const { return mO2KineGenerator.get(); } + private: EventPoolGenConfig mConfig; //! Configuration object std::unique_ptr mO2KineGenerator = nullptr; //! actual generator doing the work std::vector mPoolFilesAvailable; //! container keeping the collection of files in the event pool - std::string mFileChosen; //! the file chosen for the pool + std::vector mFilesChosen; //! the file(s) chosen from the pool // random number generator to determine a concrete file name std::mt19937 mRandomEngine; //! - ClassDefOverride(GeneratorFromEventPool, 1); + ClassDefOverride(GeneratorFromEventPool, 2); }; } // end namespace eventgen diff --git a/Generators/include/Generators/GeneratorFromO2KineParam.h b/Generators/include/Generators/GeneratorFromO2KineParam.h index e8d886186e2d2..5bc369291c29d 100644 --- a/Generators/include/Generators/GeneratorFromO2KineParam.h +++ b/Generators/include/Generators/GeneratorFromO2KineParam.h @@ -31,22 +31,24 @@ namespace eventgen struct GeneratorFromO2KineParam : public o2::conf::ConfigurableParamHelper { bool skipNonTrackable = true; bool continueMode = false; - bool roundRobin = false; // read events with period boundary conditions - bool randomize = false; // randomize the order of events + bool roundRobin = false; // start over from the first file/event once all events have been used + bool randomize = false; // serve the events of each file in random order (each one exactly once) unsigned int rngseed = 0; // randomizer seed, 0 for random value bool randomphi = false; // randomize phi angle - std::string fileName = ""; // filename to read from - takes precedence over SimConfig if given + std::string fileName = ""; // filename(s) to read from - takes precedence over SimConfig if given; + // a comma-separated list of files is read one file after the other O2ParamDef(GeneratorFromO2KineParam, "GeneratorFromO2Kine"); }; struct O2KineGenConfig { bool skipNonTrackable = true; bool continueMode = false; - bool roundRobin = false; // read events with period boundary conditions - bool randomize = false; // randomize the order of events + bool roundRobin = false; // start over from the first file/event once all events have been used + bool randomize = false; // serve the events of each file in random order (each one exactly once) unsigned int rngseed = 0; // randomizer seed, 0 for random value bool randomphi = false; // randomize phi angle - std::string fileName = ""; // filename to read from - takes precedence over SimConfig if given + std::string fileName = ""; // filename(s) to read from - takes precedence over SimConfig if given; + // a comma-separated list of files is read one file after the other }; struct EventPoolGenConfig { @@ -54,8 +56,8 @@ struct EventPoolGenConfig { // or .. a local file containing a list of files to use // or .. a concrete file path to a kinematics file bool skipNonTrackable = true; // <--- do we need this? - bool roundRobin = false; // read events with period boundary conditions - bool randomize = true; // randomize the order of events + bool roundRobin = false; // start over from the first file/event once all events have been used + bool randomize = true; // serve the events of each file in random order (each one exactly once) unsigned int rngseed = 0; // randomizer seed, 0 for random value bool randomphi = false; // randomize phi angle; rotates tracks in events by some phi-angle }; diff --git a/Generators/src/GeneratorFromFile.cxx b/Generators/src/GeneratorFromFile.cxx index 0876d1810aad2..4500fb74a88bb 100644 --- a/Generators/src/GeneratorFromFile.cxx +++ b/Generators/src/GeneratorFromFile.cxx @@ -13,6 +13,7 @@ #include "Generators/GeneratorFromO2KineParam.h" #include "SimulationDataFormat/MCTrack.h" #include "SimulationDataFormat/MCEventHeader.h" +#include "CommonUtils/StringUtils.h" #include #include #include @@ -21,6 +22,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -170,77 +175,242 @@ Bool_t GeneratorFromFile::ReadEvent(FairPrimaryGenerator* primGen) // based on O2 kinematics -GeneratorFromO2Kine::GeneratorFromO2Kine(const char* name) +namespace { - // this generator should leave all dimensions the same as in the incoming kinematics file - setMomentumUnit(1.); - setEnergyUnit(1.); - setPositionUnit(1.); - setTimeUnit(1.); - - if (strncmp(name, "alien:/", 7) == 0 && !gGrid) { - TGrid::Connect("alien:"); - if (!gGrid) { - LOG(fatal) << "Could not connect to alien, did you check the alien token?"; +// connects to AliEn in case at least one of the given file names lives there +void connectToAlienIfNeeded(std::vector const& filenames) +{ + if (gGrid) { + return; + } + for (auto const& name : filenames) { + if (name.starts_with("alien:/")) { + TGrid::Connect("alien:"); + if (!gGrid) { + LOG(fatal) << "Could not connect to alien, did you check the alien token?"; + } return; } } - mEventFile = TFile::Open(name); - if (mEventFile == nullptr) { - LOG(fatal) << "EventFile " << name << " not found"; - return; +} +} // namespace + +std::vector GeneratorFromO2Kine::splitFileNames(std::string const& filenames) +{ + // splits a comma-separated list of file names, trimming white space and dropping empty tokens + return o2::utils::Str::tokenize(filenames, ','); +} + +std::string GeneratorFromO2Kine::getCurrentFileName() const +{ + if (mCurrentFileIndex < 0 || mCurrentFileIndex >= (int)mFileNames.size()) { + return std::string(); + } + return mFileNames[mCurrentFileIndex]; +} + +void GeneratorFromO2Kine::closeCurrentFile() +{ + // the branches belong to the tree of the file, so they die with it + mEventBranch = nullptr; + mMCHeaderBranch = nullptr; + mEventOrder.clear(); + mEventsAvailable = 0; + mEventCounter = 0; + if (mCurrentFile) { + mCurrentFile->Close(); + delete mCurrentFile; + mCurrentFile = nullptr; + } +} + +void GeneratorFromO2Kine::establishEventOrder() +{ + // The order in which the events of the current file are served is decided here + mEventOrder.resize(mEventsAvailable); + std::iota(mEventOrder.begin(), mEventOrder.end(), 0); + if (mRandomize) { + // Fisher-Yates shuffle based on the ROOT random generator + for (int i = mEventsAvailable - 1; i > 0; --i) { + auto j = (int)gRandom->Integer(i + 1); + std::swap(mEventOrder[i], mEventOrder[j]); + } + } +} + +bool GeneratorFromO2Kine::openFile(int index) +{ + // opens one file of the list, connects the branches to it and fixes the + // order in which its events are going to be served; + // any previously open file is closed first, so that we never keep more than + // one input file open at a time + closeCurrentFile(); + if (index < 0 || index >= (int)mFileNames.size()) { + return false; + } + auto const& name = mFileNames[index]; + + mCurrentFile = TFile::Open(name.c_str()); + if (mCurrentFile == nullptr || mCurrentFile->IsZombie()) { + LOG(error) << "EventFile " << name << " could not be opened"; + closeCurrentFile(); + return false; } // the kinematics will be stored inside a branch MCTrack // different events are stored inside different entries - auto tree = (TTree*)mEventFile->Get("o2sim"); - if (tree) { - mEventBranch = tree->GetBranch("MCTrack"); - if (mEventBranch) { - mEventsAvailable = mEventBranch->GetEntries(); - LOG(info) << "Found " << mEventsAvailable << " events in this file"; + auto tree = (TTree*)mCurrentFile->Get("o2sim"); + if (!tree) { + LOG(error) << "EventFile " << name << " does not contain an 'o2sim' tree"; + closeCurrentFile(); + return false; + } + mEventBranch = tree->GetBranch("MCTrack"); + if (!mEventBranch) { + LOG(error) << "No MCTrack branch found in " << name; + closeCurrentFile(); + return false; + } + mEventsAvailable = mEventBranch->GetEntries(); + if (mEventsAvailable <= 0) { + LOG(warn) << "EventFile " << name << " does not contain any event"; + closeCurrentFile(); + return false; + } + mMCHeaderBranch = tree->GetBranch("MCEventHeader."); + if (!mMCHeaderBranch) { + LOG(warn) << "No MCEventHeader branch found in kinematics input file"; + } + establishEventOrder(); + mCurrentFileIndex = index; + mEventCounter = 0; + mFilesUsed++; + LOG(info) << "Reading events from kinematics file " << name << " (" << mEventsAvailable + << " events, " << (mRandomize ? "randomized" : "sequential") << " order)"; + return true; +} + +bool GeneratorFromO2Kine::openNextFile(bool wrapAround) +{ + // advances to the next usable file of the list; unusable files are skipped. + auto numFiles = (int)mFileNames.size(); + if (numFiles == 0) { + return false; + } + // we try each of the remaining files at most once + for (int trial = 0; trial < numFiles; ++trial) { + auto next = mCurrentFileIndex + 1 + trial; + if (next >= numFiles) { + if (!wrapAround) { + return false; + } + if (trial == 0) { + LOG(info) << "Reached the end of the input file list; reusing events from the beginning"; + } + next = next % numFiles; } - mMCHeaderBranch = tree->GetBranch("MCEventHeader."); - if (mMCHeaderBranch) { - LOG(info) << "Found " << mMCHeaderBranch->GetEntries() << " event-headers"; - } else { - LOG(warn) << "No MCEventHeader branch found in kinematics input file"; + if (next == mCurrentFileIndex && mCurrentFile) { + // this is the only usable file and it is already open - restart from it, + // drawing a fresh event order + establishEventOrder(); + mEventCounter = 0; + return true; + } + if (openFile(next)) { + return true; } + } + LOG(error) << "GeneratorFromO2Kine: no further usable input file"; + return false; +} + +GeneratorFromO2Kine::GeneratorFromO2Kine(std::vector const& filenames) +{ + // this generator should leave all dimensions the same as in the incoming kinematics file + setMomentumUnit(1.); + setEnergyUnit(1.); + setPositionUnit(1.); + setTimeUnit(1.); + + mFileNames = filenames; + if (mFileNames.empty()) { + LOG(error) << "GeneratorFromO2Kine: no input file given"; return; } - LOG(error) << "Problem reading events from file " << name; + LOG(info) << "GeneratorFromO2Kine will read from " << mFileNames.size() + << " file(s), one after the other"; + connectToAlienIfNeeded(mFileNames); +} + +GeneratorFromO2Kine::GeneratorFromO2Kine(const char* name) : GeneratorFromO2Kine(splitFileNames(name ? name : "")) +{ +} + +GeneratorFromO2Kine::GeneratorFromO2Kine(O2KineGenConfig const& pars) : GeneratorFromO2Kine(splitFileNames(pars.fileName)) +{ + mConfig = std::make_unique(pars); } -GeneratorFromO2Kine::GeneratorFromO2Kine(O2KineGenConfig const& pars) : GeneratorFromO2Kine(pars.fileName.c_str()) +GeneratorFromO2Kine::GeneratorFromO2Kine(O2KineGenConfig const& pars, std::vector const& filenames) : GeneratorFromO2Kine(filenames) { mConfig = std::make_unique(pars); } +GeneratorFromO2Kine::~GeneratorFromO2Kine() +{ + closeCurrentFile(); +} + bool GeneratorFromO2Kine::Init() { // read and set params LOG(info) << "Init \'FromO2Kine\' generator"; - mSkipNonTrackable = mConfig->skipNonTrackable; - mContinueMode = mConfig->continueMode; - mRoundRobin = mConfig->roundRobin; - mRandomize = mConfig->randomize; - mRngSeed = mConfig->rngseed; - mRandomPhi = mConfig->randomphi; - if (mRandomize) { + if (mConfig) { + mSkipNonTrackable = mConfig->skipNonTrackable; + mContinueMode = mConfig->continueMode; + mRoundRobin = mConfig->roundRobin; + mRandomize = mConfig->randomize; + mRngSeed = mConfig->rngseed; + mRandomPhi = mConfig->randomphi; + } + if (mRandomize && mRngSeed > 0) { + // with a zero the seed given to the driver (o2-sim / o2-sim-dpl-eventgen --seed) stays in control gRandom->SetSeed(mRngSeed); } + mCurrentFileIndex = -1; + if (!openNextFile(false)) { + LOG(error) << "Problem reading events from the given kinematics input"; + return false; + } + if (mStartEvent > 0) { + if (mStartEvent < mEventsAvailable) { + mEventCounter = mStartEvent; + } else { + LOG(error) << "start event bigger than available events"; + } + } + // Simple estimate of events without checking all the files. + // To be discussed if we want instead to do this, or provide an additional file with the pools + auto requested = getTotalNEvents(); + if (requested > 0 && !mRoundRobin && mEventsAvailable > 0) { + auto estimate = (size_t)mEventsAvailable * mFileNames.size(); + if (estimate < requested) { + LOG(warn) << "This job will request " << requested << " events, but the input (" + << mFileNames.size() << " file(s), " << mEventsAvailable + << " events in the first one) holds only about " << estimate << ". Unless the " + << "remaining files are larger, the job will stop with 'ran out of events' - " + << "provide more files/events or enable roundRobin"; + } + } return true; } void GeneratorFromO2Kine::SetStartEvent(int start) { - if (start < mEventsAvailable) { - mEventCounter = start; - } else { - LOG(error) << "start event bigger than available events\n"; - } + // this refers to the first file and is applied once that file has been opened + mStartEvent = start; } bool GeneratorFromO2Kine::importParticles() @@ -249,10 +419,26 @@ bool GeneratorFromO2Kine::importParticles() // It might need some adjustment to make it work with secondaries or to continue // from a kinematics snapshot - // Randomize the order of events in the input file + // Next file in the list opened when the events of the current one are used up + if (mEventCounter >= mEventsAvailable) { + if (!openNextFile(mRoundRobin)) { + auto requested = getTotalNEvents(); + LOG(fatal) << "GeneratorFromO2Kine: ran out of events after " << mEventsServed + << " event(s) from " << mFilesUsed << " input file(s)" + << (requested > 0 ? " (" + std::to_string(requested) + " were requested)" : "") + << ". Provide more input files/events or allow reusing them via roundRobin"; + return false; + } + } + if (mCurrentFile == nullptr || mEventBranch == nullptr || mEventCounter >= (int)mEventOrder.size()) { + LOG(fatal) << "GeneratorFromO2Kine: no input file available"; + return false; + } + // the entry to be read from the file which is currently open; the order was fixed + // when the file was opened, so every event of it is used exactly once + auto entry = mEventOrder[mEventCounter]; if (mRandomize) { - mEventCounter = gRandom->Integer(mEventsAvailable); - LOG(info) << "GeneratorFromO2Kine - Picking event " << mEventCounter; + LOG(info) << "GeneratorFromO2Kine - Picking event " << entry; } double dPhi = 0.; @@ -262,81 +448,74 @@ bool GeneratorFromO2Kine::importParticles() LOG(info) << "Rotating phi by " << dPhi; } - if (mEventCounter < mEventsAvailable) { - int particlecounter = 0; - - std::vector* tracks = nullptr; - mEventBranch->SetAddress(&tracks); - mEventBranch->GetEntry(mEventCounter); - - if (mMCHeaderBranch) { - o2::dataformats::MCEventHeader* mcheader = nullptr; - mMCHeaderBranch->SetAddress(&mcheader); - mMCHeaderBranch->GetEntry(mEventCounter); - mOrigMCEventHeader.reset(mcheader); - } - - for (auto& t : *tracks) { + int particlecounter = 0; - // in case we do not want to continue, take only primaries - if (!mContinueMode && !t.isPrimary()) { - continue; - } + std::vector* tracks = nullptr; + mEventBranch->SetAddress(&tracks); + mEventBranch->GetEntry(entry); + mLastEntryRead = entry; - auto pdg = t.GetPdgCode(); - auto px = t.Px(); - auto py = t.Py(); - if (mRandomPhi) { - // transformation applied through rotation matrix - auto cos = TMath::Cos(dPhi); - auto sin = TMath::Sin(dPhi); - auto newPx = px * cos - py * sin; - auto newPy = px * sin + py * cos; - px = newPx; - py = newPy; - } - auto pz = t.Pz(); - auto vx = t.Vx(); - auto vy = t.Vy(); - auto vz = t.Vz(); - auto m1 = t.getMotherTrackId(); - auto m2 = t.getSecondMotherTrackId(); - auto d1 = t.getFirstDaughterTrackId(); - auto d2 = t.getLastDaughterTrackId(); - auto e = t.GetEnergy(); - auto vt = t.T() * 1e-9; // MCTrack stores in ns ... generators and engines use seconds - auto weight = t.getWeight(); - auto wanttracking = t.getToBeDone(); - - if (mContinueMode) { // in case we want to continue, do only inhibited tracks - wanttracking &= t.getInhibited(); - } + if (mMCHeaderBranch) { + o2::dataformats::MCEventHeader* mcheader = nullptr; + mMCHeaderBranch->SetAddress(&mcheader); + mMCHeaderBranch->GetEntry(entry); + mOrigMCEventHeader.reset(mcheader); + } - LOG(debug) << "Putting primary " << pdg; + for (auto& t : *tracks) { - mParticles.push_back(TParticle(pdg, t.getStatusCode().fullEncoding, m1, m2, d1, d2, px, py, pz, e, vx, vy, vz, vt)); - mParticles.back().SetUniqueID((unsigned int)t.getProcess()); // we should propagate the process ID - mParticles.back().SetBit(ParticleStatus::kToBeDone, wanttracking); - mParticles.back().SetWeight(weight); + // in case we do not want to continue, take only primaries + if (!mContinueMode && !t.isPrimary()) { + continue; + } - particlecounter++; + auto pdg = t.GetPdgCode(); + auto px = t.Px(); + auto py = t.Py(); + if (mRandomPhi) { + // transformation applied through rotation matrix + auto cos = TMath::Cos(dPhi); + auto sin = TMath::Sin(dPhi); + auto newPx = px * cos - py * sin; + auto newPy = px * sin + py * cos; + px = newPx; + py = newPy; } - mEventCounter++; - if (mRoundRobin) { - LOG(info) << "Resetting event counter to 0; Reusing events from file"; - mEventCounter = mEventCounter % mEventsAvailable; + auto pz = t.Pz(); + auto vx = t.Vx(); + auto vy = t.Vy(); + auto vz = t.Vz(); + auto m1 = t.getMotherTrackId(); + auto m2 = t.getSecondMotherTrackId(); + auto d1 = t.getFirstDaughterTrackId(); + auto d2 = t.getLastDaughterTrackId(); + auto e = t.GetEnergy(); + auto vt = t.T() * 1e-9; // MCTrack stores in ns ... generators and engines use seconds + auto weight = t.getWeight(); + auto wanttracking = t.getToBeDone(); + + if (mContinueMode) { // in case we want to continue, do only inhibited tracks + wanttracking &= t.getInhibited(); } - if (tracks) { - delete tracks; - } + LOG(debug) << "Putting primary " << pdg; - LOG(info) << "Event generator put " << particlecounter << " on stack"; - return true; - } else { - LOG(error) << "GeneratorFromO2Kine: Ran out of events\n"; + mParticles.push_back(TParticle(pdg, t.getStatusCode().fullEncoding, m1, m2, d1, d2, px, py, pz, e, vx, vy, vz, vt)); + mParticles.back().SetUniqueID((unsigned int)t.getProcess()); // we should propagate the process ID + mParticles.back().SetBit(ParticleStatus::kToBeDone, wanttracking); + mParticles.back().SetWeight(weight); + + particlecounter++; } - return false; + mEventCounter++; + mEventsServed++; + + if (tracks) { + delete tracks; + } + + LOG(info) << "Event generator put " << particlecounter << " on stack"; + return true; } void GeneratorFromO2Kine::updateHeader(o2::dataformats::MCEventHeader* eventHeader) @@ -346,14 +525,14 @@ void GeneratorFromO2Kine::updateHeader(o2::dataformats::MCEventHeader* eventHead // we forward the original header information if any if (mOrigMCEventHeader.get()) { eventHeader->copyInfoFrom(*mOrigMCEventHeader.get()); + // we forward also the original basic vertex information contained in FairMCEventHeader + static_cast(*eventHeader) = static_cast(*mOrigMCEventHeader.get()); } - // we forward also the original basic vertex information contained in FairMCEventHeader - static_cast(*eventHeader) = static_cast(*mOrigMCEventHeader.get()); // put additional information about input file and event number of the current event eventHeader->putInfo("forwarding-generator", "generatorFromO2Kine"); - eventHeader->putInfo("forwarding-generator_inputFile", mEventFile->GetName()); - eventHeader->putInfo("forwarding-generator_inputEventNumber", mEventCounter - 1); + eventHeader->putInfo("forwarding-generator_inputFile", getCurrentFileName()); + eventHeader->putInfo("forwarding-generator_inputEventNumber", mLastEntryRead); } namespace @@ -391,12 +570,14 @@ bool GeneratorFromEventPool::Init() setPositionUnit(1.); setEnergyUnit(1.); - // initialize the event pool + // initialize the event pool. + // When zero is provided as seed, the global ROOT random sequence is followed + // so that the seed given to the o2-sim or o2-sim-dpl-eventgen + // also determines which files of the pool the job will pick if (mConfig.rngseed > 0) { mRandomEngine.seed(mConfig.rngseed); } else { - std::random_device rd; - mRandomEngine.seed(rd()); + mRandomEngine.seed(gRandom->Integer(std::numeric_limits::max())); } TString expPath(mConfig.eventPoolPath); gSystem->ExpandPathName(expPath); @@ -408,25 +589,31 @@ bool GeneratorFromEventPool::Init() } LOG(info) << "Found " << mPoolFilesAvailable.size() << " available event pool files"; - // now choose the actual file - std::uniform_int_distribution distribution(0, mPoolFilesAvailable.size() - 1); - auto chosenIndex = distribution(mRandomEngine); - mFileChosen = mPoolFilesAvailable[chosenIndex]; - LOG(info) << "EventPool is using file " << mFileChosen; + // shuffle the pool so that different jobs go through it in a different order + mFilesChosen = selectFiles(mPoolFilesAvailable); + LOG(info) << "EventPool will go through all " << mFilesChosen.size() << " pool files"; - // we bring up the internal mO2KineGenerator + // we bring up the internal mO2KineGenerator with the shuffled file list auto kine_config = O2KineGenConfig{ .skipNonTrackable = mConfig.skipNonTrackable, .continueMode = false, - .roundRobin = false, + .roundRobin = mConfig.roundRobin, .randomize = mConfig.randomize, .rngseed = mConfig.rngseed, - .randomphi = mConfig.randomphi, - .fileName = mFileChosen}; - mO2KineGenerator.reset(new GeneratorFromO2Kine(kine_config)); + .randomphi = mConfig.randomphi}; + mO2KineGenerator.reset(new GeneratorFromO2Kine(kine_config, mFilesChosen)); return mO2KineGenerator->Init(); } +std::vector GeneratorFromEventPool::selectFiles(std::vector const& universe) +{ + // shuffles the whole pool universe so that different jobs go through it in a + // different order + auto result = universe; + std::shuffle(result.begin(), result.end(), mRandomEngine); + return result; +} + namespace { namespace fs = std::filesystem; diff --git a/Generators/test/test_EventPoolChaining.cxx b/Generators/test/test_EventPoolChaining.cxx new file mode 100644 index 0000000000000..dc58bd7ab46f9 --- /dev/null +++ b/Generators/test/test_EventPoolChaining.cxx @@ -0,0 +1,663 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +/// \file test_EventPoolChaining.cxx +/// \brief tests reading several (event pool) kinematics files one after the other +/// in GeneratorFromO2Kine and GeneratorFromEventPool +/// \author M. Giacalone, mgiacalo@cern.ch, 08/2026 + +#define BOOST_TEST_MODULE Test EventPoolChaining +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +namespace +{ + +/// the px of the first track of an event encodes the file it belongs to and its +/// position within that file; this allows to check the reading order later on +double encodeMomentum(int fileTag, int event) +{ + return 1000. * fileTag + event; +} + +/// creates a minimal - but structurally valid - O2 kinematics file; +/// event `ev` contains `ev + 1` primary tracks +void createKineFile(std::string const& path, int nevents, int fileTag) +{ + std::unique_ptr file(TFile::Open(path.c_str(), "RECREATE")); + auto tree = new TTree("o2sim", "o2sim"); // owned by the file + std::vector tracks; + tree->Branch("MCTrack", &tracks); + o2::dataformats::MCEventHeader header; + auto headerPtr = &header; + tree->Branch("MCEventHeader.", &headerPtr); + + for (int ev = 0; ev < nevents; ++ev) { + tracks.clear(); + for (int i = 0; i <= ev; ++i) { + // a primary track has no mothers + o2::MCTrack track(211, -1, -1, -1, -1, encodeMomentum(fileTag, ev), 0., 0., 0., 0., 0., 0., 0); + track.setToBeDone(true); + tracks.push_back(track); + } + header.Reset(); + header.SetEventID(static_cast(encodeMomentum(fileTag, ev))); + header.SetVertex(0., 0., 0.); + header.putInfo("test_fileTag", fileTag); + tree->Fill(); + } + tree->Write(); + file->Close(); +} + +/// creates `nfiles` event pool files under `//evtpool.root`; +/// file i holds i + 2 events +std::vector createPool(fs::path const& tmpDir, int nfiles) +{ + std::vector filenames; + for (int i = 0; i < nfiles; ++i) { + auto fileDir = tmpDir / std::to_string(i); + fs::create_directories(fileDir); + auto filePath = fileDir / o2::eventgen::GeneratorFromEventPool::eventpool_filename; + createKineFile(filePath.string(), i + 2, i); + filenames.push_back(filePath.string()); + } + return filenames; +} + +/// number of events in pool file i (must match createPool) +int eventsInFile(int i) { return i + 2; } + +/// scratch directory that removes itself again; it has to be declared before the +/// generators using it, so that the generators (and with them the open files) are +/// destructed first +struct TempDir { + explicit TempDir(std::string const& tag) + { + path = fs::temp_directory_path() / (tag + "_" + std::to_string(getpid()) + "_" + std::to_string(std::rand())); + std::error_code ec; + fs::remove_all(path, ec); + fs::create_directories(path); + } + ~TempDir() + { + std::error_code ec; + fs::remove_all(path, ec); + } + fs::path path; +}; + +/// number of ROOT files currently open in the process +int openRootFiles() +{ + return gROOT->GetListOfFiles() ? gROOT->GetListOfFiles()->GetEntries() : 0; +} + +/// reads the next event and returns the identifier encoded in the first track +double readNextEvent(o2::eventgen::Generator& gen) +{ + gen.clearParticles(); + if (!gen.importParticles()) { + return -1.; + } + auto const& particles = gen.getParticles(); + if (particles.empty()) { + return -1.; + } + return particles.front().Px(); +} + +} // namespace + +/// several files are read one after the other, in the order in which they were given +BOOST_AUTO_TEST_CASE(Rollover_MultipleFiles_Sequential) +{ + TempDir tmpDirGuard("rollover_sequential"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 3; + auto filenames = createPool(tmpDir, numfiles); + + o2::eventgen::GeneratorFromO2Kine gen(filenames); + BOOST_CHECK_EQUAL(gen.getNumberOfFiles(), numfiles); + BOOST_CHECK(gen.Init()); + // only the first file is known/open at this point + BOOST_CHECK_EQUAL(gen.getEventsAvailable(), eventsInFile(0)); + BOOST_CHECK_EQUAL(gen.getCurrentFileIndex(), 0); + + // events must come out file by file, in order + for (int file = 0; file < numfiles; ++file) { + for (int ev = 0; ev < eventsInFile(file); ++ev) { + BOOST_CHECK_CLOSE(readNextEvent(gen), encodeMomentum(file, ev), 1E-6); + BOOST_CHECK_EQUAL(gen.getCurrentFileIndex(), file); + } + } + // all files are used up now; asking for more is a fatal condition, which cannot be + // checked from within the process (see the example script for the end-to-end check) + BOOST_CHECK_EQUAL(gen.getNumberOfFilesUsed(), numfiles); +} + +/// the files must be opened lazily: only when the events of the current one are +/// exhausted, and never more than one at a time +BOOST_AUTO_TEST_CASE(Rollover_OpensFilesLazily) +{ + TempDir tmpDirGuard("rollover_lazy"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 4; + auto filenames = createPool(tmpDir, numfiles); + + auto filesOpenBefore = openRootFiles(); + + o2::eventgen::GeneratorFromO2Kine gen(filenames); + // the constructor must not open anything at all + BOOST_CHECK_EQUAL(openRootFiles() - filesOpenBefore, 0); + BOOST_CHECK(gen.Init()); + // exactly one input file is open, no matter how many were given + BOOST_CHECK_EQUAL(openRootFiles() - filesOpenBefore, 1); + BOOST_CHECK_EQUAL(gen.getNumberOfFilesUsed(), 1); + + // reading the events of the first file must not touch any other file + for (int ev = 0; ev < eventsInFile(0); ++ev) { + readNextEvent(gen); + BOOST_CHECK_EQUAL(gen.getNumberOfFilesUsed(), 1); + BOOST_CHECK_EQUAL(openRootFiles() - filesOpenBefore, 1); + } + + // the next event triggers opening the second file - and only the second one + readNextEvent(gen); + BOOST_CHECK_EQUAL(gen.getCurrentFileIndex(), 1); + BOOST_CHECK_EQUAL(gen.getNumberOfFilesUsed(), 2); + BOOST_CHECK_EQUAL(openRootFiles() - filesOpenBefore, 1); +} + +/// a file which cannot be read is only discovered - and skipped - when it is reached +BOOST_AUTO_TEST_CASE(Rollover_SkipsBadFilesLazily) +{ + TempDir tmpDirGuard("rollover_badfiles"); + auto const& tmpDir = tmpDirGuard.path; + auto good0 = (tmpDir / "good0.root").string(); + auto good1 = (tmpDir / "good1.root").string(); + createKineFile(good0, 2, 0); + createKineFile(good1, 2, 1); + auto nonexisting = (tmpDir / "doesnotexist.root").string(); + + // the broken file sits in the middle of the list: construction must succeed + o2::eventgen::GeneratorFromO2Kine gen({good0, nonexisting, good1}); + BOOST_CHECK_EQUAL(gen.getNumberOfFiles(), 3); + BOOST_CHECK(gen.Init()); + BOOST_CHECK_EQUAL(gen.getCurrentFileIndex(), 0); + + // the two events of the first file, then the broken one is skipped + BOOST_CHECK_CLOSE(readNextEvent(gen), encodeMomentum(0, 0), 1E-6); + BOOST_CHECK_CLOSE(readNextEvent(gen), encodeMomentum(0, 1), 1E-6); + BOOST_CHECK_CLOSE(readNextEvent(gen), encodeMomentum(1, 0), 1E-6); + BOOST_CHECK_EQUAL(gen.getCurrentFileIndex(), 2); + + // nothing usable at all -> Init must fail rather than crash + o2::eventgen::GeneratorFromO2Kine badgen({nonexisting}); + BOOST_CHECK(!badgen.Init()); +} + +/// the number of particles per event must be preserved across the file boundaries +BOOST_AUTO_TEST_CASE(Rollover_ParticleContent) +{ + TempDir tmpDirGuard("rollover_content"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 3; + auto filenames = createPool(tmpDir, numfiles); + + o2::eventgen::GeneratorFromO2Kine gen(filenames); + BOOST_CHECK(gen.Init()); + + for (int file = 0; file < numfiles; ++file) { + for (int ev = 0; ev < eventsInFile(file); ++ev) { + gen.clearParticles(); + BOOST_CHECK(gen.importParticles()); + BOOST_CHECK_EQUAL(gen.getParticles().size(), static_cast(ev + 1)); + } + } +} + +/// the MC event header of the original file must be forwarded also when reading +/// from a file that is not the first one of the list +BOOST_AUTO_TEST_CASE(Rollover_HeaderForwarding) +{ + TempDir tmpDirGuard("rollover_header"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 3; + auto filenames = createPool(tmpDir, numfiles); + + o2::eventgen::GeneratorFromO2Kine gen(filenames); + BOOST_CHECK(gen.Init()); + + for (int file = 0; file < numfiles; ++file) { + for (int ev = 0; ev < eventsInFile(file); ++ev) { + gen.clearParticles(); + BOOST_CHECK(gen.importParticles()); + + o2::dataformats::MCEventHeader header; + gen.updateHeader(&header); + BOOST_CHECK_EQUAL(static_cast(header.GetEventID()), static_cast(encodeMomentum(file, ev))); + + bool isvalid = false; + auto tag = header.getInfo("test_fileTag", isvalid); + BOOST_CHECK(isvalid); + BOOST_CHECK_EQUAL(tag, file); + + // the bookkeeping information must point to the file the event was read from + auto inputFile = header.getInfo("forwarding-generator_inputFile", isvalid); + BOOST_CHECK(isvalid); + BOOST_CHECK_EQUAL(inputFile, filenames[file]); + + // ... and to the entry within that very file + auto entry = header.getInfo("forwarding-generator_inputEventNumber", isvalid); + BOOST_CHECK(isvalid); + BOOST_CHECK_EQUAL(entry, ev); + } + } +} + +/// a comma-separated list of file names is read one file after the other as well +BOOST_AUTO_TEST_CASE(Rollover_CommaSeparatedFileNames) +{ + TempDir tmpDirGuard("rollover_commalist"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 3; + auto filenames = createPool(tmpDir, numfiles); + + std::string joined; + for (auto const& f : filenames) { + joined += (joined.empty() ? "" : ",") + f; + } + + auto splitted = o2::eventgen::GeneratorFromO2Kine::splitFileNames(joined); + BOOST_CHECK_EQUAL(splitted.size(), static_cast(numfiles)); + // white space around the separators must be tolerated + BOOST_CHECK_EQUAL(o2::eventgen::GeneratorFromO2Kine::splitFileNames(" a.root , b.root ,,").size(), 2u); + + o2::eventgen::GeneratorFromO2Kine gen(joined.c_str()); + BOOST_CHECK_EQUAL(gen.getNumberOfFiles(), numfiles); + BOOST_CHECK(gen.Init()); + for (int file = 0; file < numfiles; ++file) { + for (int ev = 0; ev < eventsInFile(file); ++ev) { + BOOST_CHECK_CLOSE(readNextEvent(gen), encodeMomentum(file, ev), 1E-6); + } + } +} + +/// round robin must wrap around the whole file list, not around a single file +BOOST_AUTO_TEST_CASE(Rollover_RoundRobin) +{ + TempDir tmpDirGuard("rollover_roundrobin"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 2; + auto filenames = createPool(tmpDir, numfiles); + const int total = eventsInFile(0) + eventsInFile(1); + + o2::eventgen::O2KineGenConfig config; + config.roundRobin = true; + o2::eventgen::GeneratorFromO2Kine gen(config, filenames); + BOOST_CHECK(gen.Init()); + + // read twice as many events as available; the second pass must repeat the first one + std::vector firstPass; + for (int i = 0; i < total; ++i) { + firstPass.push_back(readNextEvent(gen)); + } + for (int i = 0; i < total; ++i) { + BOOST_CHECK_CLOSE(readNextEvent(gen), firstPass[i], 1E-6); + } +} + +/// a single file, read without round robin, must NOT be silently reopened/reused once +/// exhausted: openNextFile()'s "next == mCurrentFileIndex && mCurrentFile" shortcut +/// (which restarts the currently open file in place) may only ever fire when wrapAround +/// (i.e. roundRobin) is true; with roundRobin off, running out of events is fatal +BOOST_AUTO_TEST_CASE(SingleFile_NoRoundRobin_ExhaustionIsFatal) +{ + TempDir tmpDirGuard("single_file_no_rr"); + auto const& tmpDir = tmpDirGuard.path; + auto file = (tmpDir / "kine.root").string(); + constexpr int nevents = 3; + createKineFile(file, nevents, 0); + + o2::eventgen::O2KineGenConfig config; + config.roundRobin = false; + o2::eventgen::GeneratorFromO2Kine gen(config, {file}); + BOOST_CHECK(gen.Init()); + + // all events of the single file are served normally, without any repeats + std::set seen; + for (int i = 0; i < nevents; ++i) { + auto id = readNextEvent(gen); + BOOST_CHECK(id >= 0.); + BOOST_CHECK(seen.insert(id).second); + } + BOOST_CHECK_EQUAL(seen.size(), static_cast(nevents)); + + // the next request must crash the job (fatal), not silently restart the same file + BOOST_CHECK_THROW(readNextEvent(gen), fair::FatalException); +} + +/// the same single-file setup, but with roundRobin enabled: the already-open file must +/// be reused in place (no reopen), giving a fresh pass of the very same events +BOOST_AUTO_TEST_CASE(SingleFile_RoundRobin_ReusesWithoutReopening) +{ + TempDir tmpDirGuard("single_file_rr"); + auto const& tmpDir = tmpDirGuard.path; + auto file = (tmpDir / "kine.root").string(); + constexpr int nevents = 3; + createKineFile(file, nevents, 0); + + auto filesOpenBefore = openRootFiles(); + + o2::eventgen::O2KineGenConfig config; + config.roundRobin = true; + o2::eventgen::GeneratorFromO2Kine gen(config, {file}); + BOOST_CHECK(gen.Init()); + BOOST_CHECK_EQUAL(openRootFiles() - filesOpenBefore, 1); + + std::vector firstPass; + for (int i = 0; i < nevents; ++i) { + firstPass.push_back(readNextEvent(gen)); + } + // wrapping around must not close and reopen the file + for (int i = 0; i < nevents; ++i) { + BOOST_CHECK_CLOSE(readNextEvent(gen), firstPass[i], 1E-6); + BOOST_CHECK_EQUAL(openRootFiles() - filesOpenBefore, 1); + } + BOOST_CHECK_EQUAL(gen.getNumberOfFilesUsed(), 1); +} + +/// round robin must serve every event of every file, in order, and only then start +/// over with the first file again - for an arbitrary number of passes +BOOST_AUTO_TEST_CASE(Rollover_RoundRobin_FullPasses) +{ + TempDir tmpDirGuard("rollover_rr_full"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 3; + auto filenames = createPool(tmpDir, numfiles); + + auto filesOpenBefore = openRootFiles(); + + o2::eventgen::O2KineGenConfig config; + config.roundRobin = true; + o2::eventgen::GeneratorFromO2Kine gen(config, filenames); + BOOST_CHECK(gen.Init()); + + constexpr int npasses = 3; + for (int pass = 0; pass < npasses; ++pass) { + for (int file = 0; file < numfiles; ++file) { + for (int ev = 0; ev < eventsInFile(file); ++ev) { + // the very same sequence must come back on every pass + BOOST_CHECK_CLOSE(readNextEvent(gen), encodeMomentum(file, ev), 1E-6); + BOOST_CHECK_EQUAL(gen.getCurrentFileIndex(), file); + BOOST_CHECK_EQUAL(gen.getEventsAvailable(), eventsInFile(file)); + // laziness is not given up when wrapping around + BOOST_CHECK_EQUAL(openRootFiles() - filesOpenBefore, 1); + } + } + } + // every file was opened exactly once per pass + BOOST_CHECK_EQUAL(gen.getNumberOfFilesUsed(), numfiles * npasses); +} + +/// with randomization each round robin pass must again contain every event exactly +/// once, but in a freshly drawn order +BOOST_AUTO_TEST_CASE(Rollover_RoundRobin_RandomizedPasses) +{ + TempDir tmpDirGuard("rollover_rr_random"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 3; + auto filenames = createPool(tmpDir, numfiles); + + int total = 0; + std::set allEvents; + for (int file = 0; file < numfiles; ++file) { + total += eventsInFile(file); + for (int ev = 0; ev < eventsInFile(file); ++ev) { + allEvents.insert(encodeMomentum(file, ev)); + } + } + + o2::eventgen::O2KineGenConfig config; + config.roundRobin = true; + config.randomize = true; + config.rngseed = 99; + o2::eventgen::GeneratorFromO2Kine gen(config, filenames); + BOOST_CHECK(gen.Init()); + + constexpr int npasses = 4; + std::set> passOrders; + for (int pass = 0; pass < npasses; ++pass) { + std::set seen; + std::vector order; + for (int i = 0; i < total; ++i) { + auto id = readNextEvent(gen); + BOOST_CHECK(seen.insert(id).second); // no event twice within a pass + order.push_back(id); + } + // a full pass covers the whole input, no more and no less + BOOST_CHECK(seen == allEvents); + passOrders.insert(order); + } + // the passes must not all come out in the very same order + BOOST_CHECK(passOrders.size() > 1); +} + +/// the generator knows, through Generator::gTotalNEvents, how many events the job is +/// going to ask for, and counts how many it has actually served +BOOST_AUTO_TEST_CASE(Rollover_AccountsForRequestedEvents) +{ + TempDir tmpDirGuard("rollover_accounting"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 3; + auto filenames = createPool(tmpDir, numfiles); + int total = 0; + for (int i = 0; i < numfiles; ++i) { + total += eventsInFile(i); + } + + // this is what o2-sim / o2-sim-dpl-eventgen do before creating the generators + unsigned int requested = total; + o2::eventgen::Generator::setTotalNEvents(requested); + BOOST_CHECK_EQUAL(o2::eventgen::Generator::getTotalNEvents(), static_cast(total)); + + o2::eventgen::GeneratorFromO2Kine gen(filenames); + BOOST_CHECK(gen.Init()); + BOOST_CHECK_EQUAL(gen.getEventsServed(), 0); + + for (int i = 0; i < total; ++i) { + BOOST_CHECK(readNextEvent(gen) >= 0.); + // the counter runs over the whole input, not per file + BOOST_CHECK_EQUAL(gen.getEventsServed(), i + 1); + } + BOOST_CHECK_EQUAL(gen.getEventsServed(), total); + + unsigned int reset = 0; + o2::eventgen::Generator::setTotalNEvents(reset); +} + +/// the event pool generator goes through the whole pool, one file after the other +BOOST_AUTO_TEST_CASE(EventPool_RollsOverAllFiles) +{ + TempDir tmpDirGuard("evtpool_rollover"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 5; + createPool(tmpDir, numfiles); + + int expectedEvents = 0; + for (int i = 0; i < numfiles; ++i) { + expectedEvents += eventsInFile(i); + } + + auto filesOpenBefore = openRootFiles(); + + o2::eventgen::EventPoolGenConfig config; + config.eventPoolPath = tmpDir.string(); + config.randomize = false; + config.rngseed = 42; + o2::eventgen::GeneratorFromEventPool gen(config); + BOOST_CHECK(gen.Init()); + BOOST_CHECK_EQUAL(gen.getFileUniverse().size(), static_cast(numfiles)); + BOOST_CHECK_EQUAL(gen.getChosenFiles().size(), static_cast(numfiles)); + BOOST_CHECK_EQUAL(gen.getO2KineGenerator()->getNumberOfFiles(), numfiles); + // still only one file open, whatever the size of the pool + BOOST_CHECK_EQUAL(gen.getO2KineGenerator()->getNumberOfFilesUsed(), 1); + BOOST_CHECK_EQUAL(openRootFiles() - filesOpenBefore, 1); + + // every single event of the pool must be delivered exactly once + std::set seen; + for (int i = 0; i < expectedEvents; ++i) { + auto id = readNextEvent(gen); + BOOST_CHECK(id >= 0.); + BOOST_CHECK(seen.insert(id).second); // no duplicates + BOOST_CHECK_EQUAL(openRootFiles() - filesOpenBefore, 1); + } + BOOST_CHECK_EQUAL(seen.size(), static_cast(expectedEvents)); + BOOST_CHECK_EQUAL(gen.getO2KineGenerator()->getNumberOfFilesUsed(), numfiles); +} + +/// the order in which the pool files are visited must be reproducible for a given seed +BOOST_AUTO_TEST_CASE(EventPool_SelectionIsReproducible) +{ + TempDir tmpDirGuard("evtpool_selection"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 8; + createPool(tmpDir, numfiles); + + auto orderFor = [&tmpDir](unsigned int seed) { + o2::eventgen::EventPoolGenConfig config; + config.eventPoolPath = tmpDir.string(); + config.rngseed = seed; + o2::eventgen::GeneratorFromEventPool gen(config); + gen.Init(); + return gen.getChosenFiles(); + }; + + // the same seed always gives the same order, and every file is included + auto a = orderFor(1); + auto b = orderFor(1); + BOOST_CHECK_EQUAL(a.size(), static_cast(numfiles)); + BOOST_CHECK(a == b); + + // ... while different seeds do not all collapse onto the same order + std::set> orders; + for (unsigned int seed = 1; seed <= 10; ++seed) { + orders.insert(orderFor(seed)); + } + BOOST_CHECK(orders.size() > 1); +} + +/// with randomization every event of the pool is still served exactly once: the order +/// within a file is a permutation of its entries, fixed when the file is opened +BOOST_AUTO_TEST_CASE(EventPool_RandomizeIsAPermutation) +{ + TempDir tmpDirGuard("evtpool_randomize"); + auto const& tmpDir = tmpDirGuard.path; + constexpr int numfiles = 4; + createPool(tmpDir, numfiles); + + int expectedEvents = 0; + for (int i = 0; i < numfiles; ++i) { + expectedEvents += eventsInFile(i); + } + + auto filesOpenBefore = openRootFiles(); + + o2::eventgen::EventPoolGenConfig config; + config.eventPoolPath = tmpDir.string(); + config.randomize = true; + config.rngseed = 12345; + o2::eventgen::GeneratorFromEventPool gen(config); + BOOST_CHECK(gen.Init()); + + std::set seen; + std::vector order; + for (int i = 0; i < expectedEvents; ++i) { + auto id = readNextEvent(gen); + BOOST_CHECK(id >= 0.); + // no event is served twice ... + BOOST_CHECK(seen.insert(id).second); + order.push_back(id); + // ... and still only one file is open + BOOST_CHECK_EQUAL(openRootFiles() - filesOpenBefore, 1); + } + BOOST_CHECK_EQUAL(seen.size(), static_cast(expectedEvents)); + BOOST_CHECK_EQUAL(gen.getO2KineGenerator()->getNumberOfFilesUsed(), numfiles); + + // the order must actually differ from the sequential one + auto sorted = order; + std::sort(sorted.begin(), sorted.end()); + BOOST_CHECK(order != sorted); +} + +/// randomization is a permutation also for a single file, and round robin gives a +/// fresh permutation on every pass +BOOST_AUTO_TEST_CASE(SingleFile_RandomizeRoundRobin) +{ + TempDir tmpDirGuard("single_randomize"); + auto const& tmpDir = tmpDirGuard.path; + auto file = (tmpDir / "kine.root").string(); + constexpr int nevents = 6; + createKineFile(file, nevents, 0); + + o2::eventgen::O2KineGenConfig config; + config.randomize = true; + config.roundRobin = true; + config.rngseed = 7; + o2::eventgen::GeneratorFromO2Kine gen(config, {file}); + BOOST_CHECK(gen.Init()); + + // first pass: every event exactly once + std::set firstPass; + std::vector firstOrder; + for (int i = 0; i < nevents; ++i) { + auto id = readNextEvent(gen); + BOOST_CHECK(firstPass.insert(id).second); + firstOrder.push_back(id); + } + BOOST_CHECK_EQUAL(firstPass.size(), static_cast(nevents)); + + // second pass: same events again, but re-shuffled and without reopening the file + std::set secondPass; + std::vector secondOrder; + for (int i = 0; i < nevents; ++i) { + auto id = readNextEvent(gen); + BOOST_CHECK(secondPass.insert(id).second); + secondOrder.push_back(id); + } + BOOST_CHECK(firstPass == secondPass); + BOOST_CHECK(firstOrder != secondOrder); + BOOST_CHECK_EQUAL(gen.getNumberOfFilesUsed(), 1); +} diff --git a/run/SimExamples/EventPool_Chaining/README.md b/run/SimExamples/EventPool_Chaining/README.md new file mode 100644 index 0000000000000..002ce1aa7fc49 --- /dev/null +++ b/run/SimExamples/EventPool_Chaining/README.md @@ -0,0 +1,127 @@ + + +# Chaining of event pools + +The `evtpool` generator goes through the **whole pool**, one file at a time: it starts +with the first one and, once its events are exhausted, moves on to the next. The files +are opened strictly one at a time and only when they are actually needed — a pool of a +thousand files costs exactly one open file handle, and a job that never gets past the +first file never touches the others. This is what is needed on hyperloop, where many +events have to be served from a pool made of many small files. + +# Configuration + +| key | default | meaning | +| --- | --- | --- | +| `GeneratorEventPool.eventPoolPath` | `""` | pool directory, a text file with a list of files, or a single `evtpool.root` | +| `GeneratorEventPool.randomize` | `true` | serve the events of each file in random order (a permutation, every event exactly once) | +| `GeneratorEventPool.roundRobin` | `false` | start over with the first file once the last one is exhausted | +| `GeneratorEventPool.rngseed` | `0` | seed used both for the order in which files are visited and for the event randomization | + +The order in which the pool files are visited is shuffled per job, so that different jobs +of the same production do not all read the same files in the same order. Using a fixed +`rngseed` makes that order reproducible. No file is opened at initialisation time apart +from the first one — the shuffle only picks *names*. + +Order in which events are served — it is fixed for a file at the moment that file is +opened, and every one of its events is used **exactly once** (different behaviour than the past): + +* `randomize=false`: entry 0, 1, 2, … of the first file, then entry 0, 1, 2, … of the + second file, and so on; +* `randomize=true` (the default): a random permutation of the entries of the first file, + then a random permutation of the entries of the second one, and so on. + +Once the last file has been used up the job **fails with a fatal error** unless +`roundRobin=true` is set. Making sure that the pool holds enough events for the +requested `-n` is the responsibility of the user: + +``` +[FATAL] GeneratorFromO2Kine: ran out of events after 6 event(s) from 3 input file(s) + (9 were requested). Provide more input files/events or allow reusing them via roundRobin +``` + +With `roundRobin=true` the generator starts over from the first file; in randomized mode +a fresh permutation is drawn on every pass. + +Example: + +```bash +o2-sim -n 100 -g evtpool --configKeyValues "GeneratorEventPool.eventPoolPath=/path/to/pool" +``` + +The same works for AliEn pools: + +```bash +o2-sim -n 100 -g evtpool \ + --configKeyValues "GeneratorEventPool.eventPoolPath=alien:///alice/cern.ch/user/.../evtpool_dir" +``` + +and inside the hybrid generator JSON configuration: + +```json +{ + "name": "evtpool", + "config": { + "eventPoolPath": "/path/to/pool", + "skipNonTrackable": true, + "roundRobin": false, + "randomize": false, + "rngseed": 0, + "randomphi": false + } +} +``` + +## Use from the DPL event generator (hyperloop) + +The very same configuration works with `o2-sim-dpl-eventgen`, which is the entry point +used on hyperloop: + +```bash +o2-sim-dpl-eventgen -b --nEvents 1000 --generator evtpool --vertexMode kNoVertex \ + --configKeyValues "GeneratorEventPool.eventPoolPath=/path/to/pool" |\ + o2-sim-mctracks-to-aod -b |\ + o2-analysis-mctracks-to-aod-simple-task -b +``` + +`rundpl.sh` in this folder runs that pipeline for the pool created by `run.sh`. + +Note that the seed given to the driver (`o2-sim --seed` / `o2-sim-dpl-eventgen --seed`) +governs both the order in which the pool files are visited and the event order inside them, +as long as `GeneratorEventPool.rngseed` is left at its default of 0. Setting `rngseed` +explicitly overrides the driver seed for the event pool. + +Example of a workflow reading from an event pool: + +```bash +${O2DPG_ROOT}/MC/bin/o2dpg_sim_workflow.py -eCM 900 -col pp -gen evtpool -tf 1 -ns 7 \ + -e TGeant4 -j 4 -interactionRate 50000 -run 300000 -seed 12345 \ + -confKey "GeneratorEventPool.eventPoolPath=/path/to/pool" +${O2DPG_ROOT}/MC/bin/o2dpg_workflow_runner.py -f workflow.json -tt aod +``` + +## Reading several plain kinematics files (`extkinO2`) + +The same machinery is available for the ordinary `extkinO2` generator by passing a +comma-separated list of files, which are again read one after the other: + +```bash +o2-sim -n 100 -g extkinO2 --extKinFile "kine1.root,kine2.root,kine3.root" +# or +o2-sim -n 100 -g extkinO2 --configKeyValues "GeneratorFromO2Kine.fileName=kine1.root,kine2.root" +``` + +# Provenance of the events + +Every generated event stores the file it was read from and the entry inside that file in +its MC event header: + +* `forwarding-generator_inputFile` +* `forwarding-generator_inputEventNumber` (entry within that file) + +# Files description + +- **run.sh** → creates a small event pool and reads it back +- **rundpl.sh** → the same through `o2-sim-dpl-eventgen` (the hyperloop path) diff --git a/run/SimExamples/EventPool_Chaining/run.sh b/run/SimExamples/EventPool_Chaining/run.sh new file mode 100755 index 0000000000000..4902bc06674ce --- /dev/null +++ b/run/SimExamples/EventPool_Chaining/run.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# +# Example showing how a job goes through an event pool made of several files: one +# file is read at a time and the next one is opened only once the events of the +# current one are exhausted. +# +# Stage 1: create a small event pool made of NPOOLFILES files +# Stage 2: read the pool back in its different modes +# +# Note: the example runs with '--noGeant' and a minimal geometry ('-m PIPE'), +# since only the event generation part is of interest here. +# +set -x +set -e + +[ ! "${O2_ROOT}" ] && echo "Error: This needs O2 loaded" && exit 1 + +NPOOLFILES=3 # number of files the pool consists of +NEVENTS_PER_FILE=3 # events per pool file +NEVENTS=9 # events to be read back from the whole pool + +POOLDIR=${PWD}/eventpool +COMMON="-j 4 --noGeant --vertexMode kNoVertex" + +# --------------------------------------------------------------------------- +# Stage 1: produce the event pool +# --------------------------------------------------------------------------- +# An event pool is a set of kinematics files that are all called 'evtpool.root' +# and that live in separate sub-directories of a common pool directory. +rm -rf ${POOLDIR} +for i in $(seq 0 $((NPOOLFILES - 1))); do + o2-sim ${COMMON} -n ${NEVENTS_PER_FILE} -g pythia8pp -o poolgen_${i} --seed $((i + 1)) + mkdir -p ${POOLDIR}/00${i} + mv poolgen_${i}_Kine.root ${POOLDIR}/00${i}/evtpool.root +done + +# --------------------------------------------------------------------------- +# Stage 2a: sequential roll-over through the whole pool +# --------------------------------------------------------------------------- +# The generator moves on to the next pool file once the events of the current one +# are used up, so that NPOOLFILES * NEVENTS_PER_FILE events are available in total. +# Only one file is open at a time. +o2-sim ${COMMON} -n ${NEVENTS} -g evtpool -o rollover \ + --configKeyValues "GeneratorEventPool.eventPoolPath=${POOLDIR};GeneratorEventPool.randomize=false;GeneratorEventPool.rngseed=1" + +# --------------------------------------------------------------------------- +# Stage 2b: start over with the first file once the pool is exhausted +# --------------------------------------------------------------------------- +o2-sim ${COMMON} -n $((NEVENTS + NEVENTS_PER_FILE)) -g evtpool -o rollover_roundrobin \ + --configKeyValues "GeneratorEventPool.eventPoolPath=${POOLDIR};GeneratorEventPool.roundRobin=true;GeneratorEventPool.randomize=false;GeneratorEventPool.rngseed=1" + +# --------------------------------------------------------------------------- +# Stage 2c: randomized access (the event pool default) across the files +# --------------------------------------------------------------------------- +# The entries of each file are served in a random permutation, so every event of +# the pool is still used exactly once. +o2-sim ${COMMON} -n ${NEVENTS} -g evtpool -o rollover_random \ + --configKeyValues "GeneratorEventPool.eventPoolPath=${POOLDIR};GeneratorEventPool.rngseed=1" + +# --------------------------------------------------------------------------- +# Stage 2d: asking for more events than the pool holds must fail +# --------------------------------------------------------------------------- +# It is up to the user to provide enough events; running out of them is fatal. +if o2-sim ${COMMON} -n $((NEVENTS + 1)) -g evtpool -o toofew \ + --configKeyValues "GeneratorEventPool.eventPoolPath=${POOLDIR};GeneratorEventPool.rngseed=1"; then + echo "ERROR: the simulation should have failed with 'ran out of events'" && exit 1 +else + echo "OK: the simulation failed as expected (not enough events in the pool)" +fi + +# --------------------------------------------------------------------------- +# Stage 2e: the same mechanism is available for plain 'extkinO2' by giving a +# comma-separated list of files +# --------------------------------------------------------------------------- +FILELIST=$(ls ${POOLDIR}/*/evtpool.root | paste -sd,) +o2-sim ${COMMON} -n ${NEVENTS} -g extkinO2 -o extkin_rollover --extKinFile "${FILELIST}" diff --git a/run/SimExamples/EventPool_Chaining/rundpl.sh b/run/SimExamples/EventPool_Chaining/rundpl.sh new file mode 100755 index 0000000000000..8fe2e7eb69dbb --- /dev/null +++ b/run/SimExamples/EventPool_Chaining/rundpl.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# +# Same event pool roll-over as run.sh, but through the DPL event generator +# i.e. the path used on hyperloop: +# +# o2-sim-dpl-eventgen | o2-sim-mctracks-to-aod | +# +# Requires the event pool created by run.sh (or any other pool directory given +# via the POOLDIR variable). +# +set -x +set -e + +[ ! "${O2_ROOT}" ] && echo "Error: This needs O2 loaded" && exit 1 + +POOLDIR=${POOLDIR:-${PWD}/eventpool} +[ ! -d "${POOLDIR}" ] && echo "Error: no event pool at ${POOLDIR}; run run.sh first" && exit 2 + +NEVENTS=9 +COMMON="-b --aggregate-timeframe 10 --vertexMode kNoVertex --seed 12345" + +# --------------------------------------------------------------------------- +# a) the whole pool as one event stream +# --------------------------------------------------------------------------- +o2-sim-dpl-eventgen ${COMMON} --nEvents ${NEVENTS} --generator evtpool \ + --configKeyValues "GeneratorEventPool.eventPoolPath=${POOLDIR}" |\ + o2-sim-mctracks-to-aod -b |\ + o2-analysis-mctracks-to-aod-simple-task -b + +# --------------------------------------------------------------------------- +# b) reuse the pool once it is exhausted (2 full passes here) +# --------------------------------------------------------------------------- +o2-sim-dpl-eventgen ${COMMON} --nEvents $((2 * NEVENTS)) --generator evtpool \ + --configKeyValues "GeneratorEventPool.eventPoolPath=${POOLDIR};GeneratorEventPool.roundRobin=true" |\ + o2-sim-mctracks-to-aod -b |\ + o2-analysis-mctracks-to-aod-simple-task -b + +# --------------------------------------------------------------------------- +# c) plain 'extkinO2' with a comma-separated list of files +# --------------------------------------------------------------------------- +FILELIST=$(ls ${POOLDIR}/*/evtpool.root | paste -sd,) +o2-sim-dpl-eventgen ${COMMON} --nEvents ${NEVENTS} --generator extkinO2 \ + --configKeyValues "GeneratorFromO2Kine.fileName=${FILELIST}" |\ + o2-sim-mctracks-to-aod -b |\ + o2-analysis-mctracks-to-aod-simple-task -b + +# --------------------------------------------------------------------------- +# d) asking for more events than the pool holds must fail, not hang +# --------------------------------------------------------------------------- +if o2-sim-dpl-eventgen ${COMMON} --nEvents $((NEVENTS + 1)) --generator evtpool \ + --configKeyValues "GeneratorEventPool.eventPoolPath=${POOLDIR}" |\ + o2-sim-mctracks-to-aod -b |\ + o2-analysis-mctracks-to-aod-simple-task -b; then + echo "ERROR: the workflow should have failed with 'ran out of events'" && exit 1 +else + echo "OK: the workflow failed as expected (not enough events in the pool)" +fi diff --git a/run/SimExamples/README.md b/run/SimExamples/README.md index 3a54625acf413..d65daa268cc9a 100644 --- a/run/SimExamples/README.md +++ b/run/SimExamples/README.md @@ -24,6 +24,7 @@ * \subpage refrunSimExamplesPythia * \subpage refrunSimExamplesForceDecay_Lambda_Neutron_Dalitz * \subpage refrunSimExamplesJustPrimaryKinematics +* \subpage refrunSimExamplesEventPool_Chaining * \subpage refrunSimExamplesSelective_Transport * \subpage refrunSimExamplesSelective_Transport_pi0 * \subpage refrunSimExamplesStepMonitoringSimple1