Repository: acts-project/acts
Pull request #4206: refactor: Decouple EvtGen from HepMC3 conversion
Author: login: paulgessinger
Labels: name: Infrastructure
description: Changes to build tools, continous integration, ...
name: Component - Examples
description: Affects the Examples module
name: Fails Athena tests
description: This PR causes a failure in the Athena tests
Created: 2025-04-11T14:20:48Z | Merged: 2025-04-22T08:30:29Z

Currently, the `EventGenerator` algorithm also performs the conversion from HepMC3 to our internal simulation EDM. With this PR, this is moved to a separate algorithm `HepMC3InputConverter`. This has the added benefit, that we can now reuse this code to read in HepMC3 files that were written ahead of time, without running the event generation.

Blocked by:
- #4205 
- https://github.com/acts-project/acts/pull/4208

--- END COMMIT MESSAGE ---

Any further description goes here, @-mentions are ok here!

- Use a *conventional commits* prefix: [quick summary](https://www.conventionalcommits.org/en/v1.0.0/#summary)
  - We mostly use `feat`, `fix`, `refactor`, `docs`, `chore` and `build` types.
- A milestone will be assigned by one of the maintainers


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
	- Introduced enhanced event processing with a new input conversion module.
	- Launched updated reader and writer components offering improved error handling and file management.
- **Refactor**
	- Streamlined event conversion and configuration by removing deprecated options.
	- Consolidated simulation and HepMC3 processing interfaces for a simplified user experience.
- **Bug Fixes**
	- Corrected messaging for missing directories.
- **Tests**
	- Expanded test coverage to validate HepMC3 processing and logging threshold behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/CI/physmon/workflows/physmon_simulation.py b/CI/physmon/workflows/physmon_simulation.py
index af56902f306..18753b62880 100755
--- a/CI/physmon/workflows/physmon_simulation.py
+++ b/CI/physmon/workflows/physmon_simulation.py
@@ -57,9 +57,18 @@
                     acts.PdgParticle.eElectron,
                 ]
             ],
+            randomNumbers=rnd,
+            outputEvent="particle_gun_event",
+        )
+    )
+
+    s.addAlgorithm(
+        acts.examples.hepmc3.HepMC3InputConverter(
+            level=acts.logging.INFO,
+            inputEvent="particle_gun_event",
             outputParticles="particles_generated",
             outputVertices="vertices_input",
-            randomNumbers=rnd,
+            mergePrimaries=False,
         )
     )
 
diff --git a/CMakePresets.json b/CMakePresets.json
index 5a75db8f329..362a9c461b2 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -71,7 +71,6 @@
                 "ACTS_BUILD_EXAMPLES_EDM4HEP": "ON",
                 "ACTS_BUILD_EXAMPLES_EXATRKX": "OFF",
                 "ACTS_BUILD_EXAMPLES_GEANT4": "ON",
-                "ACTS_BUILD_EXAMPLES_HEPMC3": "ON",
                 "ACTS_BUILD_EXAMPLES_PYTHIA8": "ON",
                 "ACTS_BUILD_EXAMPLES_HASHING": "ON"
             }
diff --git a/Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.cpp b/Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.cpp
index f737d0f1db5..85b5db5764f 100644
--- a/Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.cpp
+++ b/Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.cpp
@@ -8,8 +8,6 @@
 
 #include "ActsExamples/Generators/EventGenerator.hpp"
 
-#include "Acts/Definitions/ParticleData.hpp"
-#include "Acts/Definitions/PdgParticle.hpp"
 #include "Acts/Definitions/TrackParametrization.hpp"
 #include "Acts/Utilities/Logger.hpp"
 #include "Acts/Utilities/ScopedTimer.hpp"
@@ -17,10 +15,7 @@
 #include "ActsExamples/EventData/SimVertex.hpp"
 #include "ActsExamples/Framework/AlgorithmContext.hpp"
 #include "ActsExamples/Io/HepMC3/HepMC3Util.hpp"
-#include "ActsFatras/EventData/Barcode.hpp"
 
-#include <algorithm>
-#include <any>
 #include <limits>
 #include <memory>
 #include <optional>
@@ -31,19 +26,7 @@
 #include <HepMC3/GenEvent.h>
 #include <HepMC3/GenParticle.h>
 #include <HepMC3/GenVertex.h>
-
-// This is a hack to make HepMC3::Print::listing public
-// It's pretty evil but should have no side-effects
-#if defined(__clang__)
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wkeyword-macro"
-#endif
-#define private public
 #include <HepMC3/Print.h>
-#undef private
-#if defined(__clang__)
-#pragma clang diagnostic pop
-#endif
 
 using namespace Acts::UnitLiterals;
 
@@ -51,12 +34,6 @@ namespace ActsExamples {
 
 EventGenerator::EventGenerator(const Config& cfg, Acts::Logging::Level lvl)
     : m_cfg(cfg), m_logger(Acts::getDefaultLogger("EventGenerator", lvl)) {
-  if (m_cfg.outputParticles.empty()) {
-    throw std::invalid_argument("Missing output particles collection");
-  }
-  if (m_cfg.outputVertices.empty()) {
-    throw std::invalid_argument("Missing output vertices collection");
-  }
   if (m_cfg.generators.empty()) {
     throw std::invalid_argument("No generators are configured");
   }
@@ -77,8 +54,6 @@ EventGenerator::EventGenerator(const Config& cfg, Acts::Logging::Level lvl)
     throw std::invalid_argument("Missing random numbers service");
   }
 
-  m_outputParticles.initialize(m_cfg.outputParticles);
-  m_outputVertices.initialize(m_cfg.outputVertices);
   m_outputEvent.maybeInitialize(m_cfg.outputEvent);
 }
 
@@ -178,8 +153,6 @@ ProcessCode EventGenerator::read(const AlgorithmContext& ctx) {
                       << " n_primary_vertices=" << nPrimaryVertices
                       << " n_particles=" << event->particles_size());
 
-  convertHepMC3ToInternalEdm(ctx, *event);
-
   if (m_outputEvent.isInitialized()) {
     m_outputEvent(ctx, std::move(event));
   }
@@ -187,330 +160,4 @@ ProcessCode EventGenerator::read(const AlgorithmContext& ctx) {
   return ProcessCode::SUCCESS;
 }
 
-namespace {
-
-constexpr int kBeamParticleStatus = 4;
-constexpr int kUndecayedParticleStatus = 1;
-
-Acts::Vector4 convertPosition(const HepMC3::FourVector& vec) {
-  return Acts::Vector4(vec.x() * 1_mm, vec.y() * 1_mm, vec.z() * 1_mm,
-                       vec.t() * 1_mm);
-}
-
-std::string printListing(const auto& vertices, const auto& particles) {
-  auto findParticle = [&](SimBarcode particleId) {
-    if (auto it = std::ranges::find_if(particles,
-                                       [&](const auto& particle) {
-                                         return particle.particleId() ==
-                                                particleId;
-                                       });
-        it != particles.end()) {
-      return *it;
-    }
-    throw std::runtime_error("Particle not found");
-  };
-  std::stringstream ss;
-
-  for (const auto& vertex : vertices) {
-    ss << "Vtx:    " << vertex.vertexId() << " at "
-       << vertex.position4.transpose() << "\n";
-    for (const auto& [idx, particleId] : enumerate(vertex.incoming)) {
-      const auto& particle = findParticle(particleId);
-      if (idx == 0) {
-        ss << " I:     ";
-      } else {
-        ss << "        ";
-      }
-      ss << particle;
-      ss << "\n";
-    }
-
-    for (const auto& [idx, particleId] : enumerate(vertex.outgoing)) {
-      const auto& particle = findParticle(particleId);
-      if (idx == 0) {
-        ss << " O:     ";
-      } else {
-        ss << "        ";
-      }
-      ss << particle;
-      ss << "\n";
-    }
-  }
-
-  return ss.str();
-};
-}  // namespace
-
-void EventGenerator::handleVertex(const HepMC3::GenVertex& genVertex,
-                                  SimVertex& vertex,
-                                  std::vector<SimVertex>& vertices,
-                                  std::vector<SimParticle>& particles,
-                                  std::size_t& nSecondaryVertices,
-                                  std::size_t& nParticles,
-                                  std::vector<bool>& seenVertices) {
-  for (const auto& particle : genVertex.particles_out()) {
-    if (particle->end_vertex() != nullptr) {
-      // This particle has an end vertex, we need to handle that vertex
-      const auto& endVertex = *particle->end_vertex();
-      if (seenVertices.at(std::abs(endVertex.id()) - 1)) {
-        // We've seen this vertex before, we don't need to create it again
-        continue;
-      }
-      seenVertices.at(std::abs(endVertex.id()) - 1) = true;
-
-      const auto endVertexPosition = convertPosition(endVertex.position());
-
-      ACTS_VERBOSE("Found secondary vertex at "
-                   << endVertexPosition.transpose());
-      double distance = (endVertexPosition.template head<3>() -
-                         vertex.position4.template head<3>())
-                            .squaredNorm();
-
-      if (distance <=
-              m_cfg.vertexSpatialThreshold * m_cfg.vertexSpatialThreshold &&
-          m_cfg.mergeSecondaries) {
-        handleVertex(endVertex, vertex, vertices, particles, nSecondaryVertices,
-                     nParticles, seenVertices);
-      } else {
-        // Over threshold, make a new vertex
-        SimVertex secondaryVertex;
-        nSecondaryVertices += 1;
-        secondaryVertex.id =
-            SimVertexBarcode{vertex.id}.setVertexSecondary(nSecondaryVertices);
-        secondaryVertex.position4 = convertPosition(endVertex.position());
-
-        handleVertex(endVertex, secondaryVertex, vertices, particles,
-                     nSecondaryVertices, nParticles, seenVertices);
-
-        // Only keep the secondary vertex if it has outgoing particles
-        if (!secondaryVertex.outgoing.empty()) {
-          vertices.push_back(secondaryVertex);
-        }
-      }
-    } else {
-      if (particle->status() != kUndecayedParticleStatus) {
-        ACTS_ERROR("Undecayed particle has status "
-                   << particle->status() << "(and not "
-                   << kUndecayedParticleStatus << ")");
-      }
-      // This particle is a final state particle
-      SimBarcode particleId{0u};
-      nParticles += 1;
-      particleId.setVertexPrimary(vertex.vertexId().vertexPrimary())
-          .setVertexSecondary(vertex.vertexId().vertexSecondary())
-          .setParticle(nParticles);
-
-      Acts::PdgParticle pdg{particle->pdg_id()};
-      double mass = 0.0;
-      double charge = 0.0;
-
-      if (pdg != Acts::PdgParticle::eInvalid) {
-        if (auto massOpt = Acts::findMass(pdg); massOpt.has_value()) {
-          mass = massOpt.value();
-        } else {
-          ACTS_ERROR("No mass found for PDG ID " << pdg);
-          throw std::bad_optional_access{};
-        }
-
-        if (auto chargeOpt = Acts::findCharge(pdg); chargeOpt.has_value()) {
-          charge = chargeOpt.value();
-        } else {
-          ACTS_ERROR("No charge found for PDG ID " << pdg);
-          throw std::bad_optional_access{};
-        }
-      }
-
-      if (std::isnan(mass) || std::isnan(charge)) {
-        // This particle does not have a mass or a charge based on our data
-        // table:
-        // => We can't handle it
-        continue;
-      }
-
-      SimParticle simParticle{particleId, pdg, charge, mass};
-      simParticle.initial().setPosition4(vertex.position4);
-
-      const HepMC3::FourVector& genMomentum = particle->momentum();
-      Acts::Vector3 momentum{genMomentum.px() * 1_GeV, genMomentum.py() * 1_GeV,
-                             genMomentum.pz() * 1_GeV};
-      double p = momentum.norm();
-
-      simParticle.initial().setDirection(momentum.normalized());
-      simParticle.initial().setAbsoluteMomentum(p);
-
-      particles.push_back(simParticle);
-      vertex.outgoing.insert(particleId);
-    }
-  }
-}
-
-void EventGenerator::convertHepMC3ToInternalEdm(
-    const AlgorithmContext& ctx, const HepMC3::GenEvent& genEvent) {
-  ACTS_DEBUG("Converting HepMC3 event to internal EDM");
-
-  ACTS_VERBOSE("Have " << genEvent.vertices_size() << " vertices");
-  ACTS_VERBOSE("Have " << genEvent.particles_size() << " particles");
-
-  using VertexCluster = std::vector<HepMC3::ConstGenVertexPtr>;
-  std::vector<VertexCluster> vertexClusters;
-
-  ACTS_VERBOSE("Finding primary vertex clusters with threshold "
-               << m_cfg.primaryVertexSpatialThreshold);
-
-  // Find all vertices whose incoming particles are either all beam particles or
-  // that don't have any incoming particles
-  {
-    Acts::ScopedTimer timer("Finding primary vertex clusters", logger(),
-                            Acts::Logging::DEBUG);
-    for (const auto& vertex : genEvent.vertices()) {
-      if (vertex->particles_in().empty() ||
-          std::ranges::all_of(vertex->particles_in(), [](const auto& particle) {
-            return particle->status() == kBeamParticleStatus;
-          })) {
-        // Check if primary vertex is within tolerance of an existing primary
-        // vertex
-
-        ACTS_VERBOSE("Found primary vertex candidate at:\n"
-                     << [&]() {
-                          std::stringstream ss;
-                          HepMC3::Print::listing(ss, vertex);
-                          return ss.str();
-                        }());
-
-        auto position = convertPosition(vertex->position());
-
-        if (auto it = std::ranges::find_if(
-                vertexClusters,
-                [&](const auto& cluster) {
-                  const auto clusterPosition =
-                      convertPosition(cluster.at(0)->position());
-                  return (position - clusterPosition)
-                             .template head<3>()
-                             .cwiseAbs()
-                             .maxCoeff() < m_cfg.primaryVertexSpatialThreshold;
-                });
-            it != vertexClusters.end() && m_cfg.mergePrimaries) {
-          // Add the vertex to the cluster
-          it->push_back(vertex);
-        } else {
-          // Make a new cluster
-          vertexClusters.push_back({vertex});
-        }
-      }
-    }
-  }
-
-  ACTS_DEBUG("Found " << vertexClusters.size()
-                      << " primary vertex clusters (~ primary vertices)");
-
-  std::vector<SimVertex> verticesUnordered;
-  std::vector<SimParticle> particlesUnordered;
-
-  std::vector<bool> seenVertices;
-  seenVertices.resize(genEvent.vertices_size());
-
-  for (const auto& cluster : vertexClusters) {
-    for (const auto& vertex : cluster) {
-      seenVertices.at(std::abs(vertex->id()) - 1) = true;
-    }
-  }
-
-  std::size_t nPrimaryVertices = 0;
-  {
-    Acts::ScopedTimer timer("Converting HepMC3 vertices to internal EDM",
-                            logger(), Acts::Logging::DEBUG);
-
-    std::size_t nUndecayedParticles = 0;
-    for (const auto& particle : genEvent.particles()) {
-      if (particle->end_vertex() == nullptr) {
-        nUndecayedParticles += 1;
-      }
-    }
-    ACTS_DEBUG("Found " << nUndecayedParticles
-                        << " undecayed particles in the event");
-    ACTS_DEBUG("Reserving " << genEvent.vertices_size() / 2 << " vertices and "
-                            << nUndecayedParticles << " particles");
-    verticesUnordered.reserve(genEvent.vertices_size() / 2);
-    particlesUnordered.reserve(nUndecayedParticles);
-
-    for (auto& cluster : vertexClusters) {
-      ACTS_VERBOSE("Primary vertex cluster at "
-                   << convertPosition(cluster.at(0)->position()).transpose()
-                   << " containing " << cluster.size() << " vertices:\n"
-                   <<
-                   [&]() {
-                     std::stringstream ss;
-                     for (auto& vertex : cluster) {
-                       HepMC3::Print::listing(ss, vertex);
-                     }
-                     return ss.str();
-                   }()
-                   << "--------------");
-
-      nPrimaryVertices += 1;
-      SimVertex primaryVertex;
-      primaryVertex.id = SimVertexBarcode{}.setVertexPrimary(nPrimaryVertices);
-      primaryVertex.position4 = convertPosition(cluster.at(0)->position());
-
-      std::size_t nSecondaryVertices = 0;
-      std::size_t nParticles = 0;
-
-      for (auto& genVertex : cluster) {
-        handleVertex(*genVertex, primaryVertex, verticesUnordered,
-                     particlesUnordered, nSecondaryVertices, nParticles,
-                     seenVertices);
-      }
-      verticesUnordered.push_back(primaryVertex);
-    }
-  }
-
-  ACTS_DEBUG("Converted " << particlesUnordered.size() << " particles and "
-                          << verticesUnordered.size() << " vertices");
-
-  if (m_cfg.printListing) {
-    ACTS_VERBOSE("Converted event record:\n"
-                 << printListing(verticesUnordered, particlesUnordered));
-  }
-
-  {
-    Acts::ScopedTimer timer("Sorting particles and vertices", logger(),
-                            Acts::Logging::DEBUG);
-
-    std::ranges::sort(particlesUnordered, [](const auto& a, const auto& b) {
-      return a.particleId() < b.particleId();
-    });
-    std::ranges::sort(verticesUnordered, [](const auto& a, const auto& b) {
-      return a.vertexId() < b.vertexId();
-    });
-  }
-
-  if (m_cfg.checkConsistency) {
-    ACTS_DEBUG("Checking consistency of particles");
-    auto equalParticleIds = [](const auto& a, const auto& b) {
-      return a.particleId() == b.particleId();
-    };
-    if (auto it =
-            std::ranges::adjacent_find(particlesUnordered, equalParticleIds);
-        it != particlesUnordered.end()) {
-      ACTS_ERROR("Found duplicate particle id " << it->particleId());
-    }
-    auto equalVertexIds = [](const auto& a, const auto& b) {
-      return a.vertexId() == b.vertexId();
-    };
-    if (auto it = std::ranges::adjacent_find(verticesUnordered, equalVertexIds);
-        it != verticesUnordered.end()) {
-      ACTS_ERROR("Found duplicate vertex id " << it->vertexId());
-    }
-  }
-
-  SimParticleContainer particles{particlesUnordered.begin(),
-                                 particlesUnordered.end()};
-  SimVertexContainer vertices{verticesUnordered.begin(),
-                              verticesUnordered.end()};
-
-  // move generated event to the store
-  m_outputParticles(ctx, std::move(particles));
-  m_outputVertices(ctx, std::move(vertices));
-}
-
 }  // namespace ActsExamples
diff --git a/Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.hpp b/Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.hpp
index 049e2f0daa8..6fd1b56e8b6 100644
--- a/Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.hpp
+++ b/Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.hpp
@@ -93,11 +93,6 @@ class EventGenerator final : public ActsExamples::IReader {
   };
 
   struct Config {
-    /// Name of the output particles collection.
-    std::string outputParticles;
-    /// Name of the output vertex collection.
-    std::string outputVertices;
-
     /// Name of the output event collection.
     std::optional<std::string> outputEvent = "hepmc3_event";
 
@@ -109,23 +104,6 @@ class EventGenerator final : public ActsExamples::IReader {
     /// If true, print the listing of the generated event. This can be very
     /// verbose
     bool printListing = false;
-
-    /// Merge primary vertices
-    bool mergePrimaries = true;
-
-    /// The spatial vertex threshold below which to consider primary vertices
-    /// candidates identical.
-    double primaryVertexSpatialThreshold = 1 * Acts::UnitConstants::nm;
-
-    /// The spatial vertex threshold below which to consider secondary vertices
-    /// candidates identical.
-    double vertexSpatialThreshold = 1 * Acts::UnitConstants::um;
-
-    /// If true, merge secondary vertices that are close to their parent vertex
-    bool mergeSecondaries = true;
-
-    /// If true, check the consistency of the generated event.
-    bool checkConsistency = false;
   };
 
   EventGenerator(const Config& cfg, Acts::Logging::Level lvl);
@@ -144,9 +122,6 @@ class EventGenerator final : public ActsExamples::IReader {
  private:
   const Acts::Logger& logger() const { return *m_logger; }
 
-  void convertHepMC3ToInternalEdm(const AlgorithmContext& ctx,
-                                  const HepMC3::GenEvent& genEvent);
-
   void handleVertex(const HepMC3::GenVertex& genVertex, SimVertex& vertex,
                     std::vector<SimVertex>& vertices,
                     std::vector<SimParticle>& particles,
@@ -156,10 +131,6 @@ class EventGenerator final : public ActsExamples::IReader {
   Config m_cfg;
   std::unique_ptr<const Acts::Logger> m_logger;
 
-  WriteDataHandle<SimParticleContainer> m_outputParticles{this,
-                                                          "OutputParticles"};
-  WriteDataHandle<SimVertexContainer> m_outputVertices{this, "OutputVertices"};
-
   WriteDataHandle<std::shared_ptr<HepMC3::GenEvent>> m_outputEvent{
       this, "OutputEvent"};
 };
diff --git a/Examples/Algorithms/GeneratorsPythia8/ActsExamples/Generators/Pythia8ProcessGenerator.cpp b/Examples/Algorithms/GeneratorsPythia8/ActsExamples/Generators/Pythia8ProcessGenerator.cpp
index a74d3f498ba..eff3a865fa0 100644
--- a/Examples/Algorithms/GeneratorsPythia8/ActsExamples/Generators/Pythia8ProcessGenerator.cpp
+++ b/Examples/Algorithms/GeneratorsPythia8/ActsExamples/Generators/Pythia8ProcessGenerator.cpp
@@ -56,10 +56,8 @@ struct Pythia8RandomEngineWrapper : public Pythia8::RndmEngine {
 };
 
 struct Pythia8GeneratorImpl {
-#if defined(_HAS_HEPMC3)
   std::unique_ptr<HepMC3::Writer> m_hepMC3Writer;
   std::unique_ptr<HepMC3::Pythia8ToHepMC3> m_hepMC3Converter;
-#endif
   std::shared_ptr<Pythia8RandomEngineWrapper> m_pythia8RndmEngine;
 };
 
diff --git a/Examples/Algorithms/GeneratorsPythia8/CMakeLists.txt b/Examples/Algorithms/GeneratorsPythia8/CMakeLists.txt
index 497f2ad1569..eedb1ebe10d 100644
--- a/Examples/Algorithms/GeneratorsPythia8/CMakeLists.txt
+++ b/Examples/Algorithms/GeneratorsPythia8/CMakeLists.txt
@@ -13,14 +13,6 @@ target_link_libraries(
     PRIVATE Pythia8::Pythia8
 )
 
-if(ACTS_BUILD_EXAMPLES_HEPMC3)
-    target_link_libraries(ActsExamplesGeneratorsPythia8 PRIVATE HepMC3::HepMC3)
-    target_compile_definitions(
-        ActsExamplesGeneratorsPythia8
-        PRIVATE _HAS_HEPMC3
-    )
-endif()
-
 install(
     TARGETS ActsExamplesGeneratorsPythia8
     LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
diff --git a/Examples/Framework/src/Utilities/Paths.cpp b/Examples/Framework/src/Utilities/Paths.cpp
index 4de064d6eb0..b056f97c5eb 100644
--- a/Examples/Framework/src/Utilities/Paths.cpp
+++ b/Examples/Framework/src/Utilities/Paths.cpp
@@ -70,7 +70,7 @@ std::pair<std::size_t, std::size_t> ActsExamples::determineEventFilesRange(
   // ensure directory path is valid
   auto dir_path = dir.empty() ? current_path() : path(dir);
   if (!exists(dir_path)) {
-    throw std::runtime_error("'" + dir_path.native() + "' does not exists");
+    throw std::runtime_error("'" + dir_path.native() + "' does not exist");
   }
   if (!is_directory(dir_path)) {
     throw std::runtime_error("'" + dir_path.native() + "' is not a directory");
diff --git a/Examples/Io/HepMC3/CMakeLists.txt b/Examples/Io/HepMC3/CMakeLists.txt
index 86d19db7006..e8efa087778 100644
--- a/Examples/Io/HepMC3/CMakeLists.txt
+++ b/Examples/Io/HepMC3/CMakeLists.txt
@@ -3,6 +3,7 @@ add_library(
     SHARED
     src/HepMC3Reader.cpp
     src/HepMC3Writer.cpp
+    src/HepMC3InputConverter.cpp
     src/HepMC3OutputConverter.cpp
     src/HepMC3Util.cpp
 )
diff --git a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp
new file mode 100644
index 00000000000..50bff2a6b47
--- /dev/null
+++ b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp
@@ -0,0 +1,81 @@
+// This file is part of the ACTS project.
+//
+// Copyright (C) 2016 CERN for the benefit of the ACTS project
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include "Acts/Utilities/Logger.hpp"
+#include "ActsExamples/EventData/SimParticle.hpp"
+#include "ActsExamples/EventData/SimVertex.hpp"
+#include "ActsExamples/Framework/DataHandle.hpp"
+#include "ActsExamples/Framework/IAlgorithm.hpp"
+
+#include <string>
+
+namespace HepMC3 {
+class GenEvent;
+class GenVertex;
+}  // namespace HepMC3
+
+namespace ActsExamples {
+
+class HepMC3InputConverter : public IAlgorithm {
+ public:
+  struct Config {
+    std::string inputEvent;
+    std::string outputParticles;
+    std::string outputVertices;
+
+    /// If true, print the listing of the generated event. This can be very
+    /// verbose
+    bool printListing = false;
+
+    /// Merge primary vertices
+    bool mergePrimaries = true;
+
+    /// The spatial vertex threshold below which to consider primary vertices
+    /// candidates identical.
+    double primaryVertexSpatialThreshold = 1 * Acts::UnitConstants::nm;
+
+    /// The spatial vertex threshold below which to consider secondary vertices
+    /// candidates identical.
+    double vertexSpatialThreshold = 1 * Acts::UnitConstants::um;
+
+    /// If true, merge secondary vertices that are close to their parent vertex
+    bool mergeSecondaries = true;
+
+    /// If true, check the consistency of the generated event.
+    bool checkConsistency = false;
+  };
+
+  HepMC3InputConverter(const Config& config, Acts::Logging::Level level);
+
+  const Config& config() const { return m_cfg; }
+
+  ProcessCode execute(const AlgorithmContext& ctx) const final;
+
+ private:
+  void convertHepMC3ToInternalEdm(const AlgorithmContext& ctx,
+                                  const HepMC3::GenEvent& genEvent) const;
+
+  void handleVertex(const HepMC3::GenVertex& genVertex, SimVertex& vertex,
+                    std::vector<SimVertex>& vertices,
+                    std::vector<SimParticle>& particles,
+                    std::size_t& nSecondaryVertices, std::size_t& nParticles,
+                    std::vector<bool>& seenVertices) const;
+
+  Config m_cfg;
+
+  ReadDataHandle<std::shared_ptr<HepMC3::GenEvent>> m_inputEvent{this,
+                                                                 "InputEvent"};
+
+  WriteDataHandle<SimParticleContainer> m_outputParticles{this,
+                                                          "OutputParticles"};
+  WriteDataHandle<SimVertexContainer> m_outputVertices{this, "OutputVertices"};
+};
+
+}  // namespace ActsExamples
diff --git a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp
index 84dbbfd926f..dc866f3eb52 100644
--- a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp
+++ b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp
@@ -12,39 +12,57 @@
 #include "ActsExamples/Framework/DataHandle.hpp"
 #include "ActsExamples/Framework/IReader.hpp"
 
-#include <HepMC3/GenEvent.h>
-#include <HepMC3/ReaderAscii.h>
+#include <filesystem>
+#include <mutex>
+#include <string>
+
+namespace HepMC3 {
+class GenEvent;
+class Reader;
+}  // namespace HepMC3
 
 namespace ActsExamples {
 
 /// HepMC3 event reader.
-class HepMC3AsciiReader final : public IReader {
+class HepMC3Reader final : public IReader {
  public:
   struct Config {
-    // The input directory
-    std::string inputDir;
-    // The stem of input file names
-    std::string inputStem;
-    // The output collection
-    std::string outputEvents;
+    /// The input file path for reading HepMC3 events.
+    ///
+    /// This path is handled differently based on the perEvent flag:
+    /// - If perEvent is false: The path points to a single file containing all
+    /// events
+    /// - If perEvent is true: The path is used as a template for finding
+    /// per-event files
+    ///   in the format "event{number}-{filename}" in the parent directory
+    ///
+    /// When in per-event mode, the reader uses determineEventFilesRange() to
+    /// scan the directory for matching files and determine the available event
+    /// range.
+    std::filesystem::path inputPath;
+
+    /// If true, one file per event is read
+    bool perEvent = false;
+
+    /// The output collection
+    std::string outputEvent;
+
+    /// If true, print the event listing
+    bool printListing = false;
+
+    /// HepMC3 does not expose the number of events in the file, so we need to
+    /// provide it here if known, otherwise the reader will have to read the
+    /// whole file
+    std::optional<std::size_t> numEvents = std::nullopt;
   };
 
-  /// @brief Reads an event from file
-  /// @param reader reader of run files
-  /// @param event storage of the read event
-  /// @return boolean indicator if the reading was successful
-  bool readEvent(HepMC3::ReaderAscii& reader, HepMC3::GenEvent& event);
-
-  /// @brief Reports the status of the reader
-  /// @param reader reader of run files
-  /// @return boolean status indicator
-  bool status(HepMC3::ReaderAscii& reader);
-
   /// Construct the particle reader.
   ///
   /// @param [in] cfg The configuration object
   /// @param [in] lvl The logging level
-  HepMC3AsciiReader(const Config& cfg, Acts::Logging::Level lvl);
+  HepMC3Reader(const Config& cfg, Acts::Logging::Level lvl);
+
+  ~HepMC3Reader() override;
 
   std::string name() const override;
 
@@ -54,10 +72,16 @@ class HepMC3AsciiReader final : public IReader {
   /// Read out data from the input stream.
   ProcessCode read(const ActsExamples::AlgorithmContext& ctx) override;
 
+  ProcessCode finalize() override;
+
   /// Get readonly access to the config parameters
   const Config& config() const { return m_cfg; }
 
  private:
+  std::size_t determineNumEvents(HepMC3::Reader& reader) const;
+
+  std::unique_ptr<HepMC3::Reader> makeReader() const;
+
   /// The configuration of this writer
   Config m_cfg;
   /// Number of events
@@ -67,8 +91,12 @@ class HepMC3AsciiReader final : public IReader {
 
   const Acts::Logger& logger() const { return *m_logger; }
 
-  WriteDataHandle<std::vector<HepMC3::GenEvent>> m_outputEvents{this,
-                                                                "OutputEvents"};
+  WriteDataHandle<std::shared_ptr<HepMC3::GenEvent>> m_outputEvent{
+      this, "OutputEvent"};
+
+  std::mutex m_mutex;
+
+  std::unique_ptr<HepMC3::Reader> m_reader;
 };
 
 }  // namespace ActsExamples
diff --git a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Writer.hpp b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Writer.hpp
index f908c87b1cf..b36193e8f0e 100644
--- a/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Writer.hpp
+++ b/Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Writer.hpp
@@ -13,6 +13,7 @@
 #include "ActsExamples/Utilities/OptionsFwd.hpp"
 
 #include <filesystem>
+#include <mutex>
 #include <string>
 
 namespace HepMC3 {
@@ -23,18 +24,27 @@ class Writer;
 namespace ActsExamples {
 
 /// HepMC3 event writer.
-class HepMC3AsciiWriter final
-    : public WriterT<std::shared_ptr<HepMC3::GenEvent>> {
+class HepMC3Writer final : public WriterT<std::shared_ptr<HepMC3::GenEvent>> {
  public:
   struct Config {
     /// If true, one file per event is written with the event number appended to
     /// the filename
     bool perEvent = false;
 
-    /// The output file path
+    /// The output file path for writing HepMC3 events.
+    ///
+    /// This path is handled differently based on the perEvent flag:
+    /// - If perEvent is false: The path points to a single file where all
+    /// events will be written
+    /// - If perEvent is true: The path is used as a template for creating
+    /// per-event files
+    ///   in the format "event{number}-{filename}" in the parent directory
+    ///
+    /// When in per-event mode, the writer uses perEventFilepath() to generate
+    /// the appropriate filename for each event.
     std::filesystem::path outputPath;
 
-    // The input collection
+    /// The input collection
     std::string inputEvent;
   };
 
@@ -42,9 +52,9 @@ class HepMC3AsciiWriter final
   ///
   /// @param [in] config Config of the writer
   /// @param [in] level The level of the logger
-  HepMC3AsciiWriter(const Config& config, Acts::Logging::Level level);
+  HepMC3Writer(const Config& config, Acts::Logging::Level level);
 
-  ~HepMC3AsciiWriter() override;
+  ~HepMC3Writer() override;
 
   /// Writing events to file.
   ///
diff --git a/Examples/Io/HepMC3/src/HepMC3InputConverter.cpp b/Examples/Io/HepMC3/src/HepMC3InputConverter.cpp
new file mode 100644
index 00000000000..f07a6820128
--- /dev/null
+++ b/Examples/Io/HepMC3/src/HepMC3InputConverter.cpp
@@ -0,0 +1,380 @@
+// This file is part of the ACTS project.
+//
+// Copyright (C) 2016 CERN for the benefit of the ACTS project
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp"
+
+#include "Acts/Utilities/ScopedTimer.hpp"
+
+#include <HepMC3/GenEvent.h>
+#include <HepMC3/GenParticle.h>
+#include <HepMC3/GenVertex.h>
+
+// This is a hack to make HepMC3::Print::listing public
+// It's pretty evil but should have no side-effects
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wkeyword-macro"
+#endif
+#define private public
+#include <HepMC3/Print.h>
+#undef private
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+
+using namespace Acts::UnitLiterals;
+
+namespace ActsExamples {
+
+HepMC3InputConverter::HepMC3InputConverter(const Config& config,
+                                           Acts::Logging::Level level)
+    : IAlgorithm("HepMC3InputConverter", level), m_cfg(config) {
+  m_outputParticles.initialize(m_cfg.outputParticles);
+  m_outputVertices.initialize(m_cfg.outputVertices);
+  m_inputEvent.initialize(m_cfg.inputEvent);
+}
+
+ProcessCode HepMC3InputConverter::execute(const AlgorithmContext& ctx) const {
+  ACTS_DEBUG("HepMC3InputConverter::execute");
+  const auto& genEvent = *m_inputEvent(ctx);
+  ACTS_DEBUG("Have " << genEvent.vertices_size() << " vertices");
+  ACTS_DEBUG("Have " << genEvent.particles_size() << " particles");
+  ACTS_DEBUG("Have " << genEvent.event_number() << " event number");
+
+  convertHepMC3ToInternalEdm(ctx, genEvent);
+
+  return ProcessCode::SUCCESS;
+}
+
+namespace {
+
+constexpr int kBeamParticleStatus = 4;
+constexpr int kUndecayedParticleStatus = 1;
+
+Acts::Vector4 convertPosition(const HepMC3::FourVector& vec) {
+  return Acts::Vector4(vec.x() * 1_mm, vec.y() * 1_mm, vec.z() * 1_mm,
+                       vec.t() * 1_mm);
+}
+
+std::string printListing(const auto& vertices, const auto& particles) {
+  auto findParticle = [&](SimBarcode particleId) {
+    if (auto it = std::ranges::find_if(particles,
+                                       [&](const auto& particle) {
+                                         return particle.particleId() ==
+                                                particleId;
+                                       });
+        it != particles.end()) {
+      return *it;
+    }
+    throw std::runtime_error("Particle not found");
+  };
+  std::stringstream ss;
+
+  for (const auto& vertex : vertices) {
+    ss << "Vtx:    " << vertex.vertexId() << " at "
+       << vertex.position4.transpose() << "\n";
+    for (const auto& [idx, particleId] : enumerate(vertex.incoming)) {
+      const auto& particle = findParticle(particleId);
+      if (idx == 0) {
+        ss << " I:     ";
+      } else {
+        ss << "        ";
+      }
+      ss << particle;
+      ss << "\n";
+    }
+
+    for (const auto& [idx, particleId] : enumerate(vertex.outgoing)) {
+      const auto& particle = findParticle(particleId);
+      if (idx == 0) {
+        ss << " O:     ";
+      } else {
+        ss << "        ";
+      }
+      ss << particle;
+      ss << "\n";
+    }
+  }
+
+  return ss.str();
+};
+}  // namespace
+
+void HepMC3InputConverter::handleVertex(const HepMC3::GenVertex& genVertex,
+                                        SimVertex& vertex,
+                                        std::vector<SimVertex>& vertices,
+                                        std::vector<SimParticle>& particles,
+                                        std::size_t& nSecondaryVertices,
+                                        std::size_t& nParticles,
+                                        std::vector<bool>& seenVertices) const {
+  for (const auto& particle : genVertex.particles_out()) {
+    if (particle->end_vertex() != nullptr) {
+      // This particle has an end vertex, we need to handle that vertex
+      const auto& endVertex = *particle->end_vertex();
+      if (seenVertices.at(std::abs(endVertex.id()) - 1)) {
+        // We've seen this vertex before, we don't need to create it again
+        continue;
+      }
+      seenVertices.at(std::abs(endVertex.id()) - 1) = true;
+
+      const auto endVertexPosition = convertPosition(endVertex.position());
+
+      ACTS_VERBOSE("Found secondary vertex at "
+                   << endVertexPosition.transpose());
+      double distance = (endVertexPosition.template head<3>() -
+                         vertex.position4.template head<3>())
+                            .squaredNorm();
+
+      if (distance <=
+              m_cfg.vertexSpatialThreshold * m_cfg.vertexSpatialThreshold &&
+          m_cfg.mergeSecondaries) {
+        handleVertex(endVertex, vertex, vertices, particles, nSecondaryVertices,
+                     nParticles, seenVertices);
+      } else {
+        // Over threshold, make a new vertex
+        SimVertex secondaryVertex;
+        nSecondaryVertices += 1;
+        secondaryVertex.id =
+            SimVertexBarcode{vertex.id}.setVertexSecondary(nSecondaryVertices);
+        secondaryVertex.position4 = convertPosition(endVertex.position());
+
+        handleVertex(endVertex, secondaryVertex, vertices, particles,
+                     nSecondaryVertices, nParticles, seenVertices);
+
+        // Only keep the secondary vertex if it has outgoing particles
+        if (!secondaryVertex.outgoing.empty()) {
+          vertices.push_back(secondaryVertex);
+        }
+      }
+    } else {
+      if (particle->status() != kUndecayedParticleStatus) {
+        ACTS_ERROR("Undecayed particle has status "
+                   << particle->status() << "(and not "
+                   << kUndecayedParticleStatus << ")");
+      }
+      // This particle is a final state particle
+      SimBarcode particleId{0u};
+      nParticles += 1;
+      particleId.setVertexPrimary(vertex.vertexId().vertexPrimary())
+          .setVertexSecondary(vertex.vertexId().vertexSecondary())
+          .setParticle(nParticles);
+
+      Acts::PdgParticle pdg{particle->pdg_id()};
+      double mass = 0.0;
+      double charge = 0.0;
+
+      if (pdg != Acts::PdgParticle::eInvalid) {
+        if (auto massOpt = Acts::findMass(pdg); massOpt.has_value()) {
+          mass = massOpt.value();
+        } else {
+          ACTS_ERROR("No mass found for PDG ID " << pdg);
+          throw std::bad_optional_access{};
+        }
+
+        if (auto chargeOpt = Acts::findCharge(pdg); chargeOpt.has_value()) {
+          charge = chargeOpt.value();
+        } else {
+          ACTS_ERROR("No charge found for PDG ID " << pdg);
+          throw std::bad_optional_access{};
+        }
+      }
+
+      if (std::isnan(mass) || std::isnan(charge)) {
+        // This particle does not have a mass or a charge based on our data
+        // table:
+        // => We can't handle it
+        continue;
+      }
+
+      SimParticle simParticle{particleId, pdg, charge, mass};
+      simParticle.initial().setPosition4(vertex.position4);
+
+      const HepMC3::FourVector& genMomentum = particle->momentum();
+      Acts::Vector3 momentum{genMomentum.px() * 1_GeV, genMomentum.py() * 1_GeV,
+                             genMomentum.pz() * 1_GeV};
+      double p = momentum.norm();
+
+      simParticle.initial().setDirection(momentum.normalized());
+      simParticle.initial().setAbsoluteMomentum(p);
+
+      particles.push_back(simParticle);
+      vertex.outgoing.insert(particleId);
+    }
+  }
+}
+
+void HepMC3InputConverter::convertHepMC3ToInternalEdm(
+    const AlgorithmContext& ctx, const HepMC3::GenEvent& genEvent) const {
+  ACTS_DEBUG("Converting HepMC3 event to internal EDM");
+
+  ACTS_VERBOSE("Have " << genEvent.vertices_size() << " vertices");
+  ACTS_VERBOSE("Have " << genEvent.particles_size() << " particles");
+
+  using VertexCluster = std::vector<HepMC3::ConstGenVertexPtr>;
+  std::vector<VertexCluster> vertexClusters;
+
+  ACTS_VERBOSE("Finding primary vertex clusters with threshold "
+               << m_cfg.primaryVertexSpatialThreshold);
+
+  // Find all vertices whose incoming particles are either all beam particles or
+  // that don't have any incoming particles
+  {
+    Acts::ScopedTimer timer("Finding primary vertex clusters", logger(),
+                            Acts::Logging::DEBUG);
+    for (const auto& vertex : genEvent.vertices()) {
+      if (vertex->particles_in().empty() ||
+          std::ranges::all_of(vertex->particles_in(), [](const auto& particle) {
+            return particle->status() == kBeamParticleStatus;
+          })) {
+        // Check if primary vertex is within tolerance of an existing primary
+        // vertex
+
+        ACTS_VERBOSE("Found primary vertex candidate at:\n"
+                     << [&]() {
+                          std::stringstream ss;
+                          HepMC3::Print::listing(ss, vertex);
+                          return ss.str();
+                        }());
+
+        auto position = convertPosition(vertex->position());
+
+        if (auto it = std::ranges::find_if(
+                vertexClusters,
+                [&](const auto& cluster) {
+                  const auto clusterPosition =
+                      convertPosition(cluster.at(0)->position());
+                  return (position - clusterPosition)
+                             .template head<3>()
+                             .cwiseAbs()
+                             .maxCoeff() < m_cfg.primaryVertexSpatialThreshold;
+                });
+            it != vertexClusters.end() && m_cfg.mergePrimaries) {
+          // Add the vertex to the cluster
+          it->push_back(vertex);
+        } else {
+          // Make a new cluster
+          vertexClusters.push_back({vertex});
+        }
+      }
+    }
+  }
+
+  ACTS_DEBUG("Found " << vertexClusters.size()
+                      << " primary vertex clusters (~ primary vertices)");
+
+  std::vector<SimVertex> verticesUnordered;
+  std::vector<SimParticle> particlesUnordered;
+
+  std::vector<bool> seenVertices;
+  seenVertices.resize(genEvent.vertices_size());
+
+  for (const auto& cluster : vertexClusters) {
+    for (const auto& vertex : cluster) {
+      seenVertices.at(std::abs(vertex->id()) - 1) = true;
+    }
+  }
+
+  std::size_t nPrimaryVertices = 0;
+  {
+    Acts::ScopedTimer timer("Converting HepMC3 vertices to internal EDM",
+                            logger(), Acts::Logging::DEBUG);
+
+    std::size_t nUndecayedParticles = 0;
+    for (const auto& particle : genEvent.particles()) {
+      if (particle->end_vertex() == nullptr) {
+        nUndecayedParticles += 1;
+      }
+    }
+    ACTS_DEBUG("Found " << nUndecayedParticles
+                        << " undecayed particles in the event");
+    ACTS_DEBUG("Reserving " << genEvent.vertices_size() / 2 << " vertices and "
+                            << nUndecayedParticles << " particles");
+    verticesUnordered.reserve(genEvent.vertices_size() / 2);
+    particlesUnordered.reserve(nUndecayedParticles);
+
+    for (auto& cluster : vertexClusters) {
+      ACTS_VERBOSE("Primary vertex cluster at "
+                   << convertPosition(cluster.at(0)->position()).transpose()
+                   << " containing " << cluster.size() << " vertices:\n"
+                   <<
+                   [&]() {
+                     std::stringstream ss;
+                     for (auto& vertex : cluster) {
+                       HepMC3::Print::listing(ss, vertex);
+                     }
+                     return ss.str();
+                   }()
+                   << "--------------");
+
+      nPrimaryVertices += 1;
+      SimVertex primaryVertex;
+      primaryVertex.id = SimVertexBarcode{}.setVertexPrimary(nPrimaryVertices);
+      primaryVertex.position4 = convertPosition(cluster.at(0)->position());
+
+      std::size_t nSecondaryVertices = 0;
+      std::size_t nParticles = 0;
+
+      for (auto& genVertex : cluster) {
+        handleVertex(*genVertex, primaryVertex, verticesUnordered,
+                     particlesUnordered, nSecondaryVertices, nParticles,
+                     seenVertices);
+      }
+      verticesUnordered.push_back(primaryVertex);
+    }
+  }
+
+  ACTS_DEBUG("Converted " << particlesUnordered.size() << " particles and "
+                          << verticesUnordered.size() << " vertices");
+
+  if (m_cfg.printListing) {
+    ACTS_VERBOSE("Converted event record:\n"
+                 << printListing(verticesUnordered, particlesUnordered));
+  }
+
+  {
+    Acts::ScopedTimer timer("Sorting particles and vertices", logger(),
+                            Acts::Logging::DEBUG);
+
+    std::ranges::sort(particlesUnordered, [](const auto& a, const auto& b) {
+      return a.particleId() < b.particleId();
+    });
+    std::ranges::sort(verticesUnordered, [](const auto& a, const auto& b) {
+      return a.vertexId() < b.vertexId();
+    });
+  }
+
+  if (m_cfg.checkConsistency) {
+    ACTS_DEBUG("Checking consistency of particles");
+    auto equalParticleIds = [](const auto& a, const auto& b) {
+      return a.particleId() == b.particleId();
+    };
+    if (auto it =
+            std::ranges::adjacent_find(particlesUnordered, equalParticleIds);
+        it != particlesUnordered.end()) {
+      ACTS_ERROR("Found duplicate particle id " << it->particleId());
+    }
+    auto equalVertexIds = [](const auto& a, const auto& b) {
+      return a.vertexId() == b.vertexId();
+    };
+    if (auto it = std::ranges::adjacent_find(verticesUnordered, equalVertexIds);
+        it != verticesUnordered.end()) {
+      ACTS_ERROR("Found duplicate vertex id " << it->vertexId());
+    }
+  }
+
+  SimParticleContainer particles{particlesUnordered.begin(),
+                                 particlesUnordered.end()};
+  SimVertexContainer vertices{verticesUnordered.begin(),
+                              verticesUnordered.end()};
+
+  // move generated event to the store
+  m_outputParticles(ctx, std::move(particles));
+  m_outputVertices(ctx, std::move(vertices));
+}
+
+}  // namespace ActsExamples
diff --git a/Examples/Io/HepMC3/src/HepMC3Reader.cpp b/Examples/Io/HepMC3/src/HepMC3Reader.cpp
index 8d9c91c9811..bfe8bb5c866 100644
--- a/Examples/Io/HepMC3/src/HepMC3Reader.cpp
+++ b/Examples/Io/HepMC3/src/HepMC3Reader.cpp
@@ -11,70 +11,136 @@
 #include "ActsExamples/Framework/WhiteBoard.hpp"
 #include "ActsExamples/Utilities/Paths.hpp"
 
+#include <HepMC3/GenEvent.h>
+#include <HepMC3/Print.h>
+#include <HepMC3/Reader.h>
+#include <HepMC3/ReaderAscii.h>
 #include <HepMC3/Units.h>
 
 namespace ActsExamples {
 
-bool HepMC3AsciiReader::readEvent(HepMC3::ReaderAscii& reader,
-                                  HepMC3::GenEvent& event) {
-  // Read event and store it
-  return reader.read_event(event);
-}
-
-bool HepMC3AsciiReader::status(HepMC3::ReaderAscii& reader) {
-  return !reader.failed();
-}
-
-HepMC3AsciiReader::HepMC3AsciiReader(const HepMC3AsciiReader::Config& cfg,
-                                     Acts::Logging::Level lvl)
-    : m_cfg(cfg),
-      m_eventsRange(
-          determineEventFilesRange(cfg.inputDir, cfg.inputStem + ".hepmc3")),
-      m_logger(Acts::getDefaultLogger("HepMC3AsciiReader", lvl)) {
-  if (m_cfg.inputStem.empty()) {
-    throw std::invalid_argument("Missing input filename stem");
-  }
-  if (m_cfg.outputEvents.empty()) {
+HepMC3Reader::HepMC3Reader(const HepMC3Reader::Config& cfg,
+                           Acts::Logging::Level lvl)
+    : m_cfg(cfg), m_logger(Acts::getDefaultLogger("HepMC3Reader", lvl)) {
+  if (m_cfg.outputEvent.empty()) {
     throw std::invalid_argument("Missing output collection");
   }
 
-  m_outputEvents.initialize(m_cfg.outputEvents);
-}
+  m_outputEvent.initialize(m_cfg.outputEvent);
+
+  if (!m_cfg.perEvent) {
+    // Create a single file reader
+    m_reader = makeReader();
+    if (m_cfg.numEvents.has_value()) {
+      m_eventsRange = {0, m_cfg.numEvents.value()};
+    } else {
+      // Need to make a temporary reader to determine the number of events
+      m_eventsRange = {0, determineNumEvents(*makeReader())};
+    }
+  } else {
+    if (m_cfg.numEvents.has_value()) {
+      throw std::invalid_argument(
+          "perEvent and numEvents are mutually exclusive");
+    }
+    m_eventsRange = determineEventFilesRange(
+        m_cfg.inputPath.parent_path(), m_cfg.inputPath.filename().string());
+  }
 
-std::string HepMC3AsciiReader::HepMC3AsciiReader::name() const {
-  return "HepMC3AsciiReader";
+  ACTS_DEBUG("HepMC3Reader: " << m_eventsRange.first << " - "
+                              << m_eventsRange.second << " events");
 }
 
-std::pair<std::size_t, std::size_t> HepMC3AsciiReader::availableEvents() const {
-  return m_eventsRange;
+std::unique_ptr<HepMC3::Reader> HepMC3Reader::makeReader() const {
+  return std::make_unique<HepMC3::ReaderAscii>(m_cfg.inputPath);
 }
 
-ProcessCode HepMC3AsciiReader::read(const AlgorithmContext& ctx) {
-  std::vector<HepMC3::GenEvent> events;
-  HepMC3::GenEvent event(HepMC3::Units::GEV, HepMC3::Units::MM);
+HepMC3Reader::~HepMC3Reader() = default;
 
-  auto path = perEventFilepath(m_cfg.inputDir, m_cfg.inputStem + ".hepmc3",
-                               ctx.eventNumber);
+std::string HepMC3Reader::name() const {
+  return "HepMC3Reader";
+}
 
-  ACTS_DEBUG("Attempting to read event from " << path);
-  HepMC3::ReaderAscii reader(path);
+std::pair<std::size_t, std::size_t> HepMC3Reader::availableEvents() const {
+  return m_eventsRange;
+}
 
-  reader.read_event(event);
+std::size_t HepMC3Reader::determineNumEvents(HepMC3::Reader& reader) const {
+  ACTS_INFO(
+      "HepMC3Reader: Number of events not specified, will read the "
+      "whole file to determine the number of events. This might take a while.");
+  std::size_t numEvents = 0;
+  auto event =
+      std::make_shared<HepMC3::GenEvent>(HepMC3::Units::GEV, HepMC3::Units::MM);
   while (!reader.failed()) {
-    events.push_back(std::move(event));
-    event.clear();
-    reader.read_event(event);
+    reader.read_event(*event);
+    if (!reader.failed()) {
+      ++numEvents;
+    }
+  }
+  return numEvents;
+}
+
+ProcessCode HepMC3Reader::read(const AlgorithmContext& ctx) {
+  ACTS_VERBOSE("Reading event " << ctx.eventNumber << " from "
+                                << m_cfg.inputPath);
+
+  auto event =
+      std::make_shared<HepMC3::GenEvent>(HepMC3::Units::GEV, HepMC3::Units::MM);
+
+  auto read = [&](HepMC3::Reader& reader) {
+    reader.read_event(*event);
+    if (reader.failed()) {
+      return ProcessCode::ABORT;
+    }
+    return ProcessCode::SUCCESS;
+  };
+
+  if (m_cfg.perEvent) {
+    std::filesystem::path perEventFile =
+        perEventFilepath(m_cfg.inputPath.parent_path(),
+                         m_cfg.inputPath.filename().string(), ctx.eventNumber);
+    ACTS_VERBOSE("Reading per-event file " << perEventFile);
+    HepMC3::ReaderAscii reader(perEventFile);
+    auto result = read(reader);
+    reader.close();
+    if (result == ProcessCode::ABORT) {
+      return ProcessCode::ABORT;
+    }
+  } else {
+    ACTS_VERBOSE("Reading from single file " << m_cfg.inputPath);
+    // Take the lock until the end of the function
+    std::scoped_lock lock(m_mutex);
+    auto result = read(*m_reader);
+    if (result == ProcessCode::ABORT) {
+      ACTS_ERROR("HepMC3Reader: Error reading event " << ctx.eventNumber << " "
+                                                      << m_cfg.inputPath);
+      if (m_cfg.numEvents.has_value()) {
+        ACTS_ERROR("HepMC3Reader: Expected " << m_cfg.numEvents.value()
+                                             << " events, but got "
+                                             << ctx.eventNumber << " events");
+      }
+      return ProcessCode::ABORT;
+    }
   }
 
-  if (events.empty()) {
-    return ProcessCode::ABORT;
+  if (m_cfg.printListing) {
+    ACTS_VERBOSE("Read event:\n"
+                 << [&]() {
+                      std::stringstream ss;
+                      HepMC3::Print::listing(ss, *event);
+                      return ss.str();
+                    }());
   }
 
-  ACTS_VERBOSE(events.size()
-               << " events read, writing to " << m_cfg.outputEvents);
-  m_outputEvents(ctx, std::move(events));
+  m_outputEvent(ctx, std::move(event));
 
-  reader.close();
+  return ProcessCode::SUCCESS;
+}
+
+ProcessCode HepMC3Reader::finalize() {
+  if (m_reader) {
+    m_reader->close();
+  }
   return ProcessCode::SUCCESS;
 }
 
diff --git a/Examples/Io/HepMC3/src/HepMC3Writer.cpp b/Examples/Io/HepMC3/src/HepMC3Writer.cpp
index 0057076392a..2660555f816 100644
--- a/Examples/Io/HepMC3/src/HepMC3Writer.cpp
+++ b/Examples/Io/HepMC3/src/HepMC3Writer.cpp
@@ -8,32 +8,40 @@
 
 #include "ActsExamples/Io/HepMC3/HepMC3Writer.hpp"
 
+#include "ActsExamples/Utilities/Paths.hpp"
+
+#include <filesystem>
+
 #include <HepMC3/WriterAscii.h>
 
 namespace ActsExamples {
 
-HepMC3AsciiWriter::HepMC3AsciiWriter(const Config& config,
-                                     Acts::Logging::Level level)
-    : WriterT(config.inputEvent, "HepMC3AsciiWriter", level), m_cfg(config) {
+HepMC3Writer::HepMC3Writer(const Config& config, Acts::Logging::Level level)
+    : WriterT(config.inputEvent, "HepMC3Writer", level), m_cfg(config) {
   if (m_cfg.outputPath.empty()) {
     throw std::invalid_argument("Missing output file path");
   }
 
-  if (auto absolute = std::filesystem::absolute(m_cfg.outputPath);
-      !std::filesystem::exists(absolute.parent_path())) {
-    throw std::invalid_argument("Output directory does not exist: " +
-                                absolute.parent_path().string());
-  }
-
   if (!m_cfg.perEvent) {
+    auto absolute = std::filesystem::absolute(m_cfg.outputPath);
+    if (std::filesystem::exists(absolute) &&
+        std::filesystem::is_directory(absolute)) {
+      throw std::invalid_argument("Output path is a directory: " +
+                                  absolute.string());
+    }
+
+    if (!std::filesystem::exists(absolute.parent_path())) {
+      throw std::invalid_argument("Directory to write into does not exist: " +
+                                  absolute.parent_path().string());
+    }
     // Create a single file writer
     m_writer = std::make_unique<HepMC3::WriterAscii>(m_cfg.outputPath);
   }
 }
 
-HepMC3AsciiWriter::~HepMC3AsciiWriter() = default;
+HepMC3Writer::~HepMC3Writer() = default;
 
-ProcessCode HepMC3AsciiWriter::writeT(
+ProcessCode HepMC3Writer::writeT(
     const AlgorithmContext& ctx,
     const std::shared_ptr<HepMC3::GenEvent>& event) {
   ACTS_VERBOSE("Writing " << event->particles().size() << " particles to "
@@ -48,11 +56,10 @@ ProcessCode HepMC3AsciiWriter::writeT(
   };
 
   if (m_cfg.perEvent) {
-    auto stem = m_cfg.outputPath.stem();
-    auto ext = m_cfg.outputPath.extension();
     std::filesystem::path perEventFile =
-        m_cfg.outputPath.parent_path() /
-        (stem.string() + "_" + std::to_string(ctx.eventNumber) + ext.string());
+        perEventFilepath(m_cfg.outputPath.parent_path(),
+                         m_cfg.outputPath.filename().string(), ctx.eventNumber);
+
     ACTS_VERBOSE("Writing per-event file " << perEventFile);
     HepMC3::WriterAscii writer(perEventFile);
     auto result = write(writer);
@@ -66,8 +73,8 @@ ProcessCode HepMC3AsciiWriter::writeT(
   }
 }
 
-ProcessCode HepMC3AsciiWriter::finalize() {
-  ACTS_VERBOSE("Finalizing HepMC3AsciiWriter");
+ProcessCode HepMC3Writer::finalize() {
+  ACTS_VERBOSE("Finalizing HepMC3Writer");
   if (m_writer) {
     m_writer->close();
   }
diff --git a/Examples/Python/CMakeLists.txt b/Examples/Python/CMakeLists.txt
index 74dd5c61600..ca7a1580503 100644
--- a/Examples/Python/CMakeLists.txt
+++ b/Examples/Python/CMakeLists.txt
@@ -191,13 +191,9 @@ if(ACTS_BUILD_EXAMPLES_GEANT4)
     list(APPEND py_files examples/geant4/__init__.py)
 endif()
 
-if(ACTS_BUILD_EXAMPLES_HEPMC3)
-    target_link_libraries(ActsPythonBindings PUBLIC ActsExamplesIoHepMC3)
-    target_sources(ActsPythonBindings PRIVATE src/HepMC3.cpp)
-    list(APPEND py_files examples/hepmc3.py)
-else()
-    target_sources(ActsPythonBindings PRIVATE src/HepMC3Stub.cpp)
-endif()
+target_link_libraries(ActsPythonBindings PUBLIC ActsExamplesIoHepMC3)
+target_sources(ActsPythonBindings PRIVATE src/HepMC3.cpp)
+list(APPEND py_files examples/hepmc3.py)
 
 if(ACTS_BUILD_EXAMPLES_EXATRKX)
     target_link_libraries(
diff --git a/Examples/Python/python/acts/examples/simulation.py b/Examples/Python/python/acts/examples/simulation.py
index 927c7228141..e4590b1bef7 100644
--- a/Examples/Python/python/acts/examples/simulation.py
+++ b/Examples/Python/python/acts/examples/simulation.py
@@ -14,6 +14,8 @@
     RootVertexWriter,
 )
 
+import acts.examples.hepmc3
+
 # Defaults (given as `None` here) use class defaults defined in
 # Examples/Algorithms/Generators/ActsExamples/Generators/ParametricParticleGenerator.hpp
 MomentumConfig = namedtuple(
@@ -161,23 +163,32 @@ def addParticleGun(
                 ),
             )
         ],
+        randomNumbers=rnd,
+        outputEvent="particle_gun_event",
+    )
+    s.addReader(evGen)
+
+    hepmc3Converter = acts.examples.hepmc3.HepMC3InputConverter(
+        level=customLogLevel(),
+        inputEvent=evGen.config.outputEvent,
         outputParticles="particles_generated",
         outputVertices="vertices_generated",
-        randomNumbers=rnd,
         mergePrimaries=False,
     )
-    s.addReader(evGen)
+    s.addAlgorithm(hepmc3Converter)
 
-    s.addWhiteboardAlias("particles", evGen.config.outputParticles)
-    s.addWhiteboardAlias("vertices_truth", evGen.config.outputVertices)
+    s.addWhiteboardAlias("particles", hepmc3Converter.config.outputParticles)
+    s.addWhiteboardAlias("vertices_truth", hepmc3Converter.config.outputVertices)
 
-    s.addWhiteboardAlias("particles_generated_selected", evGen.config.outputParticles)
+    s.addWhiteboardAlias(
+        "particles_generated_selected", hepmc3Converter.config.outputParticles
+    )
 
     if printParticles:
         s.addAlgorithm(
             ParticlesPrinter(
                 level=customLogLevel(),
-                inputParticles=evGen.config.outputParticles,
+                inputParticles=hepmc3Converter.config.outputParticles,
             )
         )
 
@@ -189,7 +200,7 @@ def addParticleGun(
         s.addWriter(
             CsvParticleWriter(
                 level=customLogLevel(),
-                inputParticles=evGen.config.outputParticles,
+                inputParticles=hepmc3Converter.config.outputParticles,
                 outputDir=str(outputDirCsv),
                 outputStem="particles",
             )
@@ -203,7 +214,7 @@ def addParticleGun(
         s.addWriter(
             RootParticleWriter(
                 level=customLogLevel(),
-                inputParticles=evGen.config.outputParticles,
+                inputParticles=hepmc3Converter.config.outputParticles,
                 filePath=str(outputDirRoot / "particles.root"),
             )
         )
@@ -211,7 +222,7 @@ def addParticleGun(
         s.addWriter(
             RootVertexWriter(
                 level=customLogLevel(),
-                inputVertices=evGen.config.outputVertices,
+                inputVertices=hepmc3Converter.config.outputVertices,
                 filePath=str(outputDirRoot / "vertices.root"),
             )
         )
@@ -236,7 +247,6 @@ def addPythia8(
     printParticles: bool = False,
     printPythiaEventListing: Optional[Union[None, str]] = None,
     writeHepMC3: Optional[Path] = None,
-    outputEvent: Optional[str] = None,
     printListing: bool = False,
     logLevel: Optional[acts.logging.Level] = None,
 ) -> None:
@@ -333,24 +343,32 @@ def addPythia8(
     evGen = acts.examples.EventGenerator(
         level=customLogLevel(),
         generators=generators,
-        outputParticles="particles_generated",
-        outputVertices="vertices_generated",
         randomNumbers=rnd,
-        outputEvent=outputEvent,
+        outputEvent="pythia8-event",
         printListing=printListing,
     )
     s.addReader(evGen)
 
-    s.addWhiteboardAlias("particles", evGen.config.outputParticles)
-    s.addWhiteboardAlias("vertices_truth", evGen.config.outputVertices)
+    hepmc3Converter = acts.examples.hepmc3.HepMC3InputConverter(
+        level=customLogLevel(),
+        inputEvent=evGen.config.outputEvent,
+        outputParticles="particles_generated",
+        outputVertices="vertices_generated",
+        printListing=printListing,
+    )
+    s.addAlgorithm(hepmc3Converter)
+    s.addWhiteboardAlias("particles", hepmc3Converter.config.outputParticles)
+    s.addWhiteboardAlias("vertices_truth", hepmc3Converter.config.outputVertices)
 
-    s.addWhiteboardAlias("particles_generated_selected", evGen.config.outputParticles)
+    s.addWhiteboardAlias(
+        "particles_generated_selected", hepmc3Converter.config.outputParticles
+    )
 
     if printParticles:
         s.addAlgorithm(
             acts.examples.ParticlesPrinter(
                 level=customLogLevel(),
-                inputParticles=evGen.config.outputParticles,
+                inputParticles=hepmc3Converter.config.outputParticles,
             )
         )
 
@@ -362,7 +380,7 @@ def addPythia8(
         s.addWriter(
             acts.examples.CsvParticleWriter(
                 level=customLogLevel(),
-                inputParticles=evGen.config.outputParticles,
+                inputParticles=hepmc3Converter.config.outputParticles,
                 outputDir=str(outputDirCsv),
                 outputStem="particles",
             )
@@ -376,7 +394,7 @@ def addPythia8(
         s.addWriter(
             acts.examples.RootParticleWriter(
                 level=customLogLevel(),
-                inputParticles=evGen.config.outputParticles,
+                inputParticles=hepmc3Converter.config.outputParticles,
                 filePath=str(outputDirRoot / "particles.root"),
             )
         )
@@ -384,7 +402,7 @@ def addPythia8(
         s.addWriter(
             acts.examples.RootVertexWriter(
                 level=customLogLevel(),
-                inputVertices=evGen.config.outputVertices,
+                inputVertices=hepmc3Converter.config.outputVertices,
                 filePath=str(outputDirRoot / "vertices.root"),
             )
         )
diff --git a/Examples/Python/src/Generators.cpp b/Examples/Python/src/Generators.cpp
index 67dffa597ca..842261b8f35 100644
--- a/Examples/Python/src/Generators.cpp
+++ b/Examples/Python/src/Generators.cpp
@@ -75,11 +75,8 @@ void addGenerators(Context& ctx) {
 
     auto config = py::class_<Config>(gen, "Config").def(py::init<>());
 
-    ACTS_PYTHON_STRUCT(config, outputParticles, outputVertices, outputEvent,
-                       generators, randomNumbers, printListing,
-                       checkConsistency, mergePrimaries,
-                       primaryVertexSpatialThreshold, vertexSpatialThreshold,
-                       mergeSecondaries);
+    ACTS_PYTHON_STRUCT(config, outputEvent, generators, randomNumbers,
+                       printListing);
   }
 
   py::class_<
diff --git a/Examples/Python/src/HepMC3.cpp b/Examples/Python/src/HepMC3.cpp
index 0aa40080301..e0539c85007 100644
--- a/Examples/Python/src/HepMC3.cpp
+++ b/Examples/Python/src/HepMC3.cpp
@@ -7,6 +7,7 @@
 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
 
 #include "Acts/Plugins/Python/Utilities.hpp"
+#include "ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp"
 #include "ActsExamples/Io/HepMC3/HepMC3OutputConverter.hpp"
 #include "ActsExamples/Io/HepMC3/HepMC3Reader.hpp"
 #include "ActsExamples/Io/HepMC3/HepMC3Writer.hpp"
@@ -26,16 +27,21 @@ void addHepMC3(Context& ctx) {
 
   auto hepmc3 = mex.def_submodule("_hepmc3");
 
-  ACTS_PYTHON_DECLARE_WRITER(ActsExamples::HepMC3AsciiWriter, hepmc3,
-                             "HepMC3AsciiWriter", outputPath, perEvent,
-                             inputEvent);
+  ACTS_PYTHON_DECLARE_WRITER(ActsExamples::HepMC3Writer, hepmc3, "HepMC3Writer",
+                             outputPath, perEvent, inputEvent);
 
-  ACTS_PYTHON_DECLARE_READER(ActsExamples::HepMC3AsciiReader, hepmc3,
-                             "HepMC3AsciiReader", inputDir, inputStem,
-                             outputEvents);
+  ACTS_PYTHON_DECLARE_READER(ActsExamples::HepMC3Reader, hepmc3, "HepMC3Reader",
+                             inputPath, perEvent, outputEvent, printListing,
+                             numEvents);
 
   ACTS_PYTHON_DECLARE_ALGORITHM(ActsExamples::HepMC3OutputConverter, hepmc3,
                                 "HepMC3OutputConverter", inputParticles,
                                 inputVertices, outputEvent);
+
+  ACTS_PYTHON_DECLARE_ALGORITHM(
+      ActsExamples::HepMC3InputConverter, hepmc3, "HepMC3InputConverter",
+      inputEvent, outputParticles, outputVertices, printListing,
+      checkConsistency, mergePrimaries, primaryVertexSpatialThreshold,
+      vertexSpatialThreshold, mergeSecondaries);
 }
 }  // namespace Acts::Python
diff --git a/Examples/Python/src/HepMC3Stub.cpp b/Examples/Python/src/HepMC3Stub.cpp
deleted file mode 100644
index 40aae062eb8..00000000000
--- a/Examples/Python/src/HepMC3Stub.cpp
+++ /dev/null
@@ -1,15 +0,0 @@
-// This file is part of the ACTS project.
-//
-// Copyright (C) 2016 CERN for the benefit of the ACTS project
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-namespace Acts::Python {
-struct Context;
-}  // namespace Acts::Python
-
-namespace Acts::Python {
-void addHepMC3(Context& /*ctx*/) {}
-}  // namespace Acts::Python
diff --git a/Examples/Python/tests/conftest.py b/Examples/Python/tests/conftest.py
index f23cba21fba..ff613b32dbd 100644
--- a/Examples/Python/tests/conftest.py
+++ b/Examples/Python/tests/conftest.py
@@ -325,14 +325,21 @@ def _factory(s):
                     ),
                 )
             ],
-            outputParticles="particles_generated",
-            outputVertices="vertices_input",
+            outputEvent="particle_gun_event",
             randomNumbers=rng,
         )
 
         s.addReader(evGen)
 
-        return evGen
+        hepmc3Converter = acts.examples.hepmc3.HepMC3InputConverter(
+            level=acts.logging.INFO,
+            inputEvent=evGen.config.outputEvent,
+            outputParticles="particles_generated",
+            outputVertices="vertices_input",
+        )
+        s.addAlgorithm(hepmc3Converter)
+
+        return evGen, hepmc3Converter
 
     return _factory
 
@@ -340,12 +347,12 @@ def _factory(s):
 @pytest.fixture
 def fatras(ptcl_gun, trk_geo, rng):
     def _factory(s):
-        evGen = ptcl_gun(s)
+        evGen, h3conv = ptcl_gun(s)
 
         field = acts.ConstantBField(acts.Vector3(0, 0, 2 * acts.UnitConstants.T))
         simAlg = acts.examples.FatrasSimulation(
             level=acts.logging.INFO,
-            inputParticles=evGen.config.outputParticles,
+            inputParticles=h3conv.config.outputParticles,
             outputParticles="particles_simulated",
             outputSimHits="simhits",
             randomNumbers=rng,
diff --git a/Examples/Python/tests/test_examples.py b/Examples/Python/tests/test_examples.py
index 332af3952c4..2a02ff5e7e1 100644
--- a/Examples/Python/tests/test_examples.py
+++ b/Examples/Python/tests/test_examples.py
@@ -183,7 +183,10 @@ def test_geant4(tmp_path, assert_root_hash):
             stderr=subprocess.STDOUT,
         )
     except subprocess.CalledProcessError as e:
-        print(e.output.decode("utf-8"))
+        if e.output is not None:
+            print(e.output.decode("utf-8"))
+        if e.stderr is not None:
+            print(e.stderr.decode("utf-8"))
         raise
 
     assert_csv_output(csv, "particles_simulated")
diff --git a/Examples/Python/tests/test_hepmc3.py b/Examples/Python/tests/test_hepmc3.py
index 907bdec7ac9..67234eeb0a2 100644
--- a/Examples/Python/tests/test_hepmc3.py
+++ b/Examples/Python/tests/test_hepmc3.py
@@ -11,6 +11,8 @@
 from acts.examples import Sequencer
 from acts import UnitConstants as u
 
+ScopedFailureThreshold = acts.logging.ScopedFailureThreshold
+
 from helpers import (
     dd4hepEnabled,
     hepmc3Enabled,
@@ -26,10 +28,9 @@
 
 
 @pytest.mark.parametrize("per_event", [True, False], ids=["per_event", "combined"])
-def test_hepmc3_particle_writer(tmp_path, rng, per_event):
+def test_hepmc3_writer(tmp_path, rng, per_event):
     from acts.examples.hepmc3 import (
-        HepMC3AsciiWriter,
-        HepMC3OutputConverter,
+        HepMC3Writer,
     )
 
     s = Sequencer(numThreads=1, events=10)
@@ -52,19 +53,35 @@ def test_hepmc3_particle_writer(tmp_path, rng, per_event):
                 ),
             )
         ],
-        outputParticles="particles_generated",
-        outputVertices="vertices_input",
         outputEvent="hepmc3_event",
         randomNumbers=rng,
     )
-
     s.addReader(evGen)
 
-    out = tmp_path / "out" / "events_pytest.hepmc3"
+    s.addAlgorithm(
+        acts.examples.hepmc3.HepMC3InputConverter(
+            level=acts.logging.DEBUG,
+            inputEvent=evGen.config.outputEvent,
+            outputParticles="particles_generated",
+            outputVertices="vertices_truth",
+        )
+    )
+
+    alg = AssertCollectionExistsAlg(
+        [
+            "particles_generated",
+            "vertices_truth",
+        ],
+        "check_alg",
+        acts.logging.WARNING,
+    )
+    s.addAlgorithm(alg)
+
+    out = tmp_path / "out" / "pytest.hepmc3"
     out.parent.mkdir(parents=True, exist_ok=True)
 
     s.addWriter(
-        HepMC3AsciiWriter(
+        HepMC3Writer(
             acts.logging.DEBUG,
             inputEvent="hepmc3_event",
             outputPath=out,
@@ -78,7 +95,7 @@ def test_hepmc3_particle_writer(tmp_path, rng, per_event):
         files = list(out.parent.iterdir())
         assert len(files) == 10
         assert all(f.suffix == ".hepmc3" for f in files)
-        assert all(f.stem.startswith("events_pytest") for f in files)
+        assert all(f.stem.startswith("event") for f in files)
         for f in files:
             with f.open("r") as f:
                 assert len(f.readlines()) == 25
@@ -102,11 +119,11 @@ def test_hepmc3_particle_writer(tmp_path, rng, per_event):
             pass
 
 
-def test_hepmc3_particle_writer_pythia8(tmp_path, rng):
+def test_hepmc3_writer_pythia8(tmp_path, rng):
     from acts.examples.hepmc3 import (
-        HepMC3AsciiWriter,
-        HepMC3OutputConverter,
+        HepMC3Writer,
     )
+
     from pythia8 import addPythia8
 
     s = Sequencer(numThreads=1, events=1)
@@ -123,30 +140,33 @@ def test_hepmc3_particle_writer_pythia8(tmp_path, rng):
         npileup=10,
         outputDirCsv=None,
         outputDirRoot=None,
-        outputEvent="hepmc3_event",
         logLevel=acts.logging.INFO,
         vtxGen=vtxGen,
     )
 
     out = tmp_path / "events.hepmc3"
 
-    # s.addAlgorithm(
-    #     HepMC3OutputConverter(
-    #         level=acts.logging.VERBOSE,
-    #         inputParticles="particles_generated",
-    #         inputVertices="vertices_generated",
-    #         outputEvents="hepmc-events",
-    #     )
-    # )
-
     s.addWriter(
-        HepMC3AsciiWriter(
+        HepMC3Writer(
             acts.logging.VERBOSE,
-            inputEvent="hepmc3_event",
+            inputEvent="pythia8-event",
             outputPath=out,
+            perEvent=False,
         )
     )
 
+    # Assert particles and vertices are present
+    alg = AssertCollectionExistsAlg(
+        [
+            "particles_generated",
+            "vertices_truth",
+            "pythia8-event",
+        ],
+        "check_alg",
+        acts.logging.WARNING,
+    )
+    s.addAlgorithm(alg)
+
     s.run()
 
     assert out.exists(), f"File {out} does not exist"
@@ -165,3 +185,236 @@ def test_hepmc3_particle_writer_pythia8(tmp_path, rng):
 
     except ImportError:
         pass
+
+
+def test_hepmc3_reader(tmp_path, rng):
+    from acts.examples.hepmc3 import (
+        HepMC3Writer,
+        HepMC3Reader,
+    )
+
+    s = Sequencer(numThreads=1, events=12)
+
+    evGen = acts.examples.EventGenerator(
+        level=acts.logging.DEBUG,
+        generators=[
+            acts.examples.EventGenerator.Generator(
+                multiplicity=acts.examples.FixedMultiplicityGenerator(n=2),
+                vertex=acts.examples.GaussianVertexGenerator(
+                    stddev=acts.Vector4(50 * u.um, 50 * u.um, 150 * u.mm, 20 * u.ns),
+                    mean=acts.Vector4(0, 0, 0, 0),
+                ),
+                particles=acts.examples.ParametricParticleGenerator(
+                    p=(100 * u.GeV, 100 * u.GeV),
+                    eta=(-2, 2),
+                    phi=(0, 360 * u.degree),
+                    randomizeCharge=True,
+                    numParticles=2,
+                ),
+            )
+        ],
+        randomNumbers=rng,
+    )
+
+    s.addReader(evGen)
+
+    out = tmp_path / "out" / "events_pytest.hepmc3"
+    out.parent.mkdir(parents=True, exist_ok=True)
+
+    s.addWriter(
+        HepMC3Writer(
+            acts.logging.DEBUG,
+            inputEvent="hepmc3_event",
+            outputPath=out,
+            perEvent=False,
+        )
+    )
+
+    s.run()
+
+    assert out.exists(), f"File {out} does not exist"
+
+    # Without external event number, we need to read all events
+    s = Sequencer(numThreads=1)
+
+    s.addReader(
+        HepMC3Reader(
+            acts.logging.DEBUG,
+            inputPath=out,
+            perEvent=False,
+            outputEvent="hepmc3_event",
+        )
+    )
+
+    s.addAlgorithm(
+        acts.examples.hepmc3.HepMC3InputConverter(
+            level=acts.logging.DEBUG,
+            inputEvent="hepmc3_event",
+            outputParticles="particles_read",
+            outputVertices="vertices_read",
+        )
+    )
+
+    alg = AssertCollectionExistsAlg(
+        [
+            "particles_read",
+            "vertices_read",
+            "hepmc3_event",
+        ],
+        "check_alg",
+        acts.logging.WARNING,
+    )
+    s.addAlgorithm(alg)
+
+    s.run()
+
+    assert alg.events_seen == 12
+
+    # With external event number, we can read a specific event
+    # Test 11 and 12 as both a lower number than is available and a higher number
+    # than is available is valid
+    for num in (11, 12):
+        s = Sequencer(numThreads=1)
+
+        s.addReader(
+            HepMC3Reader(
+                acts.logging.DEBUG,
+                inputPath=out,
+                perEvent=False,
+                outputEvent="hepmc3_event",
+                numEvents=num,
+            )
+        )
+
+        alg = AssertCollectionExistsAlg(
+            "hepmc3_event", "check_alg", acts.logging.WARNING
+        )
+        s.addAlgorithm(alg)
+
+        s.run()
+
+        assert alg.events_seen == num
+
+    s = Sequencer(numThreads=1)
+
+    s.addReader(
+        HepMC3Reader(
+            acts.logging.DEBUG,
+            inputPath=out,
+            perEvent=False,
+            outputEvent="hepmc3_event",
+            numEvents=13,
+        )
+    )
+
+    with ScopedFailureThreshold(acts.logging.MAX):
+        with pytest.raises(RuntimeError) as excinfo:
+            s.run()
+
+    assert "Failed to process event" in str(excinfo.value)
+
+
+def test_hepmc3_reader_per_event(tmp_path, rng):
+    from acts.examples.hepmc3 import (
+        HepMC3Writer,
+        HepMC3Reader,
+    )
+
+    s = Sequencer(numThreads=1, events=12)
+
+    evGen = acts.examples.EventGenerator(
+        level=acts.logging.DEBUG,
+        generators=[
+            acts.examples.EventGenerator.Generator(
+                multiplicity=acts.examples.FixedMultiplicityGenerator(n=2),
+                vertex=acts.examples.GaussianVertexGenerator(
+                    stddev=acts.Vector4(50 * u.um, 50 * u.um, 150 * u.mm, 20 * u.ns),
+                    mean=acts.Vector4(0, 0, 0, 0),
+                ),
+                particles=acts.examples.ParametricParticleGenerator(
+                    p=(100 * u.GeV, 100 * u.GeV),
+                    eta=(-2, 2),
+                    phi=(0, 360 * u.degree),
+                    randomizeCharge=True,
+                    numParticles=2,
+                ),
+            )
+        ],
+        outputEvent="hepmc3_event",
+        randomNumbers=rng,
+    )
+
+    s.addReader(evGen)
+
+    s.addAlgorithm(
+        acts.examples.hepmc3.HepMC3InputConverter(
+            level=acts.logging.DEBUG,
+            inputEvent="hepmc3_event",
+            outputParticles="particles_generated",
+            outputVertices="vertices_generated",
+        )
+    )
+    alg = AssertCollectionExistsAlg(
+        [
+            "particles_generated",
+            "vertices_generated",
+            "hepmc3_event",
+        ],
+        "check_alg",
+        acts.logging.WARNING,
+    )
+    s.addAlgorithm(alg)
+
+    out = tmp_path / "out" / "pytest.hepmc3"
+    out.parent.mkdir(parents=True, exist_ok=True)
+
+    s.addWriter(
+        HepMC3Writer(
+            acts.logging.DEBUG,
+            inputEvent="hepmc3_event",
+            outputPath=out,
+            perEvent=True,
+        )
+    )
+
+    s.run()
+
+    assert alg.events_seen == 12
+
+    s = Sequencer(numThreads=1)
+
+    print(out)
+
+    import os
+
+    os.system(f"ls -l {out.parent}")
+    # assert False
+
+    s.addReader(
+        HepMC3Reader(
+            acts.logging.DEBUG,
+            inputPath=out,
+            perEvent=True,
+            outputEvent="hepmc3_event",
+        )
+    )
+
+    s.run()
+
+
+def test_hepmc3_reader_per_event_with_num_events(tmp_path, rng):
+    from acts.examples.hepmc3 import (
+        HepMC3Reader,
+    )
+
+    # Test that using both perEvent=True and numEvents raises an error
+    with pytest.raises(ValueError) as excinfo:
+        HepMC3Reader(
+            acts.logging.DEBUG,
+            inputPath="dummy.hepmc3",
+            perEvent=True,
+            outputEvent="hepmc3_event",
+            numEvents=5,
+        )
+
+    assert "perEvent and numEvents are mutually exclusive" in str(excinfo.value)
diff --git a/Examples/Python/tests/test_reader.py b/Examples/Python/tests/test_reader.py
index 928ec8e5d35..5cabaed0d04 100644
--- a/Examples/Python/tests/test_reader.py
+++ b/Examples/Python/tests/test_reader.py
@@ -31,14 +31,14 @@
 def test_root_particle_reader(tmp_path, conf_const, ptcl_gun):
     # need to write out some particles first
     s = Sequencer(numThreads=1, events=10, logLevel=acts.logging.WARNING)
-    evGen = ptcl_gun(s)
+    _, h3conv = ptcl_gun(s)
 
     file = tmp_path / "particles.root"
     s.addWriter(
         conf_const(
             RootParticleWriter,
             acts.logging.WARNING,
-            inputParticles=evGen.config.outputParticles,
+            inputParticles=h3conv.config.outputParticles,
             filePath=str(file),
         )
     )
@@ -71,7 +71,7 @@ def test_root_particle_reader(tmp_path, conf_const, ptcl_gun):
 @pytest.mark.csv
 def test_csv_particle_reader(tmp_path, conf_const, ptcl_gun):
     s = Sequencer(numThreads=1, events=10, logLevel=acts.logging.WARNING)
-    evGen = ptcl_gun(s)
+    _, h3conv = ptcl_gun(s)
 
     out = tmp_path / "csv"
 
@@ -81,7 +81,7 @@ def test_csv_particle_reader(tmp_path, conf_const, ptcl_gun):
         conf_const(
             CsvParticleWriter,
             acts.logging.WARNING,
-            inputParticles=evGen.config.outputParticles,
+            inputParticles=h3conv.config.outputParticles,
             outputStem="particle",
             outputDir=str(out),
         )
@@ -446,14 +446,14 @@ def test_buffered_reader(tmp_path, conf_const, ptcl_gun):
     eventsToProcess = 10
 
     s = Sequencer(numThreads=1, events=eventsInBuffer, logLevel=acts.logging.WARNING)
-    evGen = ptcl_gun(s)
+    _, h3conv = ptcl_gun(s)
 
     file = tmp_path / "particles.root"
     s.addWriter(
         conf_const(
             RootParticleWriter,
             acts.logging.WARNING,
-            inputParticles=evGen.config.outputParticles,
+            inputParticles=h3conv.config.outputParticles,
             filePath=str(file),
         )
     )
diff --git a/Examples/Python/tests/test_writer.py b/Examples/Python/tests/test_writer.py
index 7c763d587af..ddc3a872eeb 100644
--- a/Examples/Python/tests/test_writer.py
+++ b/Examples/Python/tests/test_writer.py
@@ -73,7 +73,7 @@ def test_obj_propagation_step_writer(tmp_path, trk_geo, conf_const, basic_prop_s
 @pytest.mark.csv
 def test_csv_particle_writer(tmp_path, conf_const, ptcl_gun):
     s = Sequencer(numThreads=1, events=10)
-    evGen = ptcl_gun(s)
+    _, h3conv = ptcl_gun(s)
 
     out = tmp_path / "csv"
 
@@ -83,7 +83,7 @@ def test_csv_particle_writer(tmp_path, conf_const, ptcl_gun):
         conf_const(
             CsvParticleWriter,
             acts.logging.INFO,
-            inputParticles=evGen.config.outputParticles,
+            inputParticles=h3conv.config.outputParticles,
             outputStem="particle",
             outputDir=str(out),
         )
@@ -125,7 +125,7 @@ def test_root_prop_step_writer(
 @pytest.mark.root
 def test_root_particle_writer(tmp_path, conf_const, ptcl_gun, assert_root_hash):
     s = Sequencer(numThreads=1, events=10)
-    evGen = ptcl_gun(s)
+    _, h3conv = ptcl_gun(s)
 
     file = tmp_path / "particles.root"
 
@@ -135,7 +135,7 @@ def test_root_particle_writer(tmp_path, conf_const, ptcl_gun, assert_root_hash):
         conf_const(
             RootParticleWriter,
             acts.logging.INFO,
-            inputParticles=evGen.config.outputParticles,
+            inputParticles=h3conv.config.outputParticles,
             filePath=str(file),
         )
     )
@@ -495,7 +495,7 @@ def test_edm4hep_particle_writer(tmp_path, conf_const, ptcl_gun):
     from acts.examples.edm4hep import EDM4hepParticleWriter
 
     s = Sequencer(numThreads=1, events=10)
-    evGen = ptcl_gun(s)
+    _, h3conv = ptcl_gun(s)
 
     out = tmp_path / "particles_edm4hep.root"
 
@@ -505,7 +505,7 @@ def test_edm4hep_particle_writer(tmp_path, conf_const, ptcl_gun):
         conf_const(
             EDM4hepParticleWriter,
             acts.logging.INFO,
-            inputParticles=evGen.config.outputParticles,
+            inputParticles=h3conv.config.outputParticles,
             outputPath=str(out),
         )
     )
diff --git a/Examples/Scripts/Python/material_recording.py b/Examples/Scripts/Python/material_recording.py
index c7ad6d47b22..45f0a56c7ba 100755
--- a/Examples/Scripts/Python/material_recording.py
+++ b/Examples/Scripts/Python/material_recording.py
@@ -63,18 +63,25 @@ def runMaterialRecording(
                 ),
             )
         ],
-        outputParticles="particles_initial",
-        outputVertices="vertices_initial",
         randomNumbers=rnd,
     )
 
     s.addReader(evGen)
 
+    hepmc3Converter = acts.examples.hepmc3.HepMC3InputConverter(
+        level=acts.logging.INFO,
+        inputEvent=evGen.config.outputEvent,
+        outputParticles="particles_initial",
+        outputVertices="vertices_initial",
+        mergePrimaries=False,
+    )
+    s.addAlgorithm(hepmc3Converter)
+
     g4Alg = acts.examples.geant4.Geant4MaterialRecording(
         level=acts.logging.INFO,
         detector=detector,
         randomNumbers=rnd,
-        inputParticles=evGen.config.outputParticles,
+        inputParticles=hepmc3Converter.config.outputParticles,
         outputMaterialTracks="material-tracks",
     )
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>Examples/Python/python/acts/examples/simulation.py (1)</summary><blockquote>

`342-350`: **Replaced EventGenerator with dedicated HepMC3InputConverter, wise refactoring this is.**

The decoupling of HepMC3 conversion from EventGenerator, implemented you have. Now a dedicated `HepMC3InputConverter` processes the events, hmm. Better separation of concerns this creates. 

Hard-coded DEBUG level I see. Consider using `customLogLevel()` instead for consistency with rest of codebase.

```diff
-    level=acts.logging.DEBUG,
+    level=customLogLevel(),
```

</blockquote></details>
<details>
<summary>Examples/Python/tests/test_logging.py (1)</summary><blockquote>

`15-20`: **Exception handling in context manager, wisely tested.**

Good to test both normal path and exception path, it is. Ensure threshold restoration even when errors occur, this does. Improve code style, we can.

```diff
-    with pytest.raises(RuntimeError):
-        with acts.logging.ScopedFailureThreshold(level=acts.logging.ERROR):
+    with pytest.raises(RuntimeError), acts.logging.ScopedFailureThreshold(level=acts.logging.ERROR):
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

16-17: Use a single `with` statement with multiple contexts instead of nested `with` statements

(SIM117)

</details>

</details>

</blockquote></details>
<details>
<summary>Examples/Io/HepMC3/src/HepMC3InputConverter.cpp (1)</summary><blockquote>

`17-29`: **Powerful hack, “private” redefined as public.**  
Potentially fragile, yes. If library updates break this hack, need an alternative approach, we might.

</blockquote></details>
<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp (1)</summary><blockquote>

`58-58`: **Implementing interface method, you are.**  
Proper override, it is. Additional doc comment for usage clarity, advise I do.

</blockquote></details>
<details>
<summary>Examples/Python/tests/test_hepmc3.py (2)</summary><blockquote>

`311-312`: **Nest not your with statements, combine them you should.**  
Readability improved it will be, by merging.  


Suggested refactor:
```diff
-    with ScopedFailureThreshold(acts.logging.MAX):
-        with pytest.raises(RuntimeError) as excinfo:
+    with ScopedFailureThreshold(acts.logging.MAX), pytest.raises(RuntimeError) as excinfo:
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

311-312: Use a single `with` statement with multiple contexts instead of nested `with` statements

(SIM117)

</details>

</details>

---

`389-389`: **Redefinition of 'os', unneeded duplication, it is.**  
One import above you have. Remove this one, you should.  


Proposed removal:
```diff
-import os
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

389-389: Redefinition of unused `os` from line 5

Remove definition: `os`

(F811)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f8325bc940e7e5a718a0d48829670bed565c9a83 and 678cb22d4b9680e86ac28c50838b3f265ecc2ad9.

</details>

<details>
<summary>📒 Files selected for processing (24)</summary>

* `Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.cpp` (2 hunks)
* `Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.hpp` (0 hunks)
* `Examples/Framework/src/Utilities/Paths.cpp` (1 hunks)
* `Examples/Io/HepMC3/CMakeLists.txt` (1 hunks)
* `Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Event.hpp` (0 hunks)
* `Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp` (1 hunks)
* `Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Particle.hpp` (0 hunks)
* `Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp` (3 hunks)
* `Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Util.hpp` (1 hunks)
* `Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Vertex.hpp` (0 hunks)
* `Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Writer.hpp` (2 hunks)
* `Examples/Io/HepMC3/src/HepMC3Event.cpp` (0 hunks)
* `Examples/Io/HepMC3/src/HepMC3InputConverter.cpp` (1 hunks)
* `Examples/Io/HepMC3/src/HepMC3Particle.cpp` (0 hunks)
* `Examples/Io/HepMC3/src/HepMC3Reader.cpp` (1 hunks)
* `Examples/Io/HepMC3/src/HepMC3Util.cpp` (1 hunks)
* `Examples/Io/HepMC3/src/HepMC3Vertex.cpp` (0 hunks)
* `Examples/Io/HepMC3/src/HepMC3Writer.cpp` (3 hunks)
* `Examples/Python/python/acts/examples/simulation.py` (3 hunks)
* `Examples/Python/src/Base.cpp` (1 hunks)
* `Examples/Python/src/Generators.cpp` (1 hunks)
* `Examples/Python/src/HepMC3.cpp` (2 hunks)
* `Examples/Python/tests/test_hepmc3.py` (7 hunks)
* `Examples/Python/tests/test_logging.py` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (7)</summary>

* Examples/Io/HepMC3/src/HepMC3Vertex.cpp
* Examples/Io/HepMC3/src/HepMC3Event.cpp
* Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.hpp
* Examples/Io/HepMC3/src/HepMC3Particle.cpp
* Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Vertex.hpp
* Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Particle.hpp
* Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Event.hpp

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (6)</summary>

<details>
<summary>Examples/Python/src/Base.cpp (1)</summary><blockquote>

<details>
<summary>Core/include/Acts/Utilities/Logger.hpp (1)</summary>

* `m_level` (341-341)

</details>

</blockquote></details>
<details>
<summary>Examples/Io/HepMC3/src/HepMC3InputConverter.cpp (2)</summary><blockquote>

<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp (5)</summary>

* `HepMC3InputConverter` (54-54)
* `m_cfg` (56-56)
* `ctx` (58-58)
* `ctx` (61-62)
* `genVertex` (64-68)

</details>
<details>
<summary>Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.hpp (3)</summary>

* `m_cfg` (120-120)
* `ctx` (117-117)
* `genVertex` (125-129)

</details>

</blockquote></details>
<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Writer.hpp (1)</summary><blockquote>

<details>
<summary>Examples/Io/HepMC3/src/HepMC3Writer.cpp (2)</summary>

* `HepMC3Writer` (19-41)
* `HepMC3Writer` (43-43)

</details>

</blockquote></details>
<details>
<summary>Examples/Python/tests/test_hepmc3.py (6)</summary><blockquote>

<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Writer.hpp (2)</summary>

* `HepMC3Writer` (55-55)
* `HepMC3Writer` (57-57)

</details>
<details>
<summary>Examples/Io/HepMC3/src/HepMC3Writer.cpp (2)</summary>

* `HepMC3Writer` (19-41)
* `HepMC3Writer` (43-43)

</details>
<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp (1)</summary>

* `HepMC3InputConverter` (54-54)

</details>
<details>
<summary>Examples/Io/HepMC3/src/HepMC3InputConverter.cpp (1)</summary>

* `HepMC3InputConverter` (34-40)

</details>
<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp (2)</summary>

* `HepMC3Reader` (63-63)
* `HepMC3Reader` (65-65)

</details>
<details>
<summary>Examples/Io/HepMC3/src/HepMC3Reader.cpp (2)</summary>

* `HepMC3Reader` (22-51)
* `HepMC3Reader` (57-57)

</details>

</blockquote></details>
<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp (2)</summary><blockquote>

<details>
<summary>Examples/Io/HepMC3/src/HepMC3Reader.cpp (3)</summary>

* `HepMC3Reader` (22-51)
* `HepMC3Reader` (57-57)
* `reader` (103-103)

</details>
<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Writer.hpp (1)</summary>

* `m_cfg` (71-71)

</details>

</blockquote></details>
<details>
<summary>Examples/Io/HepMC3/src/HepMC3Writer.cpp (3)</summary><blockquote>

<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Writer.hpp (4)</summary>

* `HepMC3Writer` (55-55)
* `HepMC3Writer` (57-57)
* `m_cfg` (71-71)
* `ctx` (65-66)

</details>
<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3OutputConverter.hpp (2)</summary>

* `m_cfg` (34-34)
* `ctx` (36-36)

</details>
<details>
<summary>Examples/Io/HepMC3/src/HepMC3Reader.cpp (2)</summary>

* `finalize` (140-145)
* `finalize` (140-140)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>Examples/Python/tests/test_logging.py</summary>

16-17: Use a single `with` statement with multiple contexts instead of nested `with` statements

(SIM117)

</details>
<details>
<summary>Examples/Python/tests/test_hepmc3.py</summary>

311-312: Use a single `with` statement with multiple contexts instead of nested `with` statements

(SIM117)

---

389-389: Redefinition of unused `os` from line 5

Remove definition: `os`

(F811)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: merge-sentinel

</details>

<details>
<summary>🔇 Additional comments (73)</summary><blockquote>

<details>
<summary>Examples/Framework/src/Utilities/Paths.cpp (1)</summary>

`73-73`: **Fixed grammatical error in exception message, I see.**

Correct error message now you have. From "does not exists" to "does not exist" the change is. Simple but important, grammar is, hmm!

</details>
<details>
<summary>Examples/Io/HepMC3/CMakeLists.txt (1)</summary>

`6-6`: **New source files added to library, hmm!**

To the shared library, two new source files added you have. The `HepMC3InputConverter.cpp` and `HepMC3Util.cpp`, part of the decoupling effort they are. Replacing the old event, particle, and vertex conversion files, these new components do. A good refactor this is, yes!



Also applies to: 8-8

</details>
<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Util.hpp (1)</summary>

`1-28`: **New utility header for HepMC3 event merging, well structured it is.**

A clean and focused utility header, created you have. The `mergeEvents` function, specialized for combining multiple HepMC3 events, valuable it will be. Forward declarations wisely used, hmm. Good practice this is, avoid unnecessary includes it does.

Match the PR objectives, this file does - decouple EvtGen from HepMC3 conversion by moving functionality to specialized components.

</details>
<details>
<summary>Examples/Python/python/acts/examples/simulation.py (5)</summary>

`351-356`: **Whiteboard aliases updated correctly for new converter.**

Properly updated, the whiteboard aliases are. Now they point to the converter's outputs instead of generator's outputs. Consistency maintained this ensures throughout the simulation chain.

---

`362-363`: **ParticlesPrinter input source updated correctly.**

Changed from evGen to hepmc3Converter, the input source for ParticlesPrinter has been. Appropriate this change is, follows the refactoring pattern.

---

`374-375`: **CsvParticleWriter input source updated correctly.**

For the CsvParticleWriter, updated the input source you have. From generator to converter it now points. Consistent with the entire refactoring, this change is.

---

`388-389`: **RootParticleWriter input source updated correctly.**

For the RootParticleWriter as well, the input source properly updated it is. Consistent throughout all writers, hmm, good practice this shows.

---

`396-397`: **RootVertexWriter input source updated correctly.**

Last but not least, RootVertexWriter's input source updated correctly, yes. Complete and thorough the refactoring is, missed no references to old structure.

</details>
<details>
<summary>Examples/Io/HepMC3/src/HepMC3Util.cpp (5)</summary>

`1-8`: **Proper license header, this has.**

Good practice followed, the standard ACTS project license header is. Approved, this is.

---

`19-23`: **Well implemented timer functionality, this is.**

Wise use of `Acts::AveragingScopedTimer`, hmm! Performance measurement during event merging, it provides. Good logging level for debugging purposes, DEBUG is.

---

`34-40`: **Clever lambda definition for attribute copying, I see.**

Elegant solution for code reuse within the function, this lambda is. Avoid repetition, it does. The way of the Jedi, this is.

---

`45-59`: **Validate ID consistency, you must.**

Check for consecutive particle IDs, you do. Good defensive programming, this is. Runtime error thrown when expectations not met. Safe handling of particle properties and storage, I observe.

---

`61-77`: **Proper vertex handling and particle linking, you have implemented.**

Preserve the relationships between vertices and particles, this code does. Access particles by their IDs, it does. Strong attention to detail, you have shown.

</details>
<details>
<summary>Examples/Python/src/Base.cpp (2)</summary>

`211-225`: **Well structured context manager, this is.**

The `ScopedFailureThresholdContextManager` class, clean implementation it has. Optional threshold with proper reset functionality in exit method, I see. Scope properly managed, it is.

---

`227-231`: **Python bindings well exposed, they are.**

Python context manager protocol correctly implemented with `__enter__` and `__exit__`, hmm! Pythonic way this follows. Named parameters used wisely.

</details>
<details>
<summary>Examples/Python/src/Generators.cpp (1)</summary>

`78-79`: **Simplified configuration structure, I see.**

From eleven parameters to four, reduced this has been. Align with PR objective of decoupling EvtGen from HepMC3 conversion, this does. Only necessary parameters kept: `outputEvent`, `generators`, `randomNumbers`, and `printListing`.

</details>
<details>
<summary>Examples/Python/tests/test_logging.py (2)</summary>

`1-7`: **Verify default threshold setting, this test does.**

Asserts that WARNING is the default threshold level. Simple but effective test, this is.

---

`9-13`: **Context manager behavior, correctly tested this is.**

Verify threshold changes inside context and restores after exit, this test does. Clear assertion of expected behavior.

</details>
<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Writer.hpp (5)</summary>

`16-16`: **Add concurrency, do you? Good, hmm.**  
A wise choice including `<mutex>` for thread-safe writing it is. Coordination in multi-threaded environments, ensure it does.

---

`27-27`: **Declare it final, you do.**  
Clarity in preventing further inheritance, best practice it can be.

---

`34-44`: **Improved docs, clarity they bring.**  
Yes, detailed explanation of `outputPath` usage helps those who read. Merge this, you should.

---

`55-55`: **Constructor design, well-structured it is.**  
Throws an exception for missing path or invalid config, correct approach indeed.

---

`57-57`: **Destructor override, consistent with polymorphic usage it is.**  
No issues found, proceed you may.

</details>
<details>
<summary>Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.cpp (2)</summary>

`17-17`: **New utility included, helpful it shall be.**  
Use `HepMC3Util.hpp` for merging events, quite streamlined it appears.

---

`139-139`: **Event merging logic, look good it does.**  
Merged events, further transformations outside this scope, you handle.

</details>
<details>
<summary>Examples/Io/HepMC3/src/HepMC3Writer.cpp (7)</summary>

`11-14`: **Include statements, updated they are.**  
Paths utilities added, consistent with writing approach, yes.

---

`19-20`: **Constructor checks, robust they seem.**  
Validate the `outputPath` thoroughly, good practice it is. Traps no one shall fall into.

---

`26-36`: **Directory checks, wise caution show they do.**  
Yes, verifying path isn't a directory, user confusion it prevents.

---

`43-43`: **Destructor, default and clean it is.**  
Cleanup ephemeral resources with ease, you do. No concerns here.

---

`45-74`: **Thread-safety in writeT, well-handled.**  
Scoped lock usage for single-file writing is prudent, yes. Per-event mode also well considered.

---

`61-63`: **Per-event path generation, properly you handle.**  
Clear naming for `perEventFilepath`, it fosters.

---

`77-78`: **Finalize method, stable and neat.**  
Closing and cleaning up writer references, done it is.

</details>
<details>
<summary>Examples/Python/src/HepMC3.cpp (4)</summary>

`10-10`: **Useful new include, this is.**  
Mmm, references the input converter needed for decoupling from the event generator, it does. Enhances modularity, yes. No issues, see I do.

---

`30-31`: **Clear writer declaration, you have.**  
Parameters named well, they are. The usage consistent with the codebase’s macro approach, it looks. No further concerns, have I.

---

`40-45`: **Excellent introduction of HepMC3InputConverter algorithm.**  
Streamlines the event flow, it does. If the naming consistent with pipeline, verify you should.

---

`33-35`: <details>
<summary>✅ Verification successful</summary>

**Good addition: new reader.**  
Matches the writer’s approach, does it. If consistent with up/downstream code usage, confirm you must.  

Run a quick search for “HepMC3Reader” references in the codebase, we should.

---

🏁 Script executed:

```shell
#!/bin/bash
# Searching references to ensure consistent usage, yes
rg "HepMC3Reader"
```

Length of output: 2576

---



</details>

**Consistent integration, this new reader is—approve the change, we must.**

Verified, the "HepMC3Reader" is consistently used across the codebase, including tests and CMake files. No removed or replaced code observed in the snippet; the approach aligns with up/downstream usage.

</details>
<details>
<summary>Examples/Io/HepMC3/src/HepMC3InputConverter.cpp (3)</summary>

`40-45`: **Constructor well structured, it is.**  
All data handles properly initialized, they appear to be. Good clarity, see I do.

---

`42-52`: **Execute method correct, it seems.**  
Yet mindful be of error handling for missing or invalid events. Currently safe to assume `m_inputEvent` always valid, is it?

---

`211-379`: **Conversion method elegantly merges vertices, though handle edge cases you must.**  
Potential mismatch between `genEvent.vertices_size()` and vertex IDs, there is. Defensive approach, wise it would be. Code is fine for typical usage, yes.

</details>
<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp (2)</summary>

`26-28`: **Class definition well placed, it is.**  
Clear domain separation, see I do. The public config structure is straightforward, yes.

---

`70-78`: **Private members, well structured they are.**  
Naming consistent with the framework, yes. Implementation detail, you keep separated from config, good.

</details>
<details>
<summary>Examples/Python/tests/test_hepmc3.py (12)</summary>

`14-15`: **Use of ScopedFailureThreshold is aligned with advanced logging, hmmm.**  
A good approach to specify a maximum failure threshold, it is.

---

`30-31`: **Nicely parametrized test, hrrrm.**  
Helpful it is to verify both per-event and combined writing with a single test function.

---

`33-33`: **Import of HepMC3Writer consistent with the new code structure, yes.**  
All references properly updated, I see. Continue, you should.

---

`61-69`: **HepMC3InputConverter addition, correct it seems.**  
Essential is this converter for reading and converting events for further processing.

---

`70-78`: **Assertion of collections, hrrrm.**  
Ensures pipeline readiness by verifying essential data products, your test does.

---

`80-80`: **Output path updated to 'pytest.hepmc3', yes.**  
Clearer naming, it provides.

---

`84-84`: **New usage of HepMC3Writer, good.**  
The single-writer approach is simpler, it is.

---

`98-98`: **Check for per-event file prefix, implemented well, hmm.**  
Ensures correct naming and separation of event files.

---

`122-126`: **Pythia8 writer test, added it is.**  
Valuable coverage for external generator integration, indeed.

---

`151-155`: **Writing with perEvent=False, consistent with the desired single-file approach, hmmm.**  
No issues found, see I do.

---

`159-170`: **Assertion of presence for multiple collections, correct it is.**  
Verifies that the pipeline has placed correct data in each collection.

---

`191-232`: **Comprehensive test_hepmc3_reader function, yes.**  
Covers multi-event reading, partial reading, and error handling thoroughly, it does.

</details>
<details>
<summary>Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp (11)</summary>

`15-17`: **New includes for filesystem, mutex, and string, handle extra functionalities they do, hmm.**  
Valuable for concurrency guard and path handling, these are.

---

`19-22`: **Forward declarations of HepMC3 classes, wise technique for compile-time efficiency, yes.**  
Minimize dependencies in the header, you do. Good practice.

---

`27-27`: **Class declaration with final keyword, hrrrm.**  
Indicates no further inheritance, clarity it provides.

---

`30-56`: **Config struct well-defined, strong compatibility with per-event and single-file usage, yes.**  
Comprehensively covers user options, it does.

---

`63-63`: **Constructor signature, matches refactored design it does.**  
All mandatory config items passed in—straightforward usage, I see.

---

`65-65`: **Destructor override, yes.**  
Explicitly declared for clarity, if custom or future resource handling needed, you have.

---

`75-75`: **Finalize method introduced, excellent.**  
Allows closure or final checks, it does.

---

`82-83`: **Helper functions determineNumEvents and makeReader.**  
A modular approach for file reading logic, you use.

---

`94-95`: **Use of WriteDataHandle for output, yes.**  
A structured approach for data passing, consistent with the framework, it is.

---

`97-97`: **Mutex added, concurrency concerns addressed, indeed.**  
Protecting the shared reader, the code is.

---

`99-99`: **Unique pointer to HepMC3::Reader, well-chosen.**  
Proper resource management fosters safety, yes.

</details>
<details>
<summary>Examples/Io/HepMC3/src/HepMC3Reader.cpp (9)</summary>

`14-19`: **Inclusion of GenEvent, Print, Reader, and ReaderAscii, correct for extended usage, hrrrm.**  
Yes, essential each for reading logic.

---

`22-51`: **Constructor logic thorough, robust it is.**  
Distinguishing between single-file and per-event reading and verifying arguments, you do.

---

`53-55`: **Factory method makeReader, wise approach.**  
Centralizes creation of the ReaderAscii, easy to expand or mock, yes.

---

`57-57`: **Destructor defaulted, mmh.**  
Concise and neat it is, no issues see I do.

---

`59-61`: **Name method returning 'HepMC3Reader', consistent with the class, hrrrm.**  
Maintains clarity by matching the actual type.

---

`63-66`: **availableEvents method, returning the stored range, yes.**  
Clear means to retrieve event boundaries, it provides.

---

`67-81`: **determineNumEvents logic, yes.**  
Allows the file to be fully scanned if the event count is unknown, though performance overhead it may cause.

---

`83-138`: **read method, elegantly handles single vs. per-event reading, yes.**  
Locks the Reader for concurrency, and checks for read failures, thorough you are.

---

`140-145`: **finalize method safely closing the reader, correct.**  
Resource management is properly done, hrrrm.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-11T14:26:37Z
user: login: andiwand
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T11:54:05Z
user: login: paulgessinger
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T13:23:57Z
user: login: andiwand
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T13:30:45Z
user: login: andiwand
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T06:45:53Z

Review comments:
diff_hunk: @@ -0,0 +1,380 @@
+// This file is part of the ACTS project.
+//
+// Copyright (C) 2016 CERN for the benefit of the ACTS project
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#include "ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp"
+
+#include "Acts/Utilities/ScopedTimer.hpp"
+
+#include <HepMC3/GenEvent.h>
+#include <HepMC3/GenParticle.h>
+#include <HepMC3/GenVertex.h>
+
+// This is a hack to make HepMC3::Print::listing public
+// It's pretty evil but should have no side-effects
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wkeyword-macro"
+#endif
+#define private public
+#include <HepMC3/Print.h>
+#undef private
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+
+using namespace Acts::UnitLiterals;
+
+namespace ActsExamples {
+
+HepMC3InputConverter::HepMC3InputConverter(const Config& config,
+                                           Acts::Logging::Level level)
+    : IAlgorithm("HepMC3InputConverter", level), m_cfg(config) {
+  m_outputParticles.initialize(m_cfg.outputParticles);
+  m_outputVertices.initialize(m_cfg.outputVertices);
+  m_inputEvent.initialize(m_cfg.inputEvent);
+}
+
+ProcessCode HepMC3InputConverter::execute(const AlgorithmContext& ctx) const {
+  ACTS_DEBUG("HepMC3InputConverter::execute");
+  const auto& genEvent = *m_inputEvent(ctx);
+  ACTS_DEBUG("Have " << genEvent.vertices_size() << " vertices");
+  ACTS_DEBUG("Have " << genEvent.particles_size() << " particles");
+  ACTS_DEBUG("Have " << genEvent.event_number() << " event number");
+
+  convertHepMC3ToInternalEdm(ctx, genEvent);
+
+  return ProcessCode::SUCCESS;
+}
+
+namespace {
+
+constexpr int kBeamParticleStatus = 4;
+constexpr int kUndecayedParticleStatus = 1;
+
+Acts::Vector4 convertPosition(const HepMC3::FourVector& vec) {
+  return Acts::Vector4(vec.x() * 1_mm, vec.y() * 1_mm, vec.z() * 1_mm,
+                       vec.t() * 1_mm);
+}
+
+std::string printListing(const auto& vertices, const auto& particles) {
+  auto findParticle = [&](SimBarcode particleId) {
+    if (auto it = std::ranges::find_if(particles,
+                                       [&](const auto& particle) {
+                                         return particle.particleId() ==
+                                                particleId;
+                                       });
+        it != particles.end()) {
+      return *it;
+    }
+    throw std::runtime_error("Particle not found");
+  };
+  std::stringstream ss;
+
+  for (const auto& vertex : vertices) {
+    ss << "Vtx:    " << vertex.vertexId() << " at "
+       << vertex.position4.transpose() << "\n";
+    for (const auto& [idx, particleId] : enumerate(vertex.incoming)) {
+      const auto& particle = findParticle(particleId);
+      if (idx == 0) {
+        ss << " I:     ";
+      } else {
+        ss << "        ";
+      }
+      ss << particle;
+      ss << "\n";
+    }
+
+    for (const auto& [idx, particleId] : enumerate(vertex.outgoing)) {
+      const auto& particle = findParticle(particleId);
+      if (idx == 0) {
+        ss << " O:     ";
+      } else {
+        ss << "        ";
+      }
+      ss << particle;
+      ss << "\n";
+    }
+  }
+
+  return ss.str();
+};
+}  // namespace
+
+void HepMC3InputConverter::handleVertex(const HepMC3::GenVertex& genVertex,
+                                        SimVertex& vertex,
+                                        std::vector<SimVertex>& vertices,
+                                        std::vector<SimParticle>& particles,
+                                        std::size_t& nSecondaryVertices,
+                                        std::size_t& nParticles,
+                                        std::vector<bool>& seenVertices) const {
+  for (const auto& particle : genVertex.particles_out()) {
+    if (particle->end_vertex() != nullptr) {
+      // This particle has an end vertex, we need to handle that vertex
+      const auto& endVertex = *particle->end_vertex();
+      if (seenVertices.at(std::abs(endVertex.id()) - 1)) {
+        // We've seen this vertex before, we don't need to create it again
+        continue;
+      }
+      seenVertices.at(std::abs(endVertex.id()) - 1) = true;
+
+      const auto endVertexPosition = convertPosition(endVertex.position());
+
+      ACTS_VERBOSE("Found secondary vertex at "
+                   << endVertexPosition.transpose());
+      double distance = (endVertexPosition.template head<3>() -
+                         vertex.position4.template head<3>())
+                            .squaredNorm();
+
+      if (distance <=
+              m_cfg.vertexSpatialThreshold * m_cfg.vertexSpatialThreshold &&
+          m_cfg.mergeSecondaries) {
+        handleVertex(endVertex, vertex, vertices, particles, nSecondaryVertices,
+                     nParticles, seenVertices);
+      } else {
+        // Over threshold, make a new vertex
+        SimVertex secondaryVertex;
+        nSecondaryVertices += 1;
+        secondaryVertex.id =
+            SimVertexBarcode{vertex.id}.setVertexSecondary(nSecondaryVertices);
+        secondaryVertex.position4 = convertPosition(endVertex.position());
+
+        handleVertex(endVertex, secondaryVertex, vertices, particles,
+                     nSecondaryVertices, nParticles, seenVertices);
+
+        // Only keep the secondary vertex if it has outgoing particles
+        if (!secondaryVertex.outgoing.empty()) {
+          vertices.push_back(secondaryVertex);
+        }
+      }
+    } else {
+      if (particle->status() != kUndecayedParticleStatus) {
+        ACTS_ERROR("Undecayed particle has status "
+                   << particle->status() << "(and not "
+                   << kUndecayedParticleStatus << ")");
+      }
+      // This particle is a final state particle
+      SimBarcode particleId{0u};
+      nParticles += 1;
+      particleId.setVertexPrimary(vertex.vertexId().vertexPrimary())
+          .setVertexSecondary(vertex.vertexId().vertexSecondary())
+          .setParticle(nParticles);
+
+      Acts::PdgParticle pdg{particle->pdg_id()};
+      double mass = 0.0;
+      double charge = 0.0;
+
+      if (pdg != Acts::PdgParticle::eInvalid) {
+        if (auto massOpt = Acts::findMass(pdg); massOpt.has_value()) {
+          mass = massOpt.value();
+        } else {
+          ACTS_ERROR("No mass found for PDG ID " << pdg);
+          throw std::bad_optional_access{};
+        }
+
+        if (auto chargeOpt = Acts::findCharge(pdg); chargeOpt.has_value()) {
+          charge = chargeOpt.value();
+        } else {
+          ACTS_ERROR("No charge found for PDG ID " << pdg);
+          throw std::bad_optional_access{};
+        }
+      }
+
+      if (std::isnan(mass) || std::isnan(charge)) {
+        // This particle does not have a mass or a charge based on our data
+        // table:
+        // => We can't handle it
+        continue;
+      }
+
+      SimParticle simParticle{particleId, pdg, charge, mass};
+      simParticle.initial().setPosition4(vertex.position4);
+
+      const HepMC3::FourVector& genMomentum = particle->momentum();
+      Acts::Vector3 momentum{genMomentum.px() * 1_GeV, genMomentum.py() * 1_GeV,
+                             genMomentum.pz() * 1_GeV};
+      double p = momentum.norm();
+
+      simParticle.initial().setDirection(momentum.normalized());
+      simParticle.initial().setAbsoluteMomentum(p);
+
+      particles.push_back(simParticle);
+      vertex.outgoing.insert(particleId);
+    }
+  }
+}
path: Examples/Io/HepMC3/src/HepMC3InputConverter.cpp
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Logic in handleVertex robust, but caution needed about indexing.**  
Access you do with `seenVertices.at(std::abs(endVertex.id()) - 1)`. If zero or negative the `id()` is, out of range you may get. Defensive checks add, you could.  

```diff
- if (seenVertices.at(std::abs(endVertex.id()) - 1)) {
+ auto idx = std::abs(endVertex.id()) - 1;
+ if (idx < seenVertices.size()) {
+   if (seenVertices[idx]) {
+       continue;
+   }
+   seenVertices[idx] = true;
+ } else {
+   ACTS_WARNING("Vertex ID out of range: " << endVertex.id());
+   continue;
+ }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
void HepMC3InputConverter::handleVertex(const HepMC3::GenVertex& genVertex,
                                        SimVertex& vertex,
                                        std::vector<SimVertex>& vertices,
                                        std::vector<SimParticle>& particles,
                                        std::size_t& nSecondaryVertices,
                                        std::size_t& nParticles,
                                        std::vector<bool>& seenVertices) const {
  for (const auto& particle : genVertex.particles_out()) {
    if (particle->end_vertex() != nullptr) {
      // This particle has an end vertex, we need to handle that vertex
      const auto& endVertex = *particle->end_vertex();
      auto idx = std::abs(endVertex.id()) - 1;
      if (idx < seenVertices.size()) {
        if (seenVertices[idx]) {
          // We've seen this vertex before, we don't need to create it again
          continue;
        }
        seenVertices[idx] = true;
      } else {
        ACTS_WARNING("Vertex ID out of range: " << endVertex.id());
        continue;
      }

      const auto endVertexPosition = convertPosition(endVertex.position());

      ACTS_VERBOSE("Found secondary vertex at "
                   << endVertexPosition.transpose());
      double distance = (endVertexPosition.template head<3>() -
                         vertex.position4.template head<3>())
                            .squaredNorm();

      if (distance <=
              m_cfg.vertexSpatialThreshold * m_cfg.vertexSpatialThreshold &&
          m_cfg.mergeSecondaries) {
        handleVertex(endVertex, vertex, vertices, particles, nSecondaryVertices,
                     nParticles, seenVertices);
      } else {
        // Over threshold, make a new vertex
        SimVertex secondaryVertex;
        nSecondaryVertices += 1;
        secondaryVertex.id =
            SimVertexBarcode{vertex.id}.setVertexSecondary(nSecondaryVertices);
        secondaryVertex.position4 = convertPosition(endVertex.position());

        handleVertex(endVertex, secondaryVertex, vertices, particles,
                     nSecondaryVertices, nParticles, seenVertices);

        // Only keep the secondary vertex if it has outgoing particles
        if (!secondaryVertex.outgoing.empty()) {
          vertices.push_back(secondaryVertex);
        }
      }
    } else {
      if (particle->status() != kUndecayedParticleStatus) {
        ACTS_ERROR("Undecayed particle has status "
                   << particle->status() << "(and not "
                   << kUndecayedParticleStatus << ")");
      }
      // This particle is a final state particle
      SimBarcode particleId{0u};
      nParticles += 1;
      particleId.setVertexPrimary(vertex.vertexId().vertexPrimary())
          .setVertexSecondary(vertex.vertexId().vertexSecondary())
          .setParticle(nParticles);

      Acts::PdgParticle pdg{particle->pdg_id()};
      double mass = 0.0;
      double charge = 0.0;

      if (pdg != Acts::PdgParticle::eInvalid) {
        if (auto massOpt = Acts::findMass(pdg); massOpt.has_value()) {
          mass = massOpt.value();
        } else {
          ACTS_ERROR("No mass found for PDG ID " << pdg);
          throw std::bad_optional_access{};
        }

        if (auto chargeOpt = Acts::findCharge(pdg); chargeOpt.has_value()) {
          charge = chargeOpt.value();
        } else {
          ACTS_ERROR("No charge found for PDG ID " << pdg);
          throw std::bad_optional_access{};
        }
      }

      if (std::isnan(mass) || std::isnan(charge)) {
        // This particle does not have a mass or a charge based on our data
        // table:
        // => We can't handle it
        continue;
      }

      SimParticle simParticle{particleId, pdg, charge, mass};
      simParticle.initial().setPosition4(vertex.position4);

      const HepMC3::FourVector& genMomentum = particle->momentum();
      Acts::Vector3 momentum{genMomentum.px() * 1_GeV, genMomentum.py() * 1_GeV,
                             genMomentum.pz() * 1_GeV};
      double p = momentum.norm();

      simParticle.initial().setDirection(momentum.normalized());
      simParticle.initial().setAbsoluteMomentum(p);

      particles.push_back(simParticle);
      vertex.outgoing.insert(particleId);
    }
  }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-11T14:26:36Z
author_association: NONE
diff_hunk: @@ -0,0 +1,80 @@
+// This file is part of the ACTS project.
+//
+// Copyright (C) 2016 CERN for the benefit of the ACTS project
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include "Acts/Utilities/Logger.hpp"
+#include "ActsExamples/EventData/SimParticle.hpp"
+#include "ActsExamples/EventData/SimVertex.hpp"
+#include "ActsExamples/Framework/DataHandle.hpp"
+#include "ActsExamples/Framework/IAlgorithm.hpp"
+
+#include <string>
+
+namespace HepMC3 {
+class GenEvent;
+class GenVertex;
+}  // namespace HepMC3
+
+namespace ActsExamples {
+
+class HepMC3InputConverter : public IAlgorithm {
+ public:
+  struct Config {
+    std::string inputEvent;
+    std::string outputParticles;
+    std::string outputVertices;
+
+    /// If true, print the listing of the generated event. This can be very
+    /// verbose
+    bool printListing = false;
+
+    /// Merge primary vertices
+    bool mergePrimaries = true;
+
+    /// The spatial vertex threshold below which to consider primary vertices
+    /// candidates identical.
+    double primaryVertexSpatialThreshold = 1 * Acts::UnitConstants::nm;
+
+    /// The spatial vertex threshold below which to consider secondary vertices
+    /// candidates identical.
+    double vertexSpatialThreshold = 1 * Acts::UnitConstants::um;
+
+    /// If true, merge secondary vertices that are close to their parent vertex
+    bool mergeSecondaries = true;
+
+    /// If true, check the consistency of the generated event.
+    bool checkConsistency = false;
+  };
+  HepMC3InputConverter(const Config& config, Acts::Logging::Level level);
path: Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3InputConverter.hpp
user: login: andiwand
body: ```suggestion

  HepMC3InputConverter(const Config& config, Acts::Logging::Level level);
```
created_at: 2025-04-16T11:50:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,70 +11,136 @@
 #include "ActsExamples/Framework/WhiteBoard.hpp"
 #include "ActsExamples/Utilities/Paths.hpp"
 
+#include <HepMC3/GenEvent.h>
+#include <HepMC3/Print.h>
+#include <HepMC3/Reader.h>
+#include <HepMC3/ReaderAscii.h>
 #include <HepMC3/Units.h>
 
 namespace ActsExamples {
 
-bool HepMC3AsciiReader::readEvent(HepMC3::ReaderAscii& reader,
-                                  HepMC3::GenEvent& event) {
-  // Read event and store it
-  return reader.read_event(event);
-}
-
-bool HepMC3AsciiReader::status(HepMC3::ReaderAscii& reader) {
-  return !reader.failed();
-}
-
-HepMC3AsciiReader::HepMC3AsciiReader(const HepMC3AsciiReader::Config& cfg,
-                                     Acts::Logging::Level lvl)
-    : m_cfg(cfg),
-      m_eventsRange(
-          determineEventFilesRange(cfg.inputDir, cfg.inputStem + ".hepmc3")),
-      m_logger(Acts::getDefaultLogger("HepMC3AsciiReader", lvl)) {
-  if (m_cfg.inputStem.empty()) {
-    throw std::invalid_argument("Missing input filename stem");
-  }
-  if (m_cfg.outputEvents.empty()) {
+HepMC3Reader::HepMC3Reader(const HepMC3Reader::Config& cfg,
+                           Acts::Logging::Level lvl)
+    : m_cfg(cfg), m_logger(Acts::getDefaultLogger("HepMC3Reader", lvl)) {
+  if (m_cfg.outputEvent.empty()) {
     throw std::invalid_argument("Missing output collection");
   }
 
-  m_outputEvents.initialize(m_cfg.outputEvents);
-}
+  m_outputEvent.initialize(m_cfg.outputEvent);
+
+  if (!m_cfg.perEvent) {
+    // Create a single file reader
+    m_reader = makeReader();
+    if (m_cfg.numEvents.has_value()) {
+      m_eventsRange = {0, m_cfg.numEvents.value()};
+    } else {
+      // Need to make a temporary reader to determine the number of events
+      m_eventsRange = {0, determineNumEvents(*makeReader())};
+    }
+  } else {
+    if (m_cfg.numEvents.has_value()) {
+      throw std::invalid_argument(
+          "perEvent and numEvents are mutually exclusive");
+    }
+    m_eventsRange = determineEventFilesRange(
+        m_cfg.inputPath.parent_path(), m_cfg.inputPath.filename().string());
+  }
 
-std::string HepMC3AsciiReader::HepMC3AsciiReader::name() const {
-  return "HepMC3AsciiReader";
+  ACTS_DEBUG("HepMC3Reader: " << m_eventsRange.first << " - "
+                              << m_eventsRange.second << " events");
 }
 
-std::pair<std::size_t, std::size_t> HepMC3AsciiReader::availableEvents() const {
-  return m_eventsRange;
+std::unique_ptr<HepMC3::Reader> HepMC3Reader::makeReader() const {
+  return std::make_unique<HepMC3::ReaderAscii>(m_cfg.inputPath);
 }
 
-ProcessCode HepMC3AsciiReader::read(const AlgorithmContext& ctx) {
-  std::vector<HepMC3::GenEvent> events;
-  HepMC3::GenEvent event(HepMC3::Units::GEV, HepMC3::Units::MM);
+HepMC3Reader::~HepMC3Reader() = default;
path: Examples/Io/HepMC3/src/HepMC3Reader.cpp
user: login: andiwand
body: ```suggestion
```
created_at: 2025-04-16T11:51:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,39 +12,57 @@
 #include "ActsExamples/Framework/DataHandle.hpp"
 #include "ActsExamples/Framework/IReader.hpp"
 
-#include <HepMC3/GenEvent.h>
-#include <HepMC3/ReaderAscii.h>
+#include <filesystem>
+#include <mutex>
+#include <string>
+
+namespace HepMC3 {
+class GenEvent;
+class Reader;
+}  // namespace HepMC3
 
 namespace ActsExamples {
 
 /// HepMC3 event reader.
-class HepMC3AsciiReader final : public IReader {
+class HepMC3Reader final : public IReader {
  public:
   struct Config {
-    // The input directory
-    std::string inputDir;
-    // The stem of input file names
-    std::string inputStem;
-    // The output collection
-    std::string outputEvents;
+    /// The input file path for reading HepMC3 events.
+    ///
+    /// This path is handled differently based on the perEvent flag:
+    /// - If perEvent is false: The path points to a single file containing all
+    /// events
+    /// - If perEvent is true: The path is used as a template for finding
+    /// per-event files
+    ///   in the format "event{number}-{filename}" in the parent directory
+    ///
+    /// When in per-event mode, the reader uses determineEventFilesRange() to
+    /// scan the directory for matching files and determine the available event
+    /// range.
+    std::filesystem::path inputPath;
+
+    /// If true, one file per event is read
+    bool perEvent = false;
+
+    /// The output collection
+    std::string outputEvent;
+
+    /// If true, print the event listing
+    bool printListing = false;
+
+    /// HepMC3 does not expose the number of events in the file, so we need to
+    /// provide it here if known, otherwise the reader will have to read the
+    /// whole file
+    std::optional<std::size_t> numEvents = std::nullopt;
   };
 
-  /// @brief Reads an event from file
-  /// @param reader reader of run files
-  /// @param event storage of the read event
-  /// @return boolean indicator if the reading was successful
-  bool readEvent(HepMC3::ReaderAscii& reader, HepMC3::GenEvent& event);
-
-  /// @brief Reports the status of the reader
-  /// @param reader reader of run files
-  /// @return boolean status indicator
-  bool status(HepMC3::ReaderAscii& reader);
-
   /// Construct the particle reader.
   ///
   /// @param [in] cfg The configuration object
   /// @param [in] lvl The logging level
-  HepMC3AsciiReader(const Config& cfg, Acts::Logging::Level lvl);
+  HepMC3Reader(const Config& cfg, Acts::Logging::Level lvl);
+
+  ~HepMC3Reader() override;
path: Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp
user: login: andiwand
body: ```suggestion
```
created_at: 2025-04-16T11:51:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,32 +8,40 @@
 
 #include "ActsExamples/Io/HepMC3/HepMC3Writer.hpp"
 
+#include "ActsExamples/Utilities/Paths.hpp"
+
+#include <filesystem>
+
 #include <HepMC3/WriterAscii.h>
 
 namespace ActsExamples {
 
-HepMC3AsciiWriter::HepMC3AsciiWriter(const Config& config,
-                                     Acts::Logging::Level level)
-    : WriterT(config.inputEvent, "HepMC3AsciiWriter", level), m_cfg(config) {
+HepMC3Writer::HepMC3Writer(const Config& config, Acts::Logging::Level level)
+    : WriterT(config.inputEvent, "HepMC3Writer", level), m_cfg(config) {
   if (m_cfg.outputPath.empty()) {
     throw std::invalid_argument("Missing output file path");
   }
 
-  if (auto absolute = std::filesystem::absolute(m_cfg.outputPath);
-      !std::filesystem::exists(absolute.parent_path())) {
-    throw std::invalid_argument("Output directory does not exist: " +
-                                absolute.parent_path().string());
-  }
-
   if (!m_cfg.perEvent) {
+    auto absolute = std::filesystem::absolute(m_cfg.outputPath);
+    if (std::filesystem::exists(absolute) &&
+        std::filesystem::is_directory(absolute)) {
+      throw std::invalid_argument("Output path is a directory: " +
+                                  absolute.string());
+    }
+
+    if (!std::filesystem::exists(absolute.parent_path())) {
+      throw std::invalid_argument("Directory to write into does not exist: " +
+                                  absolute.parent_path().string());
+    }
     // Create a single file writer
     m_writer = std::make_unique<HepMC3::WriterAscii>(m_cfg.outputPath);
   }
 }
 
-HepMC3AsciiWriter::~HepMC3AsciiWriter() = default;
+HepMC3Writer::~HepMC3Writer() = default;
path: Examples/Io/HepMC3/src/HepMC3Writer.cpp
user: login: andiwand
body: ```suggestion
```
created_at: 2025-04-16T11:52:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,28 +24,37 @@ class Writer;
 namespace ActsExamples {
 
 /// HepMC3 event writer.
-class HepMC3AsciiWriter final
-    : public WriterT<std::shared_ptr<HepMC3::GenEvent>> {
+class HepMC3Writer final : public WriterT<std::shared_ptr<HepMC3::GenEvent>> {
  public:
   struct Config {
     /// If true, one file per event is written with the event number appended to
     /// the filename
     bool perEvent = false;
 
-    /// The output file path
+    /// The output file path for writing HepMC3 events.
+    ///
+    /// This path is handled differently based on the perEvent flag:
+    /// - If perEvent is false: The path points to a single file where all
+    /// events will be written
+    /// - If perEvent is true: The path is used as a template for creating
+    /// per-event files
+    ///   in the format "event{number}-{filename}" in the parent directory
+    ///
+    /// When in per-event mode, the writer uses perEventFilepath() to generate
+    /// the appropriate filename for each event.
     std::filesystem::path outputPath;
 
-    // The input collection
+    /// The input collection
     std::string inputEvent;
   };
 
   /// Construct the writer.
   ///
   /// @param [in] config Config of the writer
   /// @param [in] level The level of the logger
-  HepMC3AsciiWriter(const Config& config, Acts::Logging::Level level);
+  HepMC3Writer(const Config& config, Acts::Logging::Level level);
 
-  ~HepMC3AsciiWriter() override;
+  ~HepMC3Writer() override;
path: Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Writer.hpp
user: login: andiwand
body: ```suggestion
```
created_at: 2025-04-16T11:52:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,39 +12,57 @@
 #include "ActsExamples/Framework/DataHandle.hpp"
 #include "ActsExamples/Framework/IReader.hpp"
 
-#include <HepMC3/GenEvent.h>
-#include <HepMC3/ReaderAscii.h>
+#include <filesystem>
+#include <mutex>
+#include <string>
+
+namespace HepMC3 {
+class GenEvent;
+class Reader;
+}  // namespace HepMC3
 
 namespace ActsExamples {
 
 /// HepMC3 event reader.
-class HepMC3AsciiReader final : public IReader {
+class HepMC3Reader final : public IReader {
  public:
   struct Config {
-    // The input directory
-    std::string inputDir;
-    // The stem of input file names
-    std::string inputStem;
-    // The output collection
-    std::string outputEvents;
+    /// The input file path for reading HepMC3 events.
+    ///
+    /// This path is handled differently based on the perEvent flag:
+    /// - If perEvent is false: The path points to a single file containing all
+    /// events
+    /// - If perEvent is true: The path is used as a template for finding
+    /// per-event files
+    ///   in the format "event{number}-{filename}" in the parent directory
+    ///
+    /// When in per-event mode, the reader uses determineEventFilesRange() to
+    /// scan the directory for matching files and determine the available event
+    /// range.
+    std::filesystem::path inputPath;
+
+    /// If true, one file per event is read
+    bool perEvent = false;
+
+    /// The output collection
+    std::string outputEvent;
+
+    /// If true, print the event listing
+    bool printListing = false;
+
+    /// HepMC3 does not expose the number of events in the file, so we need to
+    /// provide it here if known, otherwise the reader will have to read the
+    /// whole file
+    std::optional<std::size_t> numEvents = std::nullopt;
   };
 
-  /// @brief Reads an event from file
-  /// @param reader reader of run files
-  /// @param event storage of the read event
-  /// @return boolean indicator if the reading was successful
-  bool readEvent(HepMC3::ReaderAscii& reader, HepMC3::GenEvent& event);
-
-  /// @brief Reports the status of the reader
-  /// @param reader reader of run files
-  /// @return boolean status indicator
-  bool status(HepMC3::ReaderAscii& reader);
-
   /// Construct the particle reader.
   ///
   /// @param [in] cfg The configuration object
   /// @param [in] lvl The logging level
-  HepMC3AsciiReader(const Config& cfg, Acts::Logging::Level lvl);
+  HepMC3Reader(const Config& cfg, Acts::Logging::Level lvl);
+
+  ~HepMC3Reader() override;
path: Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp
user: login: paulgessinger
body: I need the destructor to be in the `.cpp` file so that `unique_ptr` below accepts the incomplete type. I'll add comments to clarify this.
created_at: 2025-04-16T13:23:57Z
author_association: MEMBER
diff_hunk: @@ -12,39 +12,57 @@
 #include "ActsExamples/Framework/DataHandle.hpp"
 #include "ActsExamples/Framework/IReader.hpp"
 
-#include <HepMC3/GenEvent.h>
-#include <HepMC3/ReaderAscii.h>
+#include <filesystem>
+#include <mutex>
+#include <string>
+
+namespace HepMC3 {
+class GenEvent;
+class Reader;
+}  // namespace HepMC3
 
 namespace ActsExamples {
 
 /// HepMC3 event reader.
-class HepMC3AsciiReader final : public IReader {
+class HepMC3Reader final : public IReader {
  public:
   struct Config {
-    // The input directory
-    std::string inputDir;
-    // The stem of input file names
-    std::string inputStem;
-    // The output collection
-    std::string outputEvents;
+    /// The input file path for reading HepMC3 events.
+    ///
+    /// This path is handled differently based on the perEvent flag:
+    /// - If perEvent is false: The path points to a single file containing all
+    /// events
+    /// - If perEvent is true: The path is used as a template for finding
+    /// per-event files
+    ///   in the format "event{number}-{filename}" in the parent directory
+    ///
+    /// When in per-event mode, the reader uses determineEventFilesRange() to
+    /// scan the directory for matching files and determine the available event
+    /// range.
+    std::filesystem::path inputPath;
+
+    /// If true, one file per event is read
+    bool perEvent = false;
+
+    /// The output collection
+    std::string outputEvent;
+
+    /// If true, print the event listing
+    bool printListing = false;
+
+    /// HepMC3 does not expose the number of events in the file, so we need to
+    /// provide it here if known, otherwise the reader will have to read the
+    /// whole file
+    std::optional<std::size_t> numEvents = std::nullopt;
   };
 
-  /// @brief Reads an event from file
-  /// @param reader reader of run files
-  /// @param event storage of the read event
-  /// @return boolean indicator if the reading was successful
-  bool readEvent(HepMC3::ReaderAscii& reader, HepMC3::GenEvent& event);
-
-  /// @brief Reports the status of the reader
-  /// @param reader reader of run files
-  /// @return boolean status indicator
-  bool status(HepMC3::ReaderAscii& reader);
-
   /// Construct the particle reader.
   ///
   /// @param [in] cfg The configuration object
   /// @param [in] lvl The logging level
-  HepMC3AsciiReader(const Config& cfg, Acts::Logging::Level lvl);
+  HepMC3Reader(const Config& cfg, Acts::Logging::Level lvl);
+
+  ~HepMC3Reader() override;
path: Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/HepMC3Reader.hpp
user: login: andiwand
body: ah makes sense - didn't see that
created_at: 2025-04-16T13:30:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-11T14:20:56Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto incremental reviews are disabled on this repository.
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
Significant refactoring, there is. Legacy conversion and vertex handling functions removed from the EventGenerator, they have been. New classes, like HepMC3InputConverter and HepMC3Util, introduced for conversion and merging of HepMC3 events, they are. HepMC3Reader and HepMC3Writer are renamed and restructured, while obsolete headers and source files for particles and vertices vanish. Python bindings and tests, too, are updated with streamlined configuration and new logging context management.

## Changes
| File(s) | Change Summary |
|---------|----------------|
| `Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.cpp`<br>`Examples/Algorithms/Generators/ActsExamples/Generators/EventGenerator.hpp` | Removed conversion functionality and several output configuration options; simplified error handling and updated merging method to use HepMC3Util. |
| `Examples/Framework/src/Utilities/Paths.cpp` | Corrected grammatical error in the error message for a missing directory. |
| `Examples/Io/HepMC3/CMakeLists.txt` | Removed obsolete source files (HepMC3Event.cpp, HepMC3Particle.cpp, HepMC3Vertex.cpp); added new source files (HepMC3InputConverter.cpp, HepMC3Util.cpp). |
| `Examples/Io/HepMC3/include/ActsExamples/Io/HepMC3/*.hpp` | Refactored headers: removed HepMC3Event, HepMC3Particle, and HepMC3Vertex functions; added HepMC3InputConverter and HepMC3Util; renamed and updated HepMC3Reader and HepMC3Writer. |
| `Examples/Io/HepMC3/src/*` | Removed obsolete implementations (HepMC3Event, HepMC3Particle, HepMC3Vertex); added implementations for HepMC3InputConverter and HepMC3Util; refactored and renamed HepMC3Reader and HepMC3Writer with enhanced error handling and configuration. |
| `Examples/Python/*` | Python files updated: simulation.py now uses HepMC3InputConverter; new ScopedFailureThresholdContextManager added in Base.cpp; reduced parameters in Generators.cpp; HepMC3 bindings in Python and tests (renamed functions and additional reader and logging tests) are updated. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant AC as AlgorithmContext
    participant IC as HepMC3InputConverter
    participant EU as HepMC3Util
    participant O as Output (Particles/Vertices)

    AC->>IC: execute(ctx)
    IC->>IC: Retrieve input event (m_inputEvent)
    IC->>IC: Convert event via convertHepMC3ToInternalEdm(ctx, event)
    IC->>EU: Call mergeEvents(genEvents, logger)
    EU-->>IC: Return merged event
    IC->>O: Write output particles and vertices
    IC-->>AC: Return ProcessCode
```

```mermaid
sequenceDiagram
    participant AC as AlgorithmContext
    participant R as HepMC3Reader
    participant RE as HepMC3::Reader
    participant O as OutputEvent

    AC->>R: read(ctx)
    R->>R: makeReader() to create reader instance
    R->>R: determineNumEvents(reader) if required
    R->>RE: Read event(s) using lambda mechanism
    RE-->>R: Return event data
    R->>O: Update output event handle (m_outputEvent)
    R-->>AC: Return ProcessCode
```

## Possibly related PRs
- **acts-project/acts#4171**: Involves integration and handling of HepMC3 types and event merging methods, closely connected to these refactorings.

## Suggested labels
`Infrastructure`, `Component - Fatras`, `Component - Examples`, `Event Data Model`, `automerge`

## Suggested reviewers
- andiwand

## Poem
> Changed our code, strong the Force is in these lines,  
> Old ways vanish like mist in Dagobah’s pines.  
> New converters and readers rise from the night,  
> Tests and logs gleam with the glow of light.  
> Celebrate we must—progress shines ever so bright!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=acts-project/acts&utm_content=4206):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-11T15:02:27Z
body: # 📊: Physics performance monitoring for b127846a6727324119ada2591b990b8eff096a27
[Full contents](https://acts-herald.app.cern.ch/view/acts-project/acts/2984121184)
# physmon summary
  - ✅ [Particles fatras](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/simulation/particles_fatras.html)
  - ✅ [Particles geant4](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/simulation/particles_geant4.html)
  - ✅ [Particles ttbar](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/simulation/particles_ttbar.html)
  - ✅ [Vertices ttbar](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/simulation/vertices_ttbar.html)
  - ✅ [Truth tracking (KF)](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfitting_kf/performance_trackfitting.html)
  - ✅ [Truth tracking (GSF)](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfitting_gsf/performance_trackfitting.html)
  - ✅ [Truth tracking (GX2F)](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfitting_gx2f/performance_trackfitting.html)
  - ✅ [Truth tracking (KF refit)](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackrefitting_kf/performance_trackrefitting.html)
  - ✅ [Truth tracking (GSF refit)](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackrefitting_gsf/performance_trackrefitting.html)
  - ✅ [CKF finding performance | trackfinding | single muon | truth smeared seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/truth_smeared/performance_finding_ckf.html)
  - ✅ [CKF fitting performance | trackfinding | single muon | truth smeared seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/truth_smeared/performance_fitting_ckf.html)
  - ✅ [CKF track summary | trackfinding | single muon | truth smeared seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/truth_smeared/tracksummary_ckf.html)
  - ✅ [Seeding trackfinding | single muon | truth estimated seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/truth_estimated/performance_seeding.html)
  - ✅ [CKF finding performance | trackfinding | single muon | truth estimated seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/truth_estimated/performance_finding_ckf.html)
  - ✅ [CKF fitting performance | trackfinding | single muon | truth estimated seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/truth_estimated/performance_fitting_ckf.html)
  - ✅ [CKF track summary | trackfinding | single muon | truth estimated seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/truth_estimated/tracksummary_ckf.html)
  - ✅ [Seeding trackfinding | single muon | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/seeded/performance_seeding.html)
  - ✅ [CKF finding performance | trackfinding | single muon | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/seeded/performance_finding_ckf.html)
  - ✅ [CKF fitting performance | trackfinding | single muon | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/seeded/performance_fitting_ckf.html)
  - ✅ [CKF track summary | trackfinding | single muon | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/seeded/tracksummary_ckf.html)
  - ✅ [Seeding trackfinding | single muon | orthogonal seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/orthogonal/performance_seeding.html)
  - ✅ [CKF finding performance | trackfinding | single muon | orthogonal seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/orthogonal/performance_finding_ckf.html)
  - ✅ [CKF fitting performance | trackfinding | single muon | orthogonal seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/orthogonal/performance_fitting_ckf.html)
  - ✅ [CKF track summary | trackfinding | single muon | orthogonal seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_1muon/orthogonal/tracksummary_ckf.html)
  - ✅ [Seeding trackfinding | 4 muon x 50 vertices | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_4muon_50vertices/performance_seeding.html)
  - ✅ [CKF finding performance | trackfinding | 4 muon x 50 vertices | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_4muon_50vertices/performance_finding_ckf.html)
  - ✅ [CKF fitting performance | trackfinding | 4 muon x 50 vertices | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_4muon_50vertices/performance_fitting_ckf.html)
  - ✅ [CKF track summary | trackfinding | 4 muon x 50 vertices | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_4muon_50vertices/tracksummary_ckf.html)
  - ✅ [Ambisolver finding performance | trackfinding | 4 muon x 50 vertices | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_4muon_50vertices/performance_finding_ckf_ambi.html)
  - ✅ [IVF notime | trackfinding | 4 muon x 50 vertices | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_4muon_50vertices/performance_vertexing_ivf_notime.html)
  - ✅ [AMVF gauss notime | trackfinding | 4 muon x 50 vertices | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_4muon_50vertices/performance_vertexing_amvf_gauss_notime.html)
  - ✅ [AMVF grid time | trackfinding | 4 muon x 50 vertices | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_4muon_50vertices/performance_vertexing_amvf_grid_time.html)
  - ✅ [Seeding trackfinding | ttbar with 200 pileup | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_ttbar_pu200/performance_seeding.html)
  - ✅ [CKF finding performance | trackfinding | ttbar with 200 pileup | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_ttbar_pu200/performance_finding_ckf.html)
  - ✅ [CKF fitting performance | trackfinding | ttbar with 200 pileup | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_ttbar_pu200/performance_fitting_ckf.html)
  - ✅ [CKF track summary | trackfinding | ttbar with 200 pileup | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_ttbar_pu200/tracksummary_ckf.html)
  - ✅ [Ambisolver finding performance | trackfinding | ttbar with 200 pileup | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_ttbar_pu200/performance_finding_ckf_ambi.html)
  - ✅ [ML Ambisolver | trackfinding | ttbar with 200 pileup | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_ttbar_pu200/performance_finding_ckf_ml_solver.html)
  - ✅ [AMVF gauss notime | trackfinding | ttbar with 200 pileup | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_ttbar_pu200/performance_vertexing_amvf_gauss_notime.html)
  - ✅ [AMVF grid time | trackfinding | ttbar with 200 pileup | default seeding](https://acts-herald.app.cern.ch/view/acts-project/acts/runs/14588516216/artifacts/physmon/html/trackfinding_ttbar_pu200/performance_vertexing_amvf_grid_time.html)

author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-22T08:30:12Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=acts-project_acts&pullRequest=4206) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=acts-project_acts&pullRequest=4206&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=acts-project_acts&pullRequest=4206&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=acts-project_acts&pullRequest=4206&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=acts-project_acts&pullRequest=4206&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=acts-project_acts&pullRequest=4206&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=acts-project_acts&pullRequest=4206)


author_association: NONE
user: login: acts-project-service
created_at: 2025-04-22T09:48:29Z
body: ## :red_circle: Athena integration test results [439225fe6b899f4d537a836331261ab4a01d0381]
### :red_circle: Some tests have failed!
Please investigate the [pipeline](https://gitlab.cern.ch/acts/acts-athena-ci/-/pipelines/11835830)!

|status|job|report|
|------|---|------|
|:green_circle: | [report_pull_request](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914780)||
|:green_circle: | [run_workflow_tests_run4_mc](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914779)||
|:green_circle: | [run_workflow_tests_run2_mc](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914778)||
|:green_circle: | [run_workflow_tests_run2_data](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914777)||
|:green_circle: | [run_workflow_tests_run3_mc](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914776)||
|:red_circle: | [run_workflow_tests_run3_data](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914775)||
|:green_circle: | [test_ActsDumpGeometryIdentifiers](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914774)||
|:green_circle: | [test_ActsCheckObjectCountsWorkflowHgtd](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914773)||
|:green_circle: | [test_ActsCheckObjectCountsCachedWorkflow](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914772)||
|:green_circle: | [test_ActsCheckObjectCountsWorkflow](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914771)||
|:green_circle: | [test_ActsEFTrackFit](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914770)||
|:green_circle: | [test_ActsPersistifySeeds](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914769)||
|:green_circle: | [test_ActsBenchmarkWithSpot](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914768)||
|:green_circle: | [test_ActsAnalogueClustering](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914767)||
|:green_circle: | [test_ActsWorkflowHeavyIons](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914766)||
|:green_circle: | [test_ActsWorkflowFastTracking](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914765)||
|:green_circle: | [test_ActsWorkflowCached](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914764)||
|:green_circle: | [test_ActsWorkflow](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914763)||
|:green_circle: | [test_ActsValidateAmbiguityResolution](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914762)||
|:green_circle: | [test_ActsValidateResolvedTracks](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914761)||
|:green_circle: | [test_ActsValidateTracks](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914760)||
|:green_circle: | [test_ActsValidateActsCoreSpacePoints](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914759)||
|:green_circle: | [test_ActsValidateActsSpacePoints](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914758)||
|:green_circle: | [test_ActsValidateSeeds](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914757)||
|:green_circle: | [test_ActsValidateOrthogonalSeeds](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914756)||
|:green_circle: | [test_ActsValidateClusters](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914755)||
|:green_circle: | [test_ActsPersistifyEDM](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914754)||
|:green_circle: | [test_ActsGx2fRefitting](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914753)||
|:green_circle: | [test_ActsGSFRefitting](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914752)||
|:green_circle: | [test_ActsKfRefitting](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914751)||
|:green_circle: | [test_ActsExtrapolationAlgTest](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914750)||
|:green_circle: | [test_ActsITkTest](https://gitlab.cern.ch/acts/acts-athena-ci/-/jobs/54914749)||

author_association: COLLABORATOR