From 3de40145e37f2f3042f036d7f9a4e65c5b3e2e92 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 12 Aug 2025 20:31:26 +0200 Subject: [PATCH 01/14] feat: Write global cluster position in `RootMeasurementWriter` --- .../Io/Root/src/RootMeasurementWriter.cpp | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/Examples/Io/Root/src/RootMeasurementWriter.cpp b/Examples/Io/Root/src/RootMeasurementWriter.cpp index 2b79258cbef..c88877dc02b 100644 --- a/Examples/Io/Root/src/RootMeasurementWriter.cpp +++ b/Examples/Io/Root/src/RootMeasurementWriter.cpp @@ -9,6 +9,7 @@ #include "ActsExamples/Io/Root/RootMeasurementWriter.hpp" #include "Acts/Definitions/TrackParametrization.hpp" +#include "Acts/Geometry/GeometryContext.hpp" #include "ActsExamples/EventData/AverageSimHits.hpp" #include "ActsExamples/EventData/Index.hpp" #include "ActsExamples/EventData/Measurement.hpp" @@ -42,6 +43,9 @@ struct RootMeasurementWriter::DigitizationTree { // Reconstruction information float recBound[Acts::eBoundSize] = {}; float varBound[Acts::eBoundSize] = {}; + float recGx = 0.; + float recGy = 0.; + float recGz = 0.; // Truth parameters float trueBound[Acts::eBoundSize] = {}; @@ -84,6 +88,10 @@ struct RootMeasurementWriter::DigitizationTree { tree->Branch(("var_" + bNames[ib]).c_str(), &varBound[ib]); } + tree->Branch("rec_x", &recGx); + tree->Branch("rec_y", &recGy); + tree->Branch("rec_z", &recGz); + tree->Branch("clus_size", &nch); tree->Branch("channel_value", &chValue); // Both are allocated, but only relevant ones are set @@ -164,6 +172,17 @@ struct RootMeasurementWriter::DigitizationTree { } } + void fillGlobalMeasurement(const Acts::GeometryContext& gctx, + const Acts::Surface& surface, + const ConstVariableBoundMeasurementProxy& m) { + // passing invalid direction but we expect this to be a regular surface + Acts::Vector3 global = surface.localToGlobal( + gctx, m.fullParameters().head<2>(), Acts::Vector3::Zero()); + recGx = global[Acts::ePos0]; + recGy = global[Acts::ePos1]; + recGz = global[Acts::ePos2]; + } + /// Convenience function to fill the cluster information /// /// @param c The cluster @@ -190,6 +209,9 @@ struct RootMeasurementWriter::DigitizationTree { residual[ib] = std::numeric_limits::quiet_NaN(); pull[ib] = std::numeric_limits::quiet_NaN(); } + recGx = std::numeric_limits::quiet_NaN(); + recGy = std::numeric_limits::quiet_NaN(); + recGz = std::numeric_limits::quiet_NaN(); trueGx = std::numeric_limits::quiet_NaN(); trueGy = std::numeric_limits::quiet_NaN(); trueGz = std::numeric_limits::quiet_NaN(); @@ -280,6 +302,14 @@ ProcessCode RootMeasurementWriter::writeT( // Fill the identification m_outputTree->fillIdentification(ctx.eventNumber, geoId); + // Fill reco information + m_outputTree->fillBoundMeasurement(meas); + if (clusters != nullptr) { + const auto& c = (*clusters)[hitIdx]; + m_outputTree->fillCluster(c); + } + m_outputTree->fillGlobalMeasurement(ctx.geoContext, surface, meas); + // Find the contributing simulated hits auto indices = makeRange(hitSimHitsMap.equal_range(hitIdx)); // Use average truth in the case of multiple contributing sim hits @@ -293,11 +323,6 @@ ProcessCode RootMeasurementWriter::writeT( Acts::VectorHelpers::incidentAngles(dir, rot); m_outputTree->fillTruthParameters(local, pos4, dir, angles); - m_outputTree->fillBoundMeasurement(meas); - if (clusters != nullptr) { - const auto& c = (*clusters)[hitIdx]; - m_outputTree->fillCluster(c); - } m_outputTree->fill(); m_outputTree->clear(); From e03d1ae59edc02d01ec55c2cc002262051284202 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Wed, 13 Aug 2025 08:56:32 +0200 Subject: [PATCH 02/14] minor --- Examples/Io/Root/src/RootMeasurementWriter.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/Io/Root/src/RootMeasurementWriter.cpp b/Examples/Io/Root/src/RootMeasurementWriter.cpp index c88877dc02b..a286175e11a 100644 --- a/Examples/Io/Root/src/RootMeasurementWriter.cpp +++ b/Examples/Io/Root/src/RootMeasurementWriter.cpp @@ -87,7 +87,6 @@ struct RootMeasurementWriter::DigitizationTree { for (auto ib : recoIndices) { tree->Branch(("var_" + bNames[ib]).c_str(), &varBound[ib]); } - tree->Branch("rec_x", &recGx); tree->Branch("rec_y", &recGy); tree->Branch("rec_z", &recGz); From 565f46d649e2e69142b4cd292c6c88428990779d Mon Sep 17 00:00:00 2001 From: Daniel Murnane Date: Mon, 18 Aug 2025 02:10:22 -0700 Subject: [PATCH 03/14] Switch hit counter to 16 bit --- Examples/Io/EDM4hep/src/EDM4hepSimInputConverter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Io/EDM4hep/src/EDM4hepSimInputConverter.cpp b/Examples/Io/EDM4hep/src/EDM4hepSimInputConverter.cpp index acc7d81ed09..341d2bcb9ce 100644 --- a/Examples/Io/EDM4hep/src/EDM4hepSimInputConverter.cpp +++ b/Examples/Io/EDM4hep/src/EDM4hepSimInputConverter.cpp @@ -252,7 +252,7 @@ ProcessCode EDM4hepSimInputConverter::convert(const AlgorithmContext& ctx, // container std::unordered_map edm4hepParticleMap; - std::vector numSimHits; + std::vector numSimHits; numSimHits.resize(mcParticleCollection.size()); std::size_t nGeneratorParticles = 0; @@ -285,7 +285,7 @@ ProcessCode EDM4hepSimInputConverter::convert(const AlgorithmContext& ctx, } } - std::function getNumHits = + std::function getNumHits = [&numSimHits](const edm4hep::MCParticle& p) { return numSimHits.at(p.getObjectID().index); }; From 0b7f7b39c466f7189e25d825c3846bbb3ce5c719 Mon Sep 17 00:00:00 2001 From: Daniel Murnane Date: Mon, 29 Sep 2025 05:05:25 -0700 Subject: [PATCH 04/14] Remove measurement writer changes --- Examples/Io/Root/src/RootMeasurementWriter.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Examples/Io/Root/src/RootMeasurementWriter.cpp b/Examples/Io/Root/src/RootMeasurementWriter.cpp index 9db7c0029b1..0b6b4848377 100644 --- a/Examples/Io/Root/src/RootMeasurementWriter.cpp +++ b/Examples/Io/Root/src/RootMeasurementWriter.cpp @@ -140,14 +140,6 @@ ProcessCode RootMeasurementWriter::writeT( m_measurementIo->fillIdentification(static_cast(ctx.eventNumber), geoId); - // Fill reco information - m_outputTree->fillBoundMeasurement(meas); - if (clusters != nullptr) { - const auto& c = (*clusters)[hitIdx]; - m_outputTree->fillCluster(c); - } - m_outputTree->fillGlobalMeasurement(ctx.geoContext, surface, meas); - // Find the contributing simulated hits auto indices = makeRange(hitSimHitsMap.equal_range(hitIdx)); // Use average truth in the case of multiple contributing sim hits @@ -183,4 +175,4 @@ ProcessCode RootMeasurementWriter::writeT( return ProcessCode::SUCCESS; } -} // namespace ActsExamples +} // namespace ActsExamples \ No newline at end of file From 2db5935987a145322f6cfaf8e2a60fa28dd90dc5 Mon Sep 17 00:00:00 2001 From: Daniel Murnane Date: Mon, 29 Sep 2025 05:06:38 -0700 Subject: [PATCH 05/14] Empty final line --- Examples/Io/Root/src/RootMeasurementWriter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Io/Root/src/RootMeasurementWriter.cpp b/Examples/Io/Root/src/RootMeasurementWriter.cpp index 0b6b4848377..2bc62a66984 100644 --- a/Examples/Io/Root/src/RootMeasurementWriter.cpp +++ b/Examples/Io/Root/src/RootMeasurementWriter.cpp @@ -175,4 +175,4 @@ ProcessCode RootMeasurementWriter::writeT( return ProcessCode::SUCCESS; } -} // namespace ActsExamples \ No newline at end of file +} // namespace ActsExamples From d974f9f5278239aa70b7aeadcd6271f498ebebc5 Mon Sep 17 00:00:00 2001 From: Daniel Murnane Date: Tue, 30 Sep 2025 04:37:54 -0700 Subject: [PATCH 06/14] Allow random sampler to be passed to HepMC3Reader --- .../ActsExamples/Io/HepMC3/HepMC3Reader.hpp | 11 ++++++ Examples/Io/HepMC3/src/HepMC3Reader.cpp | 36 +++++++++++++++++-- Examples/Python/src/HepMC3.cpp | 3 +- thirdparty/OpenDataDetector | 1 - 4 files changed, 46 insertions(+), 5 deletions(-) delete mode 160000 thirdparty/OpenDataDetector diff --git a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp index 9d1f28f347c..a17ab21b35b 100644 --- a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp +++ b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp @@ -12,6 +12,7 @@ #include "ActsExamples/Framework/DataHandle.hpp" #include "ActsExamples/Framework/IReader.hpp" #include "ActsExamples/Framework/RandomNumbers.hpp" +#include "ActsExamples/Utilities/MultiplicityGenerators.hpp" #include "ActsExamples/Utilities/VertexGenerators.hpp" #include @@ -66,6 +67,16 @@ class HepMC3Reader final : public IReader { std::shared_ptr randomNumbers; /// Position generator that will be used to shift read events std::shared_ptr vertexGenerator; + + /// Optional multiplicity generator for one of the inputs (e.g. pileup). + /// If set, the number of events to read for the input at + /// `multiplicityInputIndex` will be sampled per logical event using this + /// generator. Other inputs continue to use their fixed multiplicity from + /// `inputPaths`. + std::shared_ptr multiplicityGenerator; + /// The index into `inputPaths` to which `multiplicityGenerator` applies. + /// If unset, behavior defaults to no multiplicity sampling. + std::optional multiplicityInputIndex = std::nullopt; }; /// Construct the particle reader. diff --git a/Examples/Io/HepMC3/src/HepMC3Reader.cpp b/Examples/Io/HepMC3/src/HepMC3Reader.cpp index e5b00d1964b..32338212bc8 100644 --- a/Examples/Io/HepMC3/src/HepMC3Reader.cpp +++ b/Examples/Io/HepMC3/src/HepMC3Reader.cpp @@ -75,6 +75,21 @@ HepMC3Reader::HepMC3Reader(const HepMC3Reader::Config& cfg, "randomNumbers must be set if vertexGenerator is set"); } + if (m_cfg.multiplicityGenerator != nullptr) { + if (!m_cfg.multiplicityInputIndex.has_value()) { + throw std::invalid_argument( + "multiplicityInputIndex must be set if multiplicityGenerator is set"); + } + if (*m_cfg.multiplicityInputIndex >= m_inputs.size()) { + throw std::invalid_argument( + "multiplicityInputIndex is out of bounds for inputPaths"); + } + if (m_cfg.randomNumbers == nullptr) { + throw std::invalid_argument( + "randomNumbers must be set if multiplicityGenerator is set"); + } + } + ACTS_DEBUG("HepMC3Reader: " << m_eventsRange.first << " - " << m_eventsRange.second << " events"); } @@ -335,9 +350,24 @@ ProcessCode HepMC3Reader::readLogicalEvent( // @TODO: Add the index as an attribute to the event and it's content - for (const auto& [reader, numEvents, path] : m_inputs) { - ACTS_VERBOSE("Reading " << numEvents << " events from " << path); - for (std::size_t i = 0; i < numEvents; ++i) { + auto rng = (m_cfg.multiplicityGenerator != nullptr) + ? std::optional(m_cfg.randomNumbers->spawnGenerator(ctx)) + : std::nullopt; + + for (std::size_t inputIndex = 0; inputIndex < m_inputs.size(); ++inputIndex) { + auto& reader = m_inputs[inputIndex].reader; + auto& path = m_inputs[inputIndex].path; + std::size_t fixedCount = m_inputs[inputIndex].numEvents; + + std::size_t count = fixedCount; + if (m_cfg.multiplicityGenerator != nullptr && + m_cfg.multiplicityInputIndex.has_value() && + *m_cfg.multiplicityInputIndex == inputIndex) { + count = (*m_cfg.multiplicityGenerator)(*rng); + } + + ACTS_VERBOSE("Reading " << count << " events from " << path); + for (std::size_t i = 0; i < count; ++i) { auto event = makeEvent(); reader->read_event(*event); diff --git a/Examples/Python/src/HepMC3.cpp b/Examples/Python/src/HepMC3.cpp index 0d47dffe99d..633a80fdfd4 100644 --- a/Examples/Python/src/HepMC3.cpp +++ b/Examples/Python/src/HepMC3.cpp @@ -37,7 +37,8 @@ void addHepMC3(Context& ctx) { ACTS_PYTHON_DECLARE_READER(HepMC3Reader, hepmc3, "HepMC3Reader", inputPath, inputPaths, outputEvent, printListing, numEvents, checkEventNumber, maxEventBufferSize, - vertexGenerator, randomNumbers); + vertexGenerator, randomNumbers, + multiplicityGenerator, multiplicityInputIndex); ACTS_PYTHON_DECLARE_ALGORITHM(HepMC3OutputConverter, hepmc3, "HepMC3OutputConverter", inputParticles, diff --git a/thirdparty/OpenDataDetector b/thirdparty/OpenDataDetector deleted file mode 160000 index 4c5e7413a50..00000000000 --- a/thirdparty/OpenDataDetector +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4c5e7413a505a7743cbec9544901e8c55172e513 From ad990d3692da0a638468ddaad3cafbd33794fe03 Mon Sep 17 00:00:00 2001 From: Daniel Murnane Date: Tue, 30 Sep 2025 04:42:32 -0700 Subject: [PATCH 07/14] Restore OpenDataDetector submodule --- thirdparty/OpenDataDetector | 1 + 1 file changed, 1 insertion(+) create mode 160000 thirdparty/OpenDataDetector diff --git a/thirdparty/OpenDataDetector b/thirdparty/OpenDataDetector new file mode 160000 index 00000000000..4c5e7413a50 --- /dev/null +++ b/thirdparty/OpenDataDetector @@ -0,0 +1 @@ +Subproject commit 4c5e7413a505a7743cbec9544901e8c55172e513 From 4ce1f3527a9f2d69dfaa1d7a82e015abaf07e7a7 Mon Sep 17 00:00:00 2001 From: Daniel Murnane Date: Tue, 30 Sep 2025 08:59:00 -0700 Subject: [PATCH 08/14] Make inputs a config struct, remove dedicated generator inputs --- .../ActsExamples/Io/HepMC3/HepMC3Reader.hpp | 37 +++++----- Examples/Io/HepMC3/src/HepMC3Reader.cpp | 67 +++++++++---------- Examples/Python/src/HepMC3.cpp | 16 +++-- 3 files changed, 58 insertions(+), 62 deletions(-) diff --git a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp index a17ab21b35b..26812662784 100644 --- a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp +++ b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp @@ -12,7 +12,6 @@ #include "ActsExamples/Framework/DataHandle.hpp" #include "ActsExamples/Framework/IReader.hpp" #include "ActsExamples/Framework/RandomNumbers.hpp" -#include "ActsExamples/Utilities/MultiplicityGenerators.hpp" #include "ActsExamples/Utilities/VertexGenerators.hpp" #include @@ -30,16 +29,20 @@ namespace ActsExamples { class HepMC3Reader final : public IReader { public: struct Config { - /// The input file path for reading HepMC3 events. - /// This is a helper to simplify the most basic configuration, which is - /// reading single events from a single input file - std::filesystem::path inputPath; - - /// This configuration option is used to read multiple files in a single - /// run. For each file, a specific number of events is read per requested - /// event. This can be used to read e.g. hard-scatter events from one file - /// and pileup events from another. - std::vector> inputPaths; + /// Input specification per file + struct Input { + /// Path to the HepMC3 file + std::filesystem::path path; + /// Fixed number of events to read per logical event (used if multiplicityGenerator is null) + std::size_t numEvents = 1; + /// Optional multiplicity generator for variable event sampling + std::shared_ptr multiplicityGenerator = nullptr; + }; + + /// Input files to read. For each file, a specific number of events is + /// read per logical event. This can be used to read e.g. hard-scatter + /// events from one file and pileup events from another. + std::vector inputs; /// The output collection std::string outputEvent; @@ -63,20 +66,10 @@ class HepMC3Reader final : public IReader { /// used. If this number is exceeded the reader will error out. std::size_t maxEventBufferSize = 128; - /// The random number service. Required if vertexGenerator is set. + /// The random number service. Required if vertexGenerator or any multiplicityGenerator is set. std::shared_ptr randomNumbers; /// Position generator that will be used to shift read events std::shared_ptr vertexGenerator; - - /// Optional multiplicity generator for one of the inputs (e.g. pileup). - /// If set, the number of events to read for the input at - /// `multiplicityInputIndex` will be sampled per logical event using this - /// generator. Other inputs continue to use their fixed multiplicity from - /// `inputPaths`. - std::shared_ptr multiplicityGenerator; - /// The index into `inputPaths` to which `multiplicityGenerator` applies. - /// If unset, behavior defaults to no multiplicity sampling. - std::optional multiplicityInputIndex = std::nullopt; }; /// Construct the particle reader. diff --git a/Examples/Io/HepMC3/src/HepMC3Reader.cpp b/Examples/Io/HepMC3/src/HepMC3Reader.cpp index 32338212bc8..e65647967d9 100644 --- a/Examples/Io/HepMC3/src/HepMC3Reader.cpp +++ b/Examples/Io/HepMC3/src/HepMC3Reader.cpp @@ -15,6 +15,7 @@ #include "Acts/Utilities/ThrowAssert.hpp" #include "ActsExamples/Framework/ProcessCode.hpp" #include "ActsExamples/Io/HepMC3/HepMC3Util.hpp" +#include "ActsExamples/Utilities/MultiplicityGenerators.hpp" #include #include @@ -39,23 +40,14 @@ HepMC3Reader::HepMC3Reader(const HepMC3Reader::Config& cfg, m_outputEvent.initialize(m_cfg.outputEvent); - if (!m_cfg.inputPaths.empty() && !m_cfg.inputPath.empty()) { - throw std::invalid_argument( - "inputPath and inputPaths are mutually exclusive"); - } - - if (!m_cfg.inputPath.empty()) { - m_cfg.inputPaths.emplace_back(m_cfg.inputPath, 1); - } - - if (m_cfg.inputPaths.empty()) { + if (m_cfg.inputs.empty()) { throw std::invalid_argument( "HepMC3 reader was not configured with any input files"); } - for (const auto& [path, numEvents] : m_cfg.inputPaths) { - auto reader = HepMC3::deduce_reader(path); - m_inputs.emplace_back(reader, numEvents, path); + for (const auto& input : m_cfg.inputs) { + auto reader = HepMC3::deduce_reader(input.path); + m_inputs.emplace_back(reader, input.numEvents, input.path); } if (m_cfg.numEvents.has_value()) { @@ -70,24 +62,20 @@ HepMC3Reader::HepMC3Reader(const HepMC3Reader::Config& cfg, m_eventsRange = {0, determineNumEvents(*reader)}; } - if (m_cfg.vertexGenerator != nullptr && m_cfg.randomNumbers == nullptr) { - throw std::invalid_argument( - "randomNumbers must be set if vertexGenerator is set"); + // Check if randomNumbers is required + bool needsRng = (m_cfg.vertexGenerator != nullptr); + if (!needsRng && !m_cfg.inputs.empty()) { + for (const auto& input : m_cfg.inputs) { + if (input.multiplicityGenerator != nullptr) { + needsRng = true; + break; + } + } } - if (m_cfg.multiplicityGenerator != nullptr) { - if (!m_cfg.multiplicityInputIndex.has_value()) { - throw std::invalid_argument( - "multiplicityInputIndex must be set if multiplicityGenerator is set"); - } - if (*m_cfg.multiplicityInputIndex >= m_inputs.size()) { - throw std::invalid_argument( - "multiplicityInputIndex is out of bounds for inputPaths"); - } - if (m_cfg.randomNumbers == nullptr) { - throw std::invalid_argument( - "randomNumbers must be set if multiplicityGenerator is set"); - } + if (needsRng && m_cfg.randomNumbers == nullptr) { + throw std::invalid_argument( + "randomNumbers must be set if vertexGenerator or any multiplicityGenerator is set"); } ACTS_DEBUG("HepMC3Reader: " << m_eventsRange.first << " - " @@ -350,20 +338,27 @@ ProcessCode HepMC3Reader::readLogicalEvent( // @TODO: Add the index as an attribute to the event and it's content - auto rng = (m_cfg.multiplicityGenerator != nullptr) - ? std::optional(m_cfg.randomNumbers->spawnGenerator(ctx)) - : std::nullopt; + // Spawn RNG if needed by any multiplicityGenerator + std::optional rng; + if (!m_cfg.inputs.empty()) { + for (const auto& input : m_cfg.inputs) { + if (input.multiplicityGenerator != nullptr) { + rng = m_cfg.randomNumbers->spawnGenerator(ctx); + break; + } + } + } for (std::size_t inputIndex = 0; inputIndex < m_inputs.size(); ++inputIndex) { auto& reader = m_inputs[inputIndex].reader; auto& path = m_inputs[inputIndex].path; std::size_t fixedCount = m_inputs[inputIndex].numEvents; + // Use generator if present in new inputs config, otherwise fixed count std::size_t count = fixedCount; - if (m_cfg.multiplicityGenerator != nullptr && - m_cfg.multiplicityInputIndex.has_value() && - *m_cfg.multiplicityInputIndex == inputIndex) { - count = (*m_cfg.multiplicityGenerator)(*rng); + if (!m_cfg.inputs.empty() && inputIndex < m_cfg.inputs.size() && + m_cfg.inputs[inputIndex].multiplicityGenerator != nullptr) { + count = (*m_cfg.inputs[inputIndex].multiplicityGenerator)(*rng); } ACTS_VERBOSE("Reading " << count << " events from " << path); diff --git a/Examples/Python/src/HepMC3.cpp b/Examples/Python/src/HepMC3.cpp index 633a80fdfd4..cc6031693b9 100644 --- a/Examples/Python/src/HepMC3.cpp +++ b/Examples/Python/src/HepMC3.cpp @@ -11,6 +11,7 @@ #include "ActsExamples/Io/HepMC3/HepMC3Reader.hpp" #include "ActsExamples/Io/HepMC3/HepMC3Util.hpp" #include "ActsExamples/Io/HepMC3/HepMC3Writer.hpp" +#include "ActsExamples/Utilities/MultiplicityGenerators.hpp" #include "ActsPython/Utilities/Helpers.hpp" #include "ActsPython/Utilities/Macros.hpp" @@ -34,11 +35,18 @@ void addHepMC3(Context& ctx) { perEvent, inputEvent, compression, maxEventsPending, writeEventsInOrder); - ACTS_PYTHON_DECLARE_READER(HepMC3Reader, hepmc3, "HepMC3Reader", inputPath, - inputPaths, outputEvent, printListing, numEvents, + // Expose nested Input struct + auto input = py::class_(hepmc3, "Input") + .def(py::init<>()) + .def_readwrite("path", &HepMC3Reader::Config::Input::path) + .def_readwrite("numEvents", &HepMC3Reader::Config::Input::numEvents) + .def_readwrite("multiplicityGenerator", + &HepMC3Reader::Config::Input::multiplicityGenerator); + + ACTS_PYTHON_DECLARE_READER(HepMC3Reader, hepmc3, "HepMC3Reader", inputs, + outputEvent, printListing, numEvents, checkEventNumber, maxEventBufferSize, - vertexGenerator, randomNumbers, - multiplicityGenerator, multiplicityInputIndex); + vertexGenerator, randomNumbers); ACTS_PYTHON_DECLARE_ALGORITHM(HepMC3OutputConverter, hepmc3, "HepMC3OutputConverter", inputParticles, From 9259b262c210ab96c6afd6151061aa7ddaab8268 Mon Sep 17 00:00:00 2001 From: Daniel Murnane Date: Wed, 1 Oct 2025 13:14:12 -0700 Subject: [PATCH 09/14] Refactor HepMC3Reader to use Input struct with per-input multiplicity generators - Replace inputPath/inputPaths with Config::Input struct - Add multiplicityGenerator field to Input for per-input sampling - Forward declare MultiplicityGenerator to avoid header dependency - Add proper Python bindings with keyword argument constructor - Simplify validation logic and remove backward compatibility --- .../include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp | 5 ++++- Examples/Python/src/HepMC3.cpp | 13 ++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp index 26812662784..4715b865c03 100644 --- a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp +++ b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp @@ -15,6 +15,7 @@ #include "ActsExamples/Utilities/VertexGenerators.hpp" #include +#include #include #include @@ -25,6 +26,8 @@ class Reader; namespace ActsExamples { +struct MultiplicityGenerator; + /// HepMC3 event reader. class HepMC3Reader final : public IReader { public: @@ -36,7 +39,7 @@ class HepMC3Reader final : public IReader { /// Fixed number of events to read per logical event (used if multiplicityGenerator is null) std::size_t numEvents = 1; /// Optional multiplicity generator for variable event sampling - std::shared_ptr multiplicityGenerator = nullptr; + std::shared_ptr multiplicityGenerator = nullptr; }; /// Input files to read. For each file, a specific number of events is diff --git a/Examples/Python/src/HepMC3.cpp b/Examples/Python/src/HepMC3.cpp index cc6031693b9..008258450e0 100644 --- a/Examples/Python/src/HepMC3.cpp +++ b/Examples/Python/src/HepMC3.cpp @@ -36,8 +36,19 @@ void addHepMC3(Context& ctx) { maxEventsPending, writeEventsInOrder); // Expose nested Input struct - auto input = py::class_(hepmc3, "Input") + py::class_(hepmc3, "Input") .def(py::init<>()) + .def(py::init([](const std::filesystem::path& path, std::size_t numEvents, + std::shared_ptr multiplicityGenerator) { + HepMC3Reader::Config::Input inp; + inp.path = path; + inp.numEvents = numEvents; + inp.multiplicityGenerator = multiplicityGenerator; + return inp; + }), + py::arg("path"), + py::arg("numEvents") = 1, + py::arg("multiplicityGenerator") = nullptr) .def_readwrite("path", &HepMC3Reader::Config::Input::path) .def_readwrite("numEvents", &HepMC3Reader::Config::Input::numEvents) .def_readwrite("multiplicityGenerator", From f0163e904c4179d44c0f8b65fbb70e129c482958 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Mon, 13 Oct 2025 19:23:12 +0200 Subject: [PATCH 10/14] unify interface --- .../ActsExamples/Io/HepMC3/HepMC3Reader.hpp | 82 ++++++++++-- Examples/Io/HepMC3/src/HepMC3Reader.cpp | 124 ++++++++++++------ Examples/Python/src/HepMC3.cpp | 30 ++++- Examples/Python/tests/test_hepmc3.py | 5 +- Examples/Scripts/Python/hepmc3_merge.py | 6 +- 5 files changed, 189 insertions(+), 58 deletions(-) diff --git a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp index 4715b865c03..f1f7f76e782 100644 --- a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp +++ b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp @@ -29,6 +29,56 @@ namespace ActsExamples { struct MultiplicityGenerator; /// HepMC3 event reader. +/// +/// This reader supports reading events from one or more HepMC3 files, with +/// flexible control over how many physical events are merged into each logical +/// event. +/// +/// ## Multiplicity Generators +/// +/// Each input file must have an associated multiplicity generator that determines +/// how many physical events to read from that file for each logical event: +/// - FixedMultiplicityGenerator: Always reads a fixed number of events (deterministic) +/// - PoissonMultiplicityGenerator: Reads a Poisson-distributed number of events (stochastic) +/// +/// ## Usage Patterns +/// +/// 1. Single file with default multiplicity: +/// ``` +/// HepMC3Reader reader({.inputPath = "events.hepmc3", ...}); +/// ``` +/// Automatically uses FixedMultiplicityGenerator(n=1). +/// +/// 2. Multiple files with fixed multiplicities (e.g., hard-scatter + pileup): +/// ``` +/// HepMC3Reader reader({ +/// .inputs = { +/// Input{path="signal.hepmc3", multiplicityGenerator=make_shared(1)}, +/// Input{path="pileup.hepmc3", multiplicityGenerator=make_shared(50)} +/// }, +/// ... +/// }); +/// ``` +/// +/// 3. With stochastic pileup multiplicity: +/// ``` +/// HepMC3Reader reader({ +/// .inputs = { +/// Input{path="signal.hepmc3", multiplicityGenerator=make_shared(1)}, +/// Input{path="pileup.hepmc3", multiplicityGenerator=make_shared(50)} +/// }, +/// .randomNumbers = rng, // Required for stochastic generators +/// ... +/// }); +/// ``` +/// +/// ## Event Skipping +/// +/// Event skipping (via Sequencer's skip parameter) is only supported when ALL +/// multiplicity generators are Fixed. This is because skipping requires knowing +/// exactly how many physical events to skip, which is only deterministic with +/// FixedMultiplicityGenerator. +/// class HepMC3Reader final : public IReader { public: struct Config { @@ -36,17 +86,22 @@ class HepMC3Reader final : public IReader { struct Input { /// Path to the HepMC3 file std::filesystem::path path; - /// Fixed number of events to read per logical event (used if multiplicityGenerator is null) - std::size_t numEvents = 1; - /// Optional multiplicity generator for variable event sampling - std::shared_ptr multiplicityGenerator = nullptr; + /// Multiplicity generator determining how many events to read per logical event. + /// Must always be set. Use FixedMultiplicityGenerator for deterministic behavior, + /// or PoissonMultiplicityGenerator for stochastic pileup simulation. + std::shared_ptr multiplicityGenerator; }; - /// Input files to read. For each file, a specific number of events is - /// read per logical event. This can be used to read e.g. hard-scatter - /// events from one file and pileup events from another. + /// Input files to read. For each file, the multiplicity generator determines + /// how many events are read per logical event. This can be used to read e.g. + /// hard-scatter events from one file and pileup events from another. + /// Mutually exclusive with inputPath. std::vector inputs; + /// Convenience parameter for single-file reading. Mutually exclusive with inputs. + /// If set, creates a single input with FixedMultiplicityGenerator(n=1). + std::optional inputPath; + /// The output collection std::string outputEvent; @@ -69,7 +124,10 @@ class HepMC3Reader final : public IReader { /// used. If this number is exceeded the reader will error out. std::size_t maxEventBufferSize = 128; - /// The random number service. Required if vertexGenerator or any multiplicityGenerator is set. + /// The random number service. Required if: + /// - vertexGenerator is set, OR + /// - any non-Fixed multiplicityGenerator is used (e.g., PoissonMultiplicityGenerator) + /// Not required when using only FixedMultiplicityGenerator with no vertexGenerator. std::shared_ptr randomNumbers; /// Position generator that will be used to shift read events std::shared_ptr vertexGenerator; @@ -141,10 +199,16 @@ class HepMC3Reader final : public IReader { std::mutex m_queueMutex; + /// Precomputed flag: true if RNG is needed (vertex generator or non-Fixed multiplicity) + bool m_needsRng = false; + + /// Precomputed flag: true if any input uses a non-Fixed multiplicity generator + bool m_hasNonFixedMultiplicity = false; + struct InputConfig { std::shared_ptr reader; - std::size_t numEvents; std::filesystem::path path; + std::shared_ptr multiplicityGenerator; }; std::vector m_inputs; diff --git a/Examples/Io/HepMC3/src/HepMC3Reader.cpp b/Examples/Io/HepMC3/src/HepMC3Reader.cpp index e65647967d9..fb50e4b0c11 100644 --- a/Examples/Io/HepMC3/src/HepMC3Reader.cpp +++ b/Examples/Io/HepMC3/src/HepMC3Reader.cpp @@ -40,14 +40,41 @@ HepMC3Reader::HepMC3Reader(const HepMC3Reader::Config& cfg, m_outputEvent.initialize(m_cfg.outputEvent); - if (m_cfg.inputs.empty()) { + // Validate: exactly one of inputPath or inputs must be set + bool hasInputPath = m_cfg.inputPath.has_value(); + bool hasInputs = !m_cfg.inputs.empty(); + + if (!hasInputPath && !hasInputs) { + throw std::invalid_argument( + "HepMC3 reader requires either 'inputPath' or 'inputs' to be set"); + } + + if (hasInputPath && hasInputs) { throw std::invalid_argument( - "HepMC3 reader was not configured with any input files"); + "HepMC3 reader: 'inputPath' and 'inputs' are mutually exclusive. " + "Use 'inputPath' for single file or 'inputs' for multiple files."); } - for (const auto& input : m_cfg.inputs) { + // If inputPath is set, create a single input with default multiplicity + // generator + if (hasInputPath) { + Config::Input input; + input.path = m_cfg.inputPath.value(); + input.multiplicityGenerator = + std::make_shared(1); + auto reader = HepMC3::deduce_reader(input.path); - m_inputs.emplace_back(reader, input.numEvents, input.path); + m_inputs.emplace_back(reader, input.path, input.multiplicityGenerator); + } else { + // Use the provided inputs + for (const auto& input : m_cfg.inputs) { + if (!input.multiplicityGenerator) { + throw std::invalid_argument( + "All Input objects must have a multiplicityGenerator set"); + } + auto reader = HepMC3::deduce_reader(input.path); + m_inputs.emplace_back(reader, input.path, input.multiplicityGenerator); + } } if (m_cfg.numEvents.has_value()) { @@ -62,20 +89,23 @@ HepMC3Reader::HepMC3Reader(const HepMC3Reader::Config& cfg, m_eventsRange = {0, determineNumEvents(*reader)}; } + // Check if any input uses a non-Fixed multiplicity generator + m_hasNonFixedMultiplicity = std::ranges::any_of(m_inputs, [](const auto& input) { + const auto& [reader, path, multiplicityGenerator] = input; + // Check if this is NOT a FixedMultiplicityGenerator + return dynamic_cast( + multiplicityGenerator.get()) == nullptr; + }); + // Check if randomNumbers is required - bool needsRng = (m_cfg.vertexGenerator != nullptr); - if (!needsRng && !m_cfg.inputs.empty()) { - for (const auto& input : m_cfg.inputs) { - if (input.multiplicityGenerator != nullptr) { - needsRng = true; - break; - } - } - } + // RNG is needed if we have a vertex generator or any non-Fixed multiplicity + // generator + m_needsRng = (m_cfg.vertexGenerator != nullptr) || m_hasNonFixedMultiplicity; - if (needsRng && m_cfg.randomNumbers == nullptr) { + if (m_needsRng && m_cfg.randomNumbers == nullptr) { throw std::invalid_argument( - "randomNumbers must be set if vertexGenerator or any multiplicityGenerator is set"); + "randomNumbers must be set if vertexGenerator or any non-Fixed " + "multiplicityGenerator is used"); } ACTS_DEBUG("HepMC3Reader: " << m_eventsRange.first << " - " @@ -120,18 +150,38 @@ ProcessCode HepMC3Reader::skip(std::size_t events) { return SUCCESS; } - ACTS_DEBUG("Skipping " << events << " events"); + ACTS_DEBUG("Skipping " << events << " logical events"); - for (const auto& [reader, numEvents, path] : m_inputs) { - ACTS_VERBOSE("Skipping " << events << "*" << numEvents << "=" - << events * numEvents << " events from " << path); - if (!reader->skip(static_cast(events * numEvents))) { - ACTS_ERROR("Error skipping events " << events << " " << path); - return ABORT; + // Check if all multiplicity generators are Fixed (deterministic) + // Use the precomputed flag + if (m_hasNonFixedMultiplicity) { + ACTS_ERROR( + "Cannot skip events with non-Fixed multiplicityGenerator (e.g., " + "PoissonMultiplicityGenerator). Skipping requires knowing the exact " + "number of physical events to skip from each input file, which is only " + "possible with deterministic (Fixed) multiplicity generators."); + return ABORT; + } + + // For each logical event to skip, evaluate the Fixed multiplicity generators + // to determine how many physical events to skip from each input file + for (std::size_t logicalEvent = 0; logicalEvent < events; ++logicalEvent) { + for (const auto& [reader, path, multiplicityGenerator] : m_inputs) { + // Must be FixedMultiplicityGenerator (checked above), so downcast is safe + const auto* fixedGen = static_cast( + multiplicityGenerator.get()); + std::size_t count = fixedGen->n; + + ACTS_VERBOSE("Skipping " << count << " events from " << path + << " for logical event " << (m_nextEvent + logicalEvent)); + if (!reader->skip(static_cast(count))) { + ACTS_ERROR("Error skipping " << count << " events from " << path); + return ABORT; + } } } - m_nextEvent = events; + m_nextEvent += events; return SUCCESS; } @@ -338,27 +388,25 @@ ProcessCode HepMC3Reader::readLogicalEvent( // @TODO: Add the index as an attribute to the event and it's content - // Spawn RNG if needed by any multiplicityGenerator + // Spawn RNG for multiplicity generators if needed std::optional rng; - if (!m_cfg.inputs.empty()) { - for (const auto& input : m_cfg.inputs) { - if (input.multiplicityGenerator != nullptr) { - rng = m_cfg.randomNumbers->spawnGenerator(ctx); - break; - } - } + if (m_needsRng) { + rng = m_cfg.randomNumbers->spawnGenerator(ctx); } for (std::size_t inputIndex = 0; inputIndex < m_inputs.size(); ++inputIndex) { auto& reader = m_inputs[inputIndex].reader; auto& path = m_inputs[inputIndex].path; - std::size_t fixedCount = m_inputs[inputIndex].numEvents; + auto& multiplicityGenerator = m_inputs[inputIndex].multiplicityGenerator; - // Use generator if present in new inputs config, otherwise fixed count - std::size_t count = fixedCount; - if (!m_cfg.inputs.empty() && inputIndex < m_cfg.inputs.size() && - m_cfg.inputs[inputIndex].multiplicityGenerator != nullptr) { - count = (*m_cfg.inputs[inputIndex].multiplicityGenerator)(*rng); + // Use multiplicityGenerator to determine count + std::size_t count; + if (rng.has_value()) { + count = (*multiplicityGenerator)(*rng); + } else { + // Must be FixedMultiplicityGenerator if no RNG + RandomEngine dummyRng; + count = (*multiplicityGenerator)(dummyRng); } ACTS_VERBOSE("Reading " << count << " events from " << path); @@ -381,7 +429,7 @@ ProcessCode HepMC3Reader::readLogicalEvent( ProcessCode HepMC3Reader::finalize() { ACTS_VERBOSE("Closing " << m_inputs.size() << " input files"); - for (const auto& [reader, numEvents, path] : m_inputs) { + for (const auto& [reader, path, multiplicityGenerator] : m_inputs) { reader->close(); } diff --git a/Examples/Python/src/HepMC3.cpp b/Examples/Python/src/HepMC3.cpp index 008258450e0..1692067b734 100644 --- a/Examples/Python/src/HepMC3.cpp +++ b/Examples/Python/src/HepMC3.cpp @@ -38,24 +38,40 @@ void addHepMC3(Context& ctx) { // Expose nested Input struct py::class_(hepmc3, "Input") .def(py::init<>()) - .def(py::init([](const std::filesystem::path& path, std::size_t numEvents, + .def(py::init([](const std::filesystem::path& path, std::shared_ptr multiplicityGenerator) { HepMC3Reader::Config::Input inp; inp.path = path; - inp.numEvents = numEvents; inp.multiplicityGenerator = multiplicityGenerator; return inp; }), py::arg("path"), - py::arg("numEvents") = 1, - py::arg("multiplicityGenerator") = nullptr) + py::arg("multiplicityGenerator")) .def_readwrite("path", &HepMC3Reader::Config::Input::path) - .def_readwrite("numEvents", &HepMC3Reader::Config::Input::numEvents) .def_readwrite("multiplicityGenerator", - &HepMC3Reader::Config::Input::multiplicityGenerator); + &HepMC3Reader::Config::Input::multiplicityGenerator) + // Factory methods for convenience + .def_static("Fixed", + [](const std::filesystem::path& path, std::size_t n) { + HepMC3Reader::Config::Input inp; + inp.path = path; + inp.multiplicityGenerator = std::make_shared(n); + return inp; + }, + py::arg("path"), py::arg("n") = 1, + "Create Input with FixedMultiplicityGenerator") + .def_static("Poisson", + [](const std::filesystem::path& path, double mean) { + HepMC3Reader::Config::Input inp; + inp.path = path; + inp.multiplicityGenerator = std::make_shared(mean); + return inp; + }, + py::arg("path"), py::arg("mean"), + "Create Input with PoissonMultiplicityGenerator"); ACTS_PYTHON_DECLARE_READER(HepMC3Reader, hepmc3, "HepMC3Reader", inputs, - outputEvent, printListing, numEvents, + inputPath, outputEvent, printListing, numEvents, checkEventNumber, maxEventBufferSize, vertexGenerator, randomNumbers); diff --git a/Examples/Python/tests/test_hepmc3.py b/Examples/Python/tests/test_hepmc3.py index 55979c60eb3..e2688077fae 100644 --- a/Examples/Python/tests/test_hepmc3.py +++ b/Examples/Python/tests/test_hepmc3.py @@ -790,7 +790,10 @@ def test_hepmc3_reader_multiple_files(tmp_path, rng): s = Sequencer(numThreads=10, logLevel=acts.logging.INFO) reader = HepMC3Reader( - inputPaths=[(act_hs, 1), (act_pu, 10)], + inputs=[ + acts.examples.hepmc3.Input.Fixed(act_hs, 1), + acts.examples.hepmc3.Input.Fixed(act_pu, 10), + ], level=acts.logging.VERBOSE, outputEvent="hepmc3_event", vertexGenerator=vtxGen, diff --git a/Examples/Scripts/Python/hepmc3_merge.py b/Examples/Scripts/Python/hepmc3_merge.py index 4a95f57041c..9ed32670733 100755 --- a/Examples/Scripts/Python/hepmc3_merge.py +++ b/Examples/Scripts/Python/hepmc3_merge.py @@ -99,9 +99,9 @@ def main(): rng = acts.examples.RandomNumbers(seed=42) s.addReader( acts.examples.hepmc3.HepMC3Reader( - inputPaths=[ - (args.hard_scatter, 1), - (args.pileup, args.pileup_multiplicity), + inputs=[ + acts.examples.hepmc3.Input.Fixed(args.hard_scatter, 1), + acts.examples.hepmc3.Input.Fixed(args.pileup, args.pileup_multiplicity), ], level=acts.logging.INFO, outputEvent="hepmc3_event", From 0ef8faea20b1bed231db3de0a86afd0d7f7d9785 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 14 Oct 2025 10:35:54 +0200 Subject: [PATCH 11/14] improve input API, especially in python --- .../ActsExamples/Io/HepMC3/HepMC3Reader.hpp | 19 ++--- Examples/Io/HepMC3/src/HepMC3Reader.cpp | 18 +++-- Examples/Python/src/HepMC3.cpp | 74 +++++++++++-------- Examples/Python/tests/test_hepmc3.py | 4 +- Examples/Scripts/Python/hepmc3_merge.py | 7 +- 5 files changed, 70 insertions(+), 52 deletions(-) diff --git a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp index f1f7f76e782..aedf2b62d66 100644 --- a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp +++ b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp @@ -81,16 +81,17 @@ struct MultiplicityGenerator; /// class HepMC3Reader final : public IReader { public: + /// Input specification per file + struct Input { + /// Path to the HepMC3 file + std::filesystem::path path; + /// Multiplicity generator determining how many events to read per logical event. + /// Must always be set. Use FixedMultiplicityGenerator for deterministic behavior, + /// or PoissonMultiplicityGenerator for stochastic pileup simulation. + std::shared_ptr multiplicityGenerator; + }; + struct Config { - /// Input specification per file - struct Input { - /// Path to the HepMC3 file - std::filesystem::path path; - /// Multiplicity generator determining how many events to read per logical event. - /// Must always be set. Use FixedMultiplicityGenerator for deterministic behavior, - /// or PoissonMultiplicityGenerator for stochastic pileup simulation. - std::shared_ptr multiplicityGenerator; - }; /// Input files to read. For each file, the multiplicity generator determines /// how many events are read per logical event. This can be used to read e.g. diff --git a/Examples/Io/HepMC3/src/HepMC3Reader.cpp b/Examples/Io/HepMC3/src/HepMC3Reader.cpp index fb50e4b0c11..b459776e132 100644 --- a/Examples/Io/HepMC3/src/HepMC3Reader.cpp +++ b/Examples/Io/HepMC3/src/HepMC3Reader.cpp @@ -58,7 +58,7 @@ HepMC3Reader::HepMC3Reader(const HepMC3Reader::Config& cfg, // If inputPath is set, create a single input with default multiplicity // generator if (hasInputPath) { - Config::Input input; + Input input; input.path = m_cfg.inputPath.value(); input.multiplicityGenerator = std::make_shared(1); @@ -90,12 +90,13 @@ HepMC3Reader::HepMC3Reader(const HepMC3Reader::Config& cfg, } // Check if any input uses a non-Fixed multiplicity generator - m_hasNonFixedMultiplicity = std::ranges::any_of(m_inputs, [](const auto& input) { - const auto& [reader, path, multiplicityGenerator] = input; - // Check if this is NOT a FixedMultiplicityGenerator - return dynamic_cast( - multiplicityGenerator.get()) == nullptr; - }); + m_hasNonFixedMultiplicity = + std::ranges::any_of(m_inputs, [](const auto& input) { + const auto& [reader, path, multiplicityGenerator] = input; + // Check if this is NOT a FixedMultiplicityGenerator + return dynamic_cast( + multiplicityGenerator.get()) == nullptr; + }); // Check if randomNumbers is required // RNG is needed if we have a vertex generator or any non-Fixed multiplicity @@ -173,7 +174,8 @@ ProcessCode HepMC3Reader::skip(std::size_t events) { std::size_t count = fixedGen->n; ACTS_VERBOSE("Skipping " << count << " events from " << path - << " for logical event " << (m_nextEvent + logicalEvent)); + << " for logical event " + << (m_nextEvent + logicalEvent)); if (!reader->skip(static_cast(count))) { ACTS_ERROR("Error skipping " << count << " events from " << path); return ABORT; diff --git a/Examples/Python/src/HepMC3.cpp b/Examples/Python/src/HepMC3.cpp index 1692067b734..c820ebfcd2e 100644 --- a/Examples/Python/src/HepMC3.cpp +++ b/Examples/Python/src/HepMC3.cpp @@ -35,45 +35,59 @@ void addHepMC3(Context& ctx) { perEvent, inputEvent, compression, maxEventsPending, writeEventsInOrder); - // Expose nested Input struct - py::class_(hepmc3, "Input") + // Declare the HepMC3Reader class first + auto reader = + py::class_>( + hepmc3, "HepMC3Reader") + .def(py::init(), + py::arg("config"), py::arg("level")) + .def_property_readonly("config", &HepMC3Reader::config); + + // Expose Input struct as a nested class of HepMC3Reader + py::class_(reader, "Input") .def(py::init<>()) .def(py::init([](const std::filesystem::path& path, - std::shared_ptr multiplicityGenerator) { - HepMC3Reader::Config::Input inp; + std::shared_ptr + multiplicityGenerator) { + HepMC3Reader::Input inp; inp.path = path; inp.multiplicityGenerator = multiplicityGenerator; return inp; }), - py::arg("path"), - py::arg("multiplicityGenerator")) - .def_readwrite("path", &HepMC3Reader::Config::Input::path) + py::arg("path"), py::arg("multiplicityGenerator")) + .def_readwrite("path", &HepMC3Reader::Input::path) .def_readwrite("multiplicityGenerator", - &HepMC3Reader::Config::Input::multiplicityGenerator) + &HepMC3Reader::Input::multiplicityGenerator) // Factory methods for convenience - .def_static("Fixed", - [](const std::filesystem::path& path, std::size_t n) { - HepMC3Reader::Config::Input inp; - inp.path = path; - inp.multiplicityGenerator = std::make_shared(n); - return inp; - }, - py::arg("path"), py::arg("n") = 1, - "Create Input with FixedMultiplicityGenerator") - .def_static("Poisson", - [](const std::filesystem::path& path, double mean) { - HepMC3Reader::Config::Input inp; - inp.path = path; - inp.multiplicityGenerator = std::make_shared(mean); - return inp; - }, - py::arg("path"), py::arg("mean"), - "Create Input with PoissonMultiplicityGenerator"); + .def_static( + "Fixed", + [](const std::filesystem::path& path, std::size_t n) { + HepMC3Reader::Input inp; + inp.path = path; + inp.multiplicityGenerator = + std::make_shared(n); + return inp; + }, + py::arg("path"), py::arg("n") = 1, + "Create Input with FixedMultiplicityGenerator") + .def_static( + "Poisson", + [](const std::filesystem::path& path, double mean) { + HepMC3Reader::Input inp; + inp.path = path; + inp.multiplicityGenerator = + std::make_shared(mean); + return inp; + }, + py::arg("path"), py::arg("mean"), + "Create Input with PoissonMultiplicityGenerator"); - ACTS_PYTHON_DECLARE_READER(HepMC3Reader, hepmc3, "HepMC3Reader", inputs, - inputPath, outputEvent, printListing, numEvents, - checkEventNumber, maxEventBufferSize, - vertexGenerator, randomNumbers); + auto config = py::class_(reader, "Config") + .def(py::init<>(), "Default constructor"); + // Now configure the HepMC3Reader itself + ACTS_PYTHON_STRUCT(config, inputs, inputPath, outputEvent, printListing, + numEvents, checkEventNumber, maxEventBufferSize, + vertexGenerator, randomNumbers); ACTS_PYTHON_DECLARE_ALGORITHM(HepMC3OutputConverter, hepmc3, "HepMC3OutputConverter", inputParticles, diff --git a/Examples/Python/tests/test_hepmc3.py b/Examples/Python/tests/test_hepmc3.py index e2688077fae..f20993e8e7e 100644 --- a/Examples/Python/tests/test_hepmc3.py +++ b/Examples/Python/tests/test_hepmc3.py @@ -791,8 +791,8 @@ def test_hepmc3_reader_multiple_files(tmp_path, rng): reader = HepMC3Reader( inputs=[ - acts.examples.hepmc3.Input.Fixed(act_hs, 1), - acts.examples.hepmc3.Input.Fixed(act_pu, 10), + HepMC3Reader.Input.Fixed(act_hs, 1), + HepMC3Reader.Input.Fixed(act_pu, 10), ], level=acts.logging.VERBOSE, outputEvent="hepmc3_event", diff --git a/Examples/Scripts/Python/hepmc3_merge.py b/Examples/Scripts/Python/hepmc3_merge.py index 9ed32670733..4d26aecca68 100755 --- a/Examples/Scripts/Python/hepmc3_merge.py +++ b/Examples/Scripts/Python/hepmc3_merge.py @@ -97,11 +97,12 @@ def main(): s = acts.examples.Sequencer(numThreads=args.jobs, logLevel=acts.logging.WARNING) rng = acts.examples.RandomNumbers(seed=42) + HepMC3Reader = acts.examples.hepmc3.HepMC3Reader s.addReader( - acts.examples.hepmc3.HepMC3Reader( + HepMC3Reader( inputs=[ - acts.examples.hepmc3.Input.Fixed(args.hard_scatter, 1), - acts.examples.hepmc3.Input.Fixed(args.pileup, args.pileup_multiplicity), + HepMC3Reader.Input.Fixed(args.hard_scatter, 1), + HepMC3Reader.Input.Fixed(args.pileup, args.pileup_multiplicity), ], level=acts.logging.INFO, outputEvent="hepmc3_event", From 8c0d05fb15336a174ca4755148e1a0baf42c82b9 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 14 Oct 2025 10:42:26 +0200 Subject: [PATCH 12/14] lint --- .../ActsExamples/Io/HepMC3/HepMC3Reader.hpp | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp index aedf2b62d66..28f8257de25 100644 --- a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp +++ b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp @@ -36,10 +36,13 @@ struct MultiplicityGenerator; /// /// ## Multiplicity Generators /// -/// Each input file must have an associated multiplicity generator that determines -/// how many physical events to read from that file for each logical event: -/// - FixedMultiplicityGenerator: Always reads a fixed number of events (deterministic) -/// - PoissonMultiplicityGenerator: Reads a Poisson-distributed number of events (stochastic) +/// Each input file must have an associated multiplicity generator that +/// determines how many physical events to read from that file for each logical +/// event: +/// - FixedMultiplicityGenerator: Always reads a fixed number of events +/// (deterministic) +/// - PoissonMultiplicityGenerator: Reads a Poisson-distributed number of events +/// (stochastic) /// /// ## Usage Patterns /// @@ -53,8 +56,10 @@ struct MultiplicityGenerator; /// ``` /// HepMC3Reader reader({ /// .inputs = { -/// Input{path="signal.hepmc3", multiplicityGenerator=make_shared(1)}, -/// Input{path="pileup.hepmc3", multiplicityGenerator=make_shared(50)} +/// Input{path="signal.hepmc3", +/// multiplicityGenerator=make_shared(1)}, +/// Input{path="pileup.hepmc3", +/// multiplicityGenerator=make_shared(50)} /// }, /// ... /// }); @@ -64,8 +69,10 @@ struct MultiplicityGenerator; /// ``` /// HepMC3Reader reader({ /// .inputs = { -/// Input{path="signal.hepmc3", multiplicityGenerator=make_shared(1)}, -/// Input{path="pileup.hepmc3", multiplicityGenerator=make_shared(50)} +/// Input{path="signal.hepmc3", +/// multiplicityGenerator=make_shared(1)}, +/// Input{path="pileup.hepmc3", +/// multiplicityGenerator=make_shared(50)} /// }, /// .randomNumbers = rng, // Required for stochastic generators /// ... @@ -85,22 +92,23 @@ class HepMC3Reader final : public IReader { struct Input { /// Path to the HepMC3 file std::filesystem::path path; - /// Multiplicity generator determining how many events to read per logical event. - /// Must always be set. Use FixedMultiplicityGenerator for deterministic behavior, - /// or PoissonMultiplicityGenerator for stochastic pileup simulation. + /// Multiplicity generator determining how many events to read per logical + /// event. Must always be set. Use FixedMultiplicityGenerator for + /// deterministic behavior, or PoissonMultiplicityGenerator for stochastic + /// pileup simulation. std::shared_ptr multiplicityGenerator; }; struct Config { - - /// Input files to read. For each file, the multiplicity generator determines - /// how many events are read per logical event. This can be used to read e.g. - /// hard-scatter events from one file and pileup events from another. - /// Mutually exclusive with inputPath. + /// Input files to read. For each file, the multiplicity generator + /// determines how many events are read per logical event. This can be used + /// to read e.g. hard-scatter events from one file and pileup events from + /// another. Mutually exclusive with inputPath. std::vector inputs; - /// Convenience parameter for single-file reading. Mutually exclusive with inputs. - /// If set, creates a single input with FixedMultiplicityGenerator(n=1). + /// Convenience parameter for single-file reading. Mutually exclusive with + /// inputs. If set, creates a single input with + /// FixedMultiplicityGenerator(n=1). std::optional inputPath; /// The output collection @@ -127,8 +135,9 @@ class HepMC3Reader final : public IReader { /// The random number service. Required if: /// - vertexGenerator is set, OR - /// - any non-Fixed multiplicityGenerator is used (e.g., PoissonMultiplicityGenerator) - /// Not required when using only FixedMultiplicityGenerator with no vertexGenerator. + /// - any non-Fixed multiplicityGenerator is used (e.g., + /// PoissonMultiplicityGenerator) Not required when using only + /// FixedMultiplicityGenerator with no vertexGenerator. std::shared_ptr randomNumbers; /// Position generator that will be used to shift read events std::shared_ptr vertexGenerator; @@ -200,10 +209,12 @@ class HepMC3Reader final : public IReader { std::mutex m_queueMutex; - /// Precomputed flag: true if RNG is needed (vertex generator or non-Fixed multiplicity) + /// Precomputed flag: true if RNG is needed (vertex generator or non-Fixed + /// multiplicity) bool m_needsRng = false; - /// Precomputed flag: true if any input uses a non-Fixed multiplicity generator + /// Precomputed flag: true if any input uses a non-Fixed multiplicity + /// generator bool m_hasNonFixedMultiplicity = false; struct InputConfig { From 124aa4fe9f9a989839892135b8355388b20e28cd Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 14 Oct 2025 10:45:25 +0200 Subject: [PATCH 13/14] make examples valid C++ --- .../ActsExamples/Io/HepMC3/HepMC3Reader.hpp | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp index 28f8257de25..6d9258af854 100644 --- a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp +++ b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp @@ -47,36 +47,43 @@ struct MultiplicityGenerator; /// ## Usage Patterns /// /// 1. Single file with default multiplicity: -/// ``` -/// HepMC3Reader reader({.inputPath = "events.hepmc3", ...}); +/// ```cpp +/// HepMC3Reader::Config cfg; +/// cfg.inputPath = "events.hepmc3"; +/// cfg.outputEvent = "hepmc3_event"; +/// HepMC3Reader reader(cfg, Acts::Logging::INFO); /// ``` /// Automatically uses FixedMultiplicityGenerator(n=1). /// /// 2. Multiple files with fixed multiplicities (e.g., hard-scatter + pileup): -/// ``` -/// HepMC3Reader reader({ -/// .inputs = { -/// Input{path="signal.hepmc3", -/// multiplicityGenerator=make_shared(1)}, -/// Input{path="pileup.hepmc3", -/// multiplicityGenerator=make_shared(50)} -/// }, -/// ... -/// }); +/// ```cpp +/// HepMC3Reader::Config cfg; +/// cfg.inputs = { +/// {.path = "signal.hepmc3", +/// .multiplicityGenerator = +/// std::make_shared(1)}, +/// {.path = "pileup.hepmc3", +/// .multiplicityGenerator = +/// std::make_shared(50)} +/// }; +/// cfg.outputEvent = "hepmc3_event"; +/// HepMC3Reader reader(cfg, Acts::Logging::INFO); /// ``` /// /// 3. With stochastic pileup multiplicity: -/// ``` -/// HepMC3Reader reader({ -/// .inputs = { -/// Input{path="signal.hepmc3", -/// multiplicityGenerator=make_shared(1)}, -/// Input{path="pileup.hepmc3", -/// multiplicityGenerator=make_shared(50)} -/// }, -/// .randomNumbers = rng, // Required for stochastic generators -/// ... -/// }); +/// ```cpp +/// HepMC3Reader::Config cfg; +/// cfg.inputs = { +/// {.path = "signal.hepmc3", +/// .multiplicityGenerator = +/// std::make_shared(1)}, +/// {.path = "pileup.hepmc3", +/// .multiplicityGenerator = +/// std::make_shared(50.0)} +/// }; +/// cfg.outputEvent = "hepmc3_event"; +/// cfg.randomNumbers = rng; // Required for stochastic generators +/// HepMC3Reader reader(cfg, Acts::Logging::INFO); /// ``` /// /// ## Event Skipping From 7566a822183af9396868623d24429f85e264fbc3 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 14 Oct 2025 14:49:20 +0200 Subject: [PATCH 14/14] feat: More informative error output in HepMC3 reader --- Examples/Io/HepMC3/src/HepMC3Reader.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Examples/Io/HepMC3/src/HepMC3Reader.cpp b/Examples/Io/HepMC3/src/HepMC3Reader.cpp index 64b88e66df9..6009de2422d 100644 --- a/Examples/Io/HepMC3/src/HepMC3Reader.cpp +++ b/Examples/Io/HepMC3/src/HepMC3Reader.cpp @@ -401,13 +401,14 @@ ProcessCode HepMC3Reader::readLogicalEvent( auto& multiplicityGenerator = m_inputs[inputIndex].multiplicityGenerator; // Use multiplicityGenerator to determine count - std::size_t count; + std::size_t count = 0; if (rng.has_value()) { count = (*multiplicityGenerator)(*rng); } else { - // Must be FixedMultiplicityGenerator if no RNG - RandomEngine dummyRng; - count = (*multiplicityGenerator)(dummyRng); + // Must be FixedMultiplicityGenerator if no RNG, so downcast is safe + const auto& fixedGen = static_cast( + *multiplicityGenerator); + count = fixedGen.n; } ACTS_VERBOSE("Reading " << count << " events from " << path); @@ -416,7 +417,15 @@ ProcessCode HepMC3Reader::readLogicalEvent( reader->read_event(*event); if (reader->failed()) { - ACTS_ERROR("Error reading event " << i << " from " << path); + ACTS_ERROR("Error reading event " << i << " (input index = " + << inputIndex << ") from " << path); + if (inputIndex > 0) { + ACTS_ERROR("-> since this is input file index " + << inputIndex + << ", this probably means that the " + "input file has " + "fewer events than expected."); + } return ABORT; } events.push_back(std::move(event));