diff --git a/Generators/CMakeLists.txt b/Generators/CMakeLists.txt index b36fbf5697ac4..094510c2c61a5 100644 --- a/Generators/CMakeLists.txt +++ b/Generators/CMakeLists.txt @@ -139,6 +139,14 @@ if(doBuildSimulation) LABELS generator PUBLIC_LINK_LIBRARIES O2::Generators) + if(HepMC3_FOUND) + o2_add_test(GeneratorHepMCIndexed NAME test_Generator_test_GeneratorHepMCIndexed + SOURCES test/test_GeneratorHepMCIndexed.cxx + COMPONENT_NAME Generator + LABELS generator + PUBLIC_LINK_LIBRARIES O2::Generators) + endif() + # o2_add_test(GeneratorPythia8Param NAME test_Generator_test_GeneratorPythia8Param # SOURCES test/test_GeneratorPythia8Param.cxx # COMPONENT_NAME Generator diff --git a/Generators/include/Generators/GeneratorHepMC.h b/Generators/include/Generators/GeneratorHepMC.h index 176a4020e213f..b582d24e2e79c 100644 --- a/Generators/include/Generators/GeneratorHepMC.h +++ b/Generators/include/Generators/GeneratorHepMC.h @@ -18,6 +18,10 @@ #include "Generators/GeneratorFileOrCmd.h" #include "Generators/GeneratorHepMCParam.h" #include "Generators/GeneratorFileOrCmdParam.h" +#include +#include +#include +#include #ifdef GENERATORS_WITH_HEPMC3_DEPRECATED namespace HepMC @@ -68,7 +72,7 @@ class GeneratorHepMC : public Generator, public GeneratorFileOrCmd * simulation configuration. This is implemented as a member * function so as to better facilitate changes. */ void setup(const GeneratorFileOrCmdParam& param0, - const GeneratorHepMCParam& param, + const HepMCGenConfig& param, const conf::SimConfig& config); // Generator configuration from external local parameters void setup(const FileOrCmdGenConfig& param0, @@ -108,8 +112,19 @@ class GeneratorHepMC : public Generator, public GeneratorFileOrCmd /** methods that can be overridded **/ void updateHeader(o2::dataformats::MCEventHeader* eventHeader) override; - /** Make our reader */ + /** Apply the HepMC-specific configuration */ + void setupHepMC(const HepMCGenConfig& param); + /** Make our reader, taking the next file off the list of file names */ bool makeReader(); + /** Fix the order in which the entries of the input file are served */ + void establishEventOrder(); + /** Index the events of a file by byte offset and open the reader on it, so + * that any entry can later be reached with a single seek. Available only for ASCII format */ + bool buildIndex(const std::string& filename); + /** Read the given entry of the indexed file */ + bool readEntry(int entry); + /** Generate an event following the established event order in random mode */ + Bool_t generateEventOrdered(); /** Type of function to select particles to keep when pruning * events */ @@ -127,8 +142,34 @@ class GeneratorHepMC : public Generator, public GeneratorFileOrCmd HepMC3::GenEvent* mEvent = nullptr; /** Option whether to prune event */ bool mPrune; //! - - ClassDefOverride(GeneratorHepMC, 1); + /** Name of the file the reader is attached to, needed to re-open it */ + std::string mCurrentFileName; //! + /** Order in which the entries of the input file are served */ + std::vector mEventOrder; //! + /** Events already delivered in the current pass over the file */ + int mEventCounter = 0; //! + /** Events delivered in total */ + int mEventsServed = 0; //! + /** Events contained in the input file */ + int mEventsAvailable = 0; //! + /** Entry the reader is currently positioned on, -1 if none */ + int mLastEntryRead = -1; //! + /** Option whether to serve the events in random order */ + bool mRandomize = false; //! + /** Option whether to start over once all events have been used */ + bool mRoundRobin = false; //! + /** Option to have a new order when round-robin enabled */ + bool mReshuffleOnRepeat = true; //! + /** Randomizer seed, 0 to leave gRandom alone */ + unsigned int mRngSeed = 0; //! + /** Whether the indexed input is HepMC2 rather than HepMC3 ASCII */ + bool mIndexedHepMC2 = false; //! + /** The stream the reader is attached to, ours so that we may seek in it */ + std::shared_ptr mIndexedStream; //! + /** Byte offset at which every entry of the input file starts */ + std::vector mEventOffsets; //! + + ClassDefOverride(GeneratorHepMC, 2); }; /** class GeneratorHepMC **/ diff --git a/Generators/include/Generators/GeneratorHepMCParam.h b/Generators/include/Generators/GeneratorHepMCParam.h index bee094075167f..bcadb1a51d222 100644 --- a/Generators/include/Generators/GeneratorHepMCParam.h +++ b/Generators/include/Generators/GeneratorHepMCParam.h @@ -29,7 +29,7 @@ namespace eventgen ** allow the user to modify them **/ -struct GeneratorHepMCParam : public o2::conf::ConfigurableParamHelper { +struct HepMCGenConfig { /** Version number of event structure to decode. Note, when reading * from a file, this key is ignored. The interface will figure out * the version automatically. When reading from a pipe, and the @@ -51,15 +51,19 @@ struct GeneratorHepMCParam : public o2::conf::ConfigurableParamHelper { + O2ParamDef(GeneratorHepMCParam, "HepMC"); }; } // end namespace eventgen diff --git a/Generators/src/GeneratorHepMC.cxx b/Generators/src/GeneratorHepMC.cxx index d493366c46188..10646d4a8379d 100644 --- a/Generators/src/GeneratorHepMC.cxx +++ b/Generators/src/GeneratorHepMC.cxx @@ -17,16 +17,22 @@ #include "SimulationDataFormat/MCEventHeader.h" #include "SimConfig/SimConfig.h" #include "HepMC3/ReaderFactory.h" +#include "HepMC3/ReaderAscii.h" +#include "HepMC3/ReaderAsciiHepMC2.h" #include "HepMC3/GenEvent.h" #include "HepMC3/GenParticle.h" #include "HepMC3/GenVertex.h" #include "HepMC3/FourVector.h" #include "HepMC3/Version.h" #include "TParticle.h" +#include "TRandom.h" #include #include "FairPrimaryGenerator.h" +#include #include +#include +#include #include namespace o2 @@ -91,58 +97,46 @@ void GeneratorHepMC::stop() /*****************************************************************/ void GeneratorHepMC::setup(const GeneratorFileOrCmdParam& param0, - const GeneratorHepMCParam& param, + const HepMCGenConfig& param, const conf::SimConfig& config) { - if (not param.fileName.empty()) { - LOG(warn) << "The use of the key \"HepMC.fileName\" is " - << "deprecated, use \"GeneratorFileOrCmd.fileNames\" instead"; - } - GeneratorFileOrCmd::setup(param0, config); - if (not param.fileName.empty()) { - setFileNames(param.fileName); - } - - mVersion = param.version; - mPrune = param.prune; - setEventsToSkip(param.eventsToSkip); - - // we are skipping ahead in the HepMC stream now - for (int i = 0; i < mEventsToSkip; ++i) { - generateEvent(); - } - - if (param.version != 0 and mCmd.empty()) { - LOG(warn) << "The key \"HepMC.version\" is no longer needed when " - << "reading from files. The format version of the input files " - << "are automatically deduced. However, it is mandatory when reading " - << "from a pipe containing HepMC2 data."; - } + setupHepMC(param); } /*****************************************************************/ void GeneratorHepMC::setup(const FileOrCmdGenConfig& param0, const HepMCGenConfig& param, const conf::SimConfig& config) +{ + GeneratorFileOrCmd::setup(param0, config); + setupHepMC(param); +} + +/*****************************************************************/ + +void GeneratorHepMC::setupHepMC(const HepMCGenConfig& param) { if (not param.fileName.empty()) { LOG(warn) << "The use of the key \"HepMC.fileName\" is " << "deprecated, use \"GeneratorFileOrCmd.fileNames\" instead"; - } - - GeneratorFileOrCmd::setup(param0, config); - if (not param.fileName.empty()) { setFileNames(param.fileName); } mVersion = param.version; mPrune = param.prune; + mRandomize = param.randomize; + mRoundRobin = param.roundRobin; + mReshuffleOnRepeat = param.reshuffleOnRepeat; + mRngSeed = param.rngseed; setEventsToSkip(param.eventsToSkip); - // we are skipping ahead in the HepMC stream now - for (int i = 0; i < mEventsToSkip; ++i) { - generateEvent(); + // we are skipping ahead with this method only in sequential mode + // check establishEventOrder for the random mode + if (not(mRandomize or mRoundRobin)) { + for (uint64_t i = 0; i < mEventsToSkip; ++i) { + generateEvent(); + } } if (param.version != 0 and mCmd.empty()) { @@ -156,6 +150,12 @@ void GeneratorHepMC::setup(const FileOrCmdGenConfig& param0, /*****************************************************************/ Bool_t GeneratorHepMC::generateEvent() { + // when the events are not simply served in the order they appear in the file, + // the entry to read is taken from the order established in Init + if (mRandomize or mRoundRobin) { + return generateEventOrdered(); + } + LOG(debug) << "Generating an event"; /** generate event **/ int tries = 0; @@ -540,6 +540,13 @@ void GeneratorHepMC::updateHeader(o2::dataformats::MCEventHeader* eventHeader) putAttributeInfo(eventHeader, name + post, at); } } + + // When randomised is enabled, the header comes from the last served event + if (mRandomize or mRoundRobin) { + eventHeader->putInfo("forwarding-generator", "generatorHepMC"); + eventHeader->putInfo("forwarding-generator_inputFile", mCurrentFileName); + eventHeader->putInfo("forwarding-generator_inputEventNumber", mLastEntryRead); + } } /*****************************************************************/ @@ -587,6 +594,151 @@ bool GeneratorHepMC::makeReader() /*****************************************************************/ +bool GeneratorHepMC::buildIndex(const std::string& filename) +{ + // Going through the file once to know how many events it holds and to + // record where each of them starts. This way a single + // seek is performed instead of a scan from the current position + mEventOffsets.clear(); + mIndexedStream.reset(); + mIndexedHepMC2 = false; + + HepMC3::InputInfo info(filename); + if (info.m_error or info.m_remote or info.m_pipe or + not(info.m_asciiv3 or info.m_iogenevent)) { + return false; + } + mIndexedHepMC2 = info.m_iogenevent; + + auto stream = std::make_shared(filename); + if (not stream->good()) { + LOG(error) << "Could not open " << filename << " to index its events"; + return false; + } + std::shared_ptr reader; + if (mIndexedHepMC2) { + reader = std::make_shared(stream); + } else { + reader = std::make_shared(stream); + } + if (not reader or reader->failed()) { + LOG(error) << "Could not open " << filename << " to index its events"; + return false; + } + + // the offsets come from the parser itself rather than from guessing at line prefixes + constexpr int max_events = 100000000; + HepMC3::GenEvent event; + while ((int)mEventOffsets.size() < max_events) { + auto here = (std::streamoff)stream->tellg(); + event.clear(); + reader->read_event(event); + if (reader->failed()) { + break; + } + mEventOffsets.push_back(here); + } + if ((int)mEventOffsets.size() >= max_events) { + LOG(warn) << "Stopped indexing the events of " << filename << " at " << max_events; + } + if (mEventOffsets.empty()) { + LOG(error) << "No event found in HepMC file " << filename; + return false; + } + + // keep the reader and its stream: serving an entry is now a seek plus a read. + mCurrentFileName = filename; + mIndexedStream = stream; + mReader = reader; + mLastEntryRead = -1; + LOG(info) << "Indexed " << mEventOffsets.size() << " events of " << filename; + return true; +} + +/*****************************************************************/ + +bool GeneratorHepMC::readEntry(int entry) +{ + // The entry starts at a byte offset recorded by buildIndex + if (entry < 0 or entry >= (int)mEventOffsets.size() or not mIndexedStream or not mReader) { + LOG(error) << "No entry " << entry << " in " << mCurrentFileName; + return false; + } + mIndexedStream->clear(); + mIndexedStream->seekg(mEventOffsets[entry]); + + /** clear and read event **/ + mEvent->clear(); + mReader->read_event(*mEvent); + if (mReader->failed()) { + LOG(error) << "Reading entry " << entry << " of " << mCurrentFileName << " failed"; + return false; + } + /** set units to desired output **/ + mEvent->set_units(HepMC3::Units::GEV, HepMC3::Units::MM); + mLastEntryRead = entry; + LOG(debug) << "Read one event " << mEvent->event_number(); + return true; +} + +/*****************************************************************/ + +void GeneratorHepMC::establishEventOrder() +{ + // Decide the order in which the entries of the input file are served + // The events to skip at the start of the file are left out of the read + auto first = (int)std::min(mEventsToSkip, (uint64_t)std::max(mEventsAvailable, 0)); + mEventOrder.resize(std::max(mEventsAvailable, 0) - first); + std::iota(mEventOrder.begin(), mEventOrder.end(), first); + if (mRandomize) { + // Shuffle based on the ROOT random generator + for (int i = (int)mEventOrder.size() - 1; i > 0; --i) { + auto j = (int)gRandom->Integer(i + 1); + std::swap(mEventOrder[i], mEventOrder[j]); + } + } +} + +/*****************************************************************/ + +Bool_t GeneratorHepMC::generateEventOrdered() +{ + // The entry to be read is fixed by the event order established at file opening + if (mEventCounter >= (int)mEventOrder.size()) { + if (not mRoundRobin) { + auto requested = getTotalNEvents(); + LOG(fatal) << "GeneratorHepMC: ran out of events after " << mEventsServed + << " event(s) from " << mCurrentFileName + << (requested > 0 ? " (" + std::to_string(requested) + " were requested)" : "") + << ". Provide more events or allow reusing them via roundRobin"; + return false; + } + // start over from the beginning of the file, with a fresh order if requested + LOG(info) << "GeneratorHepMC - Reached the end of the input; reusing its events"; + mEventCounter = 0; + if (mReshuffleOnRepeat) { + establishEventOrder(); + } + } + if (mEventOrder.empty()) { + LOG(error) << "GeneratorHepMC: no usable event in " << mCurrentFileName; + return false; + } + + auto entry = mEventOrder[mEventCounter]; + if (mRandomize) { + LOG(info) << "GeneratorHepMC - Picking event " << entry; + } + if (not readEntry(entry)) { + return false; + } + mEventCounter++; + mEventsServed++; + return true; +} + +/*****************************************************************/ + Bool_t GeneratorHepMC::Init() { /** init **/ @@ -674,6 +826,53 @@ Bool_t GeneratorHepMC::Init() } } + // Serving the events in random order + if (mRandomize or mRoundRobin) { + if (not mCmd.empty()) { + LOG(fatal) << "HepMC.randomize/HepMC.roundRobin cannot be used when the events " + << "come from a command, as the pipe can only be read once"; + return false; + } + if (mFileNames.size() != 1) { + LOG(fatal) << "HepMC.randomize/HepMC.roundRobin need exactly one input file, but " + << mFileNames.size() << " were given"; + return false; + } + if (mRngSeed > 0) { + // with a zero the seed given to the driver (o2-sim --seed) stays in control + gRandom->SetSeed(mRngSeed); + } + LOG(info) << "GeneratorHepMC: the event order is drawn with gRandom (" << gRandom->ClassName() + << ") seeded with " << gRandom->GetSeed(); + + auto const& filename = mFileNames.front(); + // Indexing the file gives us both the number of events and constant-time access + // to any of them, and creates the reader we then serve the events from + if (not buildIndex(filename)) { + LOG(fatal) << "HepMC.randomize/HepMC.roundRobin need an input the events can be " + << "picked from in any order, which means a plain HepMC3 or HepMC2 " + << "ASCII file; " << filename << " is not one. Convert it, or convert " + << "it to O2 kinematics and read it back with -g extkinO2, which " + << "randomizes over a TTree"; + return false; + } + mEventsAvailable = (int)mEventOffsets.size(); + if (mEventsToSkip >= (uint64_t)mEventsAvailable) { + LOG(fatal) << "HepMC.eventsToSkip (" << mEventsToSkip << ") leaves no event of the " + << mEventsAvailable << " contained in " << filename; + return false; + } + establishEventOrder(); + auto requested = getTotalNEvents(); + if (requested > 0 and not mRoundRobin and mEventOrder.size() < requested) { + LOG(warn) << "This job will request " << requested << " events, but the input holds " + << "only " << mEventOrder.size() << " usable event(s). The job will stop " + << "with 'ran out of events' - provide more events or enable roundRobin"; + } + LOG(info) << "Reading events from HepMC file " << filename << " (" << mEventsAvailable + << " events, " << (mRandomize ? "randomized" : "sequential") << " order)"; + } + // Create reader for current (first) file return true; } diff --git a/Generators/src/GeneratorsLinkDef.h b/Generators/src/GeneratorsLinkDef.h index 24b3f2e452498..6269a311ee2e0 100644 --- a/Generators/src/GeneratorsLinkDef.h +++ b/Generators/src/GeneratorsLinkDef.h @@ -44,6 +44,7 @@ #pragma link C++ class o2::eventgen::GeneratorHepMC + ; #pragma link C++ class o2::eventgen::HepMCGenConfig + ; #pragma link C++ class o2::eventgen::GeneratorHepMCParam + ; +#pragma link C++ class o2::conf::ConfigurableParamPromoter < o2::eventgen::GeneratorHepMCParam, o2::eventgen::HepMCGenConfig> + ; #endif #ifdef GENERATORS_WITH_PYTHIA6 #pragma link C++ class o2::eventgen::GeneratorPythia6 + ; diff --git a/Generators/test/test_GeneratorHepMCIndexed.cxx b/Generators/test/test_GeneratorHepMCIndexed.cxx new file mode 100644 index 0000000000000..c2ba6116830ed --- /dev/null +++ b/Generators/test/test_GeneratorHepMCIndexed.cxx @@ -0,0 +1,344 @@ +// 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. + +/// Tests for the indexed (seekable) access GeneratorHepMC uses to serve the events +/// of a HepMC3 ASCII file out of order. +/// +/// The first test guards the assumption the whole scheme rests on: that a HepMC3 +/// ASCII reader holds no state between events besides the stream, so that the stream +/// can be repositioned underneath it. That is an implementation detail of HepMC3, not +/// a documented contract, so it has to be re-checked against every version we build +/// against - if it ever stops holding, the generator would silently serve the wrong +/// events rather than fail. +/// @author M. Giacalone - September 2026 +/// co-written with Claude Opus 5 + +#define BOOST_TEST_MODULE Test GeneratorHepMC indexed access +#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 +#include +#include + +namespace +{ +/// the momentum of the outgoing particle of event i, distinct for every event so that +/// an event can be recognised from the particles it produced +double expectedPx(int i) { return 10. + i; } + +/// writes a HepMC3 Asciiv3 file of nEvents events, each with two beam particles and a +/// pair of outgoing pions, and returns its name +std::string writeInput(const std::string& name, int nEvents) +{ + std::ofstream out(name); + out << "HepMC::Version 3.02.05\n" + << "HepMC::Asciiv3-START_EVENT_LISTING\n" + << "W Default\n" + << "T unit-test|indexed-access|\n"; + for (int i = 0; i < nEvents; ++i) { + const double px = expectedPx(i); + out << "E " << i + 1 << " 1 4\n" + << "U GEV MM\n" + << "W 1.0\n" + << "P 1 0 2212 0.000000e+00 0.000000e+00 +6.500000e+03 6.500000e+03 9.383000e-01 4\n" + << "P 2 0 2212 0.000000e+00 0.000000e+00 -6.500000e+03 6.500000e+03 9.383000e-01 4\n" + << "V -1 0.000000e+00 0.000000e+00 0.000000e+00 [1,2]\n" + << "P 3 -1 211 " << px << " 1.000000e+00 1.000000e+00 " << px + 5. << " 1.395700e-01 1\n" + << "P 4 -1 -211 " << -px << " -1.000000e+00 -1.000000e+00 " << px + 5. << " 1.395700e-01 1\n"; + } + out << "HepMC::Asciiv3-END_EVENT_LISTING\n"; + out.close(); + return name; +} + +/// rewrites an Asciiv3 file in the HepMC2 IO_GenEvent format, and returns its name +std::string writeInputHepMC2(const std::string& asciiv3, const std::string& name) +{ + HepMC3::ReaderAscii in(asciiv3); + HepMC3::WriterAsciiHepMC2 out(name); + while (true) { + HepMC3::GenEvent event; + in.read_event(event); + if (in.failed()) { + break; + } + out.write_event(event); + } + in.close(); + out.close(); + return name; +} + +/// everything of an event that has to survive being reached by a seek +std::string fingerprint(const HepMC3::GenEvent& event) +{ + std::string out = "n=" + std::to_string(event.event_number()) + + " np=" + std::to_string(event.particles().size()) + + " nv=" + std::to_string(event.vertices().size()) + + " nw=" + std::to_string(event.weights().size()); + char buf[128]; + for (const auto& p : event.particles()) { + snprintf(buf, sizeof buf, " [%d,%d,%.9e,%.9e,%.9e,%.9e]", p->pid(), p->status(), + p->momentum().x(), p->momentum().y(), p->momentum().z(), p->momentum().t()); + out += buf; + } + return out; +} +/// the entry the generator says the event was taken from +int announcedEntry(const o2::dataformats::MCEventHeader& header) +{ + const std::string key = "forwarding-generator_inputEventNumber"; + if (!header.hasInfo(key)) { + return -1; + } + bool valid = false; + auto entry = header.getInfo(key, valid); + return valid ? entry : -1; +} + +/// the entry the served particles actually come from, read back from their momenta +int servedEntry(const std::vector& tracks) +{ + auto outgoing = std::find_if(tracks.begin(), tracks.end(), + [](const o2::MCTrack& t) { return t.GetPdgCode() == 211; }); + if (outgoing == tracks.end()) { + return -1; + } + return (int)std::lround(outgoing->Px() - expectedPx(0)); +} + +/// points the HepMC generator at a file and configures how it serves its events +void configure(const std::string& file, int eventsToSkip = 0) +{ + o2::conf::ConfigurableParam::updateFromString( + "GeneratorFileOrCmd.fileNames=" + file + + ";HepMC.randomize=true;HepMC.roundRobin=true;HepMC.reshuffleOnRepeat=false" + ";HepMC.rngseed=12345;HepMC.eventsToSkip=" + + std::to_string(eventsToSkip)); +} + +} // namespace + +namespace +{ +/// reads every entry of a file by seeking to a recorded offset and requires the result to +/// be what reading the file from start to end gives +void checkSeekEquivalence(const std::string& file, bool hepmc2) +{ + auto makeReader = [hepmc2](std::shared_ptr stream) -> std::shared_ptr { + if (hepmc2) { + return std::make_shared(stream); + } + return std::make_shared(stream); + }; + + // read the file from start to end, recording where every event begins + std::vector offsets; + std::vector sequential; + { + auto stream = std::make_shared(file); + BOOST_REQUIRE(stream->good()); + auto reader = makeReader(stream); + while (true) { + auto here = (std::streamoff)stream->tellg(); + HepMC3::GenEvent event; + reader->read_event(event); + if (reader->failed()) { + break; + } + offsets.push_back(here); + sequential.push_back(fingerprint(event)); + } + } + BOOST_REQUIRE_MESSAGE(!offsets.empty(), "no event indexed in " << file); + + // now read them by seeking, backwards, so that every jump goes against the stream + auto stream = std::make_shared(file); + BOOST_REQUIRE(stream->good()); + auto reader = makeReader(stream); + // the run-level header sits ahead of the first event and has to be parsed once + HepMC3::GenEvent header; + reader->read_event(header); + for (int entry = (int)offsets.size() - 1; entry >= 0; --entry) { + stream->clear(); + stream->seekg(offsets[entry]); + HepMC3::GenEvent event; + reader->read_event(event); + BOOST_REQUIRE_MESSAGE(!reader->failed(), + file << ": could not read entry " << entry << " by seeking"); + BOOST_CHECK_MESSAGE(fingerprint(event) == sequential[entry], + file << ": entry " << entry << " read by seeking differs from the " + << "sequential read; the HepMC3 reader can no longer be " + << "repositioned and GeneratorHepMC's indexed access is unsafe"); + BOOST_CHECK_MESSAGE(event.run_info() != nullptr, + file << ": entry " << entry << " lost its GenRunInfo"); + } +} +} // namespace + +/// The ASCII readers must survive having their stream repositioned between events: reading +/// the entries by seeking to a recorded offset has to give exactly what reading the file +/// from start to end gives. Checked for both formats the generator indexes. +BOOST_AUTO_TEST_CASE(hepmc3_reader_can_be_seeked) +{ + constexpr int nEvents = 20; + auto asciiv3 = writeInput("test_GeneratorHepMCIndexed_seek.hepmc", nEvents); + auto hepmc2 = writeInputHepMC2(asciiv3, "test_GeneratorHepMCIndexed_seek2.hepmc"); + + checkSeekEquivalence(asciiv3, false); + checkSeekEquivalence(hepmc2, true); + + std::remove(asciiv3.c_str()); + std::remove(hepmc2.c_str()); +} + +/// The generator must serve every event of the file exactly once per pass, start over in +/// roundRobin mode, and - the part that a wrong index would break silently - hand out the +/// event that actually sits at the entry it claims to be serving. Driving this through +/// GeneratorService also exercises the configurable-parameter path the generator is +/// configured by in a real job. +BOOST_AUTO_TEST_CASE(generator_serves_a_permutation) +{ + constexpr int nEvents = 25; + auto name = writeInput("test_GeneratorHepMCIndexed_gen.hepmc", nEvents); + configure(name); + + o2::eventgen::GeneratorService service; + service.initService("hepmc", "", o2::eventgen::NoVertexOption()); + + // two full passes over the file, plus a bit + std::vector served; + for (int i = 0; i < 2 * nEvents + 5; ++i) { + auto event = service.generateEvent(); + auto entry = servedEntry(event.first); + BOOST_REQUIRE_MESSAGE(entry >= 0 && entry < nEvents, + "event " << i << " belongs to no entry of the input"); + // the event handed out has to be the one the generator says it is serving; without + // this the index could be wrong by any amount and every other check would still pass + BOOST_CHECK_MESSAGE(announcedEntry(event.second) == entry, + "event " << i << ": the generator reports entry " + << announcedEntry(event.second) + << " but handed out the event stored at entry " << entry); + served.push_back(entry); + } + + // each pass uses every event of the file exactly once ... + std::vector all(nEvents); + std::iota(all.begin(), all.end(), 0); + std::vector pass1(served.begin(), served.begin() + nEvents); + std::vector pass2(served.begin() + nEvents, served.begin() + 2 * nEvents); + std::vector sorted1 = pass1; + std::vector sorted2 = pass2; + std::sort(sorted1.begin(), sorted1.end()); + std::sort(sorted2.begin(), sorted2.end()); + BOOST_CHECK(sorted1 == all); + BOOST_CHECK(sorted2 == all); + // ... the events are not simply served in file order ... + BOOST_CHECK(pass1 != all); + // ... with reshuffleOnRepeat off every pass repeats the first one ... + BOOST_CHECK(pass2 == pass1); + // ... and roundRobin keeps going past the end of the file + BOOST_CHECK(std::equal(served.begin() + 2 * nEvents, served.end(), pass1.begin())); + + std::remove(name.c_str()); +} + +/// eventsToSkip has to leave the skipped entries out of the game entirely. This also pins +/// the index down to an absolute position in the file: an index off by any amount would +/// serve an entry from outside the requested range. +BOOST_AUTO_TEST_CASE(generator_honours_events_to_skip) +{ + constexpr int nEvents = 25; + constexpr int toSkip = 18; + auto name = writeInput("test_GeneratorHepMCIndexed_skip.hepmc", nEvents); + configure(name, toSkip); + + o2::eventgen::GeneratorService service; + service.initService("hepmc", "", o2::eventgen::NoVertexOption()); + + std::vector served; + for (int i = 0; i < 2 * (nEvents - toSkip); ++i) { + auto event = service.generateEvent(); + auto entry = servedEntry(event.first); + BOOST_CHECK_MESSAGE(entry >= toSkip && entry < nEvents, + "event " << i << " came from entry " << entry + << ", outside the requested range [" << toSkip << ", " + << nEvents << ")"); + BOOST_CHECK_MESSAGE(announcedEntry(event.second) == entry, + "event " << i << ": the generator reports entry " + << announcedEntry(event.second) + << " but handed out the event stored at entry " << entry); + served.push_back(entry); + } + + std::vector usable(nEvents - toSkip); + std::iota(usable.begin(), usable.end(), toSkip); + std::vector pass1(served.begin(), served.begin() + (nEvents - toSkip)); + auto sorted = pass1; + std::sort(sorted.begin(), sorted.end()); + BOOST_CHECK(sorted == usable); + + std::remove(name.c_str()); +} + +/// The same must hold when the input is in the HepMC2 IO_GenEvent format, which the +/// generator indexes with ReaderAsciiHepMC2 instead of ReaderAscii. +BOOST_AUTO_TEST_CASE(generator_reads_hepmc2) +{ + constexpr int nEvents = 25; + auto asciiv3 = writeInput("test_GeneratorHepMCIndexed_h2src.hepmc", nEvents); + auto name = writeInputHepMC2(asciiv3, "test_GeneratorHepMCIndexed_h2.hepmc"); + std::remove(asciiv3.c_str()); + configure(name); + + o2::eventgen::GeneratorService service; + service.initService("hepmc", "", o2::eventgen::NoVertexOption()); + + std::vector served; + for (int i = 0; i < nEvents; ++i) { + auto event = service.generateEvent(); + auto entry = servedEntry(event.first); + BOOST_REQUIRE_MESSAGE(entry >= 0 && entry < nEvents, + "event " << i << " belongs to no entry of the HepMC2 input"); + BOOST_CHECK_MESSAGE(announcedEntry(event.second) == entry, + "event " << i << ": the generator reports entry " + << announcedEntry(event.second) + << " but handed out the event stored at entry " << entry); + served.push_back(entry); + } + std::vector all(nEvents); + std::iota(all.begin(), all.end(), 0); + auto sorted = served; + std::sort(sorted.begin(), sorted.end()); + BOOST_CHECK(sorted == all); + BOOST_CHECK(served != all); + + std::remove(name.c_str()); +}