diff --git a/Core/include/Acts/Utilities/Any.hpp b/Core/include/Acts/Utilities/Any.hpp index 03ce8863ad8..1b28a3d75d9 100644 --- a/Core/include/Acts/Utilities/Any.hpp +++ b/Core/include/Acts/Utilities/Any.hpp @@ -364,12 +364,6 @@ class AnyBase : public AnyBaseAll { } } - template - static std::uint64_t typeHash() { - const static std::uint64_t value = detail::fnv1a_64(typeid(T).name()); - return value; - } - struct Handler { void (*destroy)(void* ptr) = nullptr; void (*moveConstruct)(void* from, void* to) = nullptr; diff --git a/Core/include/Acts/Utilities/HashedString.hpp b/Core/include/Acts/Utilities/HashedString.hpp index d0642e28eba..7588ae6f0f6 100644 --- a/Core/include/Acts/Utilities/HashedString.hpp +++ b/Core/include/Acts/Utilities/HashedString.hpp @@ -12,8 +12,7 @@ #include #include #include -#include -#include +#include namespace Acts { /// @brief Type alias for hashed string representation @@ -76,4 +75,15 @@ constexpr HashedString operator""_hash(char const* s, std::size_t count) { } } // namespace HashedStringLiteral + +/// Hash for a type. Since it's not possible to hash a type at compile-time, +/// this function returns a runtime hash but caches it in a static variable. +/// @tparam T Type to hash +/// @return Hashed string representation +template +std::uint64_t typeHash() { + const static std::uint64_t value = detail::fnv1a_64(typeid(T).name()); + return value; +} + } // namespace Acts diff --git a/Examples/Framework/include/ActsExamples/Framework/DataHandle.hpp b/Examples/Framework/include/ActsExamples/Framework/DataHandle.hpp index 4a1824113b3..cde9d0542eb 100644 --- a/Examples/Framework/include/ActsExamples/Framework/DataHandle.hpp +++ b/Examples/Framework/include/ActsExamples/Framework/DataHandle.hpp @@ -8,6 +8,7 @@ #pragma once +#include "Acts/Utilities/HashedString.hpp" #include "ActsExamples/Framework/AlgorithmContext.hpp" #include "ActsExamples/Framework/SequenceElement.hpp" #include "ActsExamples/Framework/WhiteBoard.hpp" @@ -51,6 +52,7 @@ class DataHandleBase { const std::string& key() const { return m_key.value(); } virtual const std::type_info& typeInfo() const = 0; + virtual std::uint64_t typeHash() const = 0; bool isInitialized() const { return m_key.has_value(); } @@ -88,6 +90,10 @@ class DataHandleBase { return wb.pop(m_key.value()); } + WhiteBoard::IHolder* getHolder(const WhiteBoard& wb) const { + return wb.getHolder(m_key.value()); + } + SequenceElement* m_parent{nullptr}; std::string m_name; std::optional m_key{}; @@ -180,6 +186,7 @@ class WriteDataHandle final : public WriteDataHandleBase { } const std::type_info& typeInfo() const override { return typeid(T); }; + std::uint64_t typeHash() const override { return Acts::typeHash(); }; }; /// A read handle for accessing data from the WhiteBoard. @@ -213,6 +220,7 @@ class ReadDataHandle final : public ReadDataHandleBase { } const std::type_info& typeInfo() const override { return typeid(T); }; + std::uint64_t typeHash() const override { return Acts::typeHash(); }; }; /// A consume handle for taking ownership of data from the WhiteBoard. @@ -247,6 +255,7 @@ class ConsumeDataHandle final : public ConsumeDataHandleBase { } const std::type_info& typeInfo() const override { return typeid(T); }; + std::uint64_t typeHash() const override { return Acts::typeHash(); }; }; } // namespace ActsExamples diff --git a/Examples/Framework/include/ActsExamples/Framework/WhiteBoard.hpp b/Examples/Framework/include/ActsExamples/Framework/WhiteBoard.hpp index 2622fa54681..a69a85b967e 100644 --- a/Examples/Framework/include/ActsExamples/Framework/WhiteBoard.hpp +++ b/Examples/Framework/include/ActsExamples/Framework/WhiteBoard.hpp @@ -9,6 +9,7 @@ #pragma once #include "Acts/Utilities/Concepts.hpp" +#include "Acts/Utilities/HashedString.hpp" #include "Acts/Utilities/Logger.hpp" #include @@ -36,6 +37,8 @@ class WhiteBoard { struct IHolder { virtual ~IHolder() = default; virtual const std::type_info& type() const = 0; + virtual const void* data() const = 0; + virtual std::uint64_t typeHash() const = 0; }; template struct HolderT : public IHolder { @@ -43,6 +46,8 @@ class WhiteBoard { explicit HolderT(T&& v) : value(std::move(v)) {} const std::type_info& type() const override { return typeid(T); } + std::uint64_t typeHash() const override { return Acts::typeHash(); } + const void* data() const override { return std::addressof(value); } }; struct StringHash { @@ -116,6 +121,8 @@ class WhiteBoard { template HolderT* getHolder(const std::string& name) const; + IHolder* getHolder(const std::string& name) const; + template T pop(const std::string& name); diff --git a/Examples/Framework/src/Framework/DataHandle.cpp b/Examples/Framework/src/Framework/DataHandle.cpp index 403f5b86495..b096973a1fc 100644 --- a/Examples/Framework/src/Framework/DataHandle.cpp +++ b/Examples/Framework/src/Framework/DataHandle.cpp @@ -12,6 +12,8 @@ #include "ActsExamples/Framework/Sequencer.hpp" #include +#include +#include #include #include @@ -73,8 +75,7 @@ void DataHandleBase::maybeInitialize(std::optional key) { } bool WriteDataHandleBase::isCompatible(const DataHandleBase& other) const { - return dynamic_cast(&other) != nullptr && - typeInfo() == other.typeInfo(); + return typeHash() == other.typeHash(); } void WriteDataHandleBase::emulate(StateMapType& state, @@ -114,8 +115,7 @@ void ReadDataHandleBase::initialize(std::string_view key) { } bool ReadDataHandleBase::isCompatible(const DataHandleBase& other) const { - return dynamic_cast(&other) != nullptr && - typeInfo() == other.typeInfo(); + return typeHash() == other.typeHash(); } void ReadDataHandleBase::emulate(StateMapType& state, diff --git a/Examples/Framework/src/Framework/WhiteBoard.cpp b/Examples/Framework/src/Framework/WhiteBoard.cpp index ab2130e547b..16d9dfa8fc0 100644 --- a/Examples/Framework/src/Framework/WhiteBoard.cpp +++ b/Examples/Framework/src/Framework/WhiteBoard.cpp @@ -136,4 +136,12 @@ std::vector WhiteBoard::getKeys() const { return keys; } +WhiteBoard::IHolder *WhiteBoard::getHolder(const std::string &name) const { + auto it = m_store.find(name); + if (it == m_store.end()) { + throw std::out_of_range("Object '" + name + "' does not exists"); + } + return it->second.get(); +} + } // namespace ActsExamples diff --git a/Examples/Io/Podio/include/ActsExamples/Io/Podio/CollectionBaseWriteHandle.hpp b/Examples/Io/Podio/include/ActsExamples/Io/Podio/CollectionBaseWriteHandle.hpp index 7932f1fb72e..acee30b996a 100644 --- a/Examples/Io/Podio/include/ActsExamples/Io/Podio/CollectionBaseWriteHandle.hpp +++ b/Examples/Io/Podio/include/ActsExamples/Io/Podio/CollectionBaseWriteHandle.hpp @@ -75,6 +75,8 @@ class CollectionBaseWriteHandle : public WriteDataHandleBase { /// Get the type info for this handle /// @return The type info for std::unique_ptr const std::type_info& typeInfo() const override; + + std::uint64_t typeHash() const override; }; } // namespace ActsExamples diff --git a/Examples/Io/Podio/src/CollectionBaseWriteHandle.cpp b/Examples/Io/Podio/src/CollectionBaseWriteHandle.cpp index 0763dcc9a84..fe2003414f2 100644 --- a/Examples/Io/Podio/src/CollectionBaseWriteHandle.cpp +++ b/Examples/Io/Podio/src/CollectionBaseWriteHandle.cpp @@ -36,4 +36,8 @@ const std::type_info& CollectionBaseWriteHandle::typeInfo() const { return typeid(std::unique_ptr); } +std::uint64_t CollectionBaseWriteHandle::typeHash() const { + return Acts::typeHash>(); +} + } // namespace ActsExamples diff --git a/Python/Examples/src/Framework.cpp b/Python/Examples/src/Framework.cpp index de3c9ca6329..0967d335af8 100644 --- a/Python/Examples/src/Framework.cpp +++ b/Python/Examples/src/Framework.cpp @@ -8,6 +8,7 @@ #include "Acts/Utilities/Logger.hpp" #include "ActsExamples/Framework/AlgorithmContext.hpp" +#include "ActsExamples/Framework/DataHandle.hpp" #include "ActsExamples/Framework/IAlgorithm.hpp" #include "ActsExamples/Framework/IReader.hpp" #include "ActsExamples/Framework/IWriter.hpp" @@ -16,9 +17,12 @@ #include "ActsExamples/Framework/SequenceElement.hpp" #include "ActsExamples/Framework/Sequencer.hpp" #include "ActsExamples/Framework/WhiteBoard.hpp" -#include "ActsPython/Utilities/Helpers.hpp" #include "ActsPython/Utilities/Macros.hpp" +#include "ActsPython/Utilities/WhiteBoardTypeRegistry.hpp" +#include + +#include #include #include @@ -82,11 +86,74 @@ class PyIAlgorithm : public IAlgorithm { } } + ProcessCode initialize() override { + py::gil_scoped_acquire acquire{}; + PYBIND11_OVERRIDE(ProcessCode, IAlgorithm, initialize, ); + } + + ProcessCode finalize() override { + py::gil_scoped_acquire acquire{}; + PYBIND11_OVERRIDE(ProcessCode, IAlgorithm, finalize, ); + } + std::string_view typeName() const override { return "Algorithm"; } const Acts::Logger& pyLogger() const { return logger(); } }; +class PyReadDataHandle : public ReadDataHandleBase { + public: + PyReadDataHandle(SequenceElement* parent, py::object pytype, + const std::string& name) + : ReadDataHandleBase(parent, name) { + m_entry = WhiteBoardRegistry::find(pytype); + if (m_entry == nullptr) { + throw py::type_error("Type '" + + pytype.attr("__qualname__").cast() + + "' is not registered for WhiteBoard access"); + } + if (m_entry->typeinfo == nullptr) { + throw py::type_error("Type '" + + pytype.attr("__qualname__").cast() + + "' is not registered for WhiteBoard access"); + } + + m_pytype = std::move(pytype); + registerAsReadHandle(); + } + + const std::type_info& typeInfo() const override { return *m_entry->typeinfo; } + + std::uint64_t typeHash() const override { return m_entry->typeHash; } + + py::object call(const py::object& wbPy) const { + if (!isInitialized()) { + throw std::runtime_error("ReadDataHandle '" + name() + + "' not initialized"); + } + const auto& wb = wbPy.cast(); + + if (!wb.exists(key())) { + throw py::key_error("Key '" + key() + "' does not exist"); + } + + const auto& holder = getHolder(wb); + + if (m_entry->typeHash != holder->typeHash()) { + const auto& expected = boost::core::demangle(m_entry->typeinfo->name()); + const auto& actual = boost::core::demangle(holder->type().name()); + throw py::type_error("Type mismatch for key '" + key() + "'. Expected " + + expected + " but got " + actual); + } + + return m_entry->fn(holder->data(), wbPy); + } + + private: + py::object m_pytype; + const WhiteBoardRegistry::RegistryEntry* m_entry{nullptr}; +}; + void trigger_divbyzero() { volatile float j = 0.0; volatile float r = 123 / j; // MARK: divbyzero @@ -132,6 +199,23 @@ void addFramework(py::module& mex) { .def("exists", &WhiteBoard::exists) .def_property_readonly("keys", &WhiteBoard::getKeys); + py::class_(mex, "ReadDataHandle") + .def(py::init([](const py::object& parent_py, py::object pytype, + const std::string& name) { + auto* parent = parent_py.cast(); + return std::make_unique(parent, + std::move(pytype), name); + }), + py::arg("parent"), py::arg("type"), py::arg("name"), + py::keep_alive<1, 2>()) + .def( + "initialize", + [](PyReadDataHandle& self, std::string_view key) { + self.initialize(key); + }, + py::arg("key")) + .def("__call__", &PyReadDataHandle::call, py::arg("whiteboard")); + py::class_(mex, "AlgorithmContext") .def(py::init(), "alg"_a, "event"_a, "store"_a, "thread"_a) diff --git a/Python/Examples/src/Generators.cpp b/Python/Examples/src/Generators.cpp index d5b57ce704d..4cc74fb276f 100644 --- a/Python/Examples/src/Generators.cpp +++ b/Python/Examples/src/Generators.cpp @@ -14,6 +14,7 @@ #include "ActsExamples/Utilities/ParametricParticleGenerator.hpp" #include "ActsExamples/Utilities/VertexGenerators.hpp" #include "ActsPython/Utilities/Macros.hpp" +#include "ActsPython/Utilities/WhiteBoardTypeRegistry.hpp" #include #include @@ -136,7 +137,15 @@ void addGenerators(py::module& mex) { .def_readwrite("fixed", &FixedPrimaryVertexPositionGenerator::fixed); py::class_(mex, "SimParticle"); - py::class_(mex, "SimParticleContainer"); + auto simParticleContainer = + py::class_(mex, "SimParticleContainer") + .def("__len__", + [](const SimParticleContainer& self) { return self.size(); }) + .def("__iter__", [](const SimParticleContainer& self) { + return py::make_iterator(self.begin(), self.end()); + }); + + WhiteBoardRegistry::registerClass(simParticleContainer); { using Config = ParametricParticleGenerator::Config; diff --git a/Python/Examples/tests/test_read_data_handle.py b/Python/Examples/tests/test_read_data_handle.py new file mode 100644 index 00000000000..0dd2edc9cc6 --- /dev/null +++ b/Python/Examples/tests/test_read_data_handle.py @@ -0,0 +1,145 @@ +import pytest +import acts +import acts.examples + +u = acts.UnitConstants + +hepmc3 = pytest.importorskip("acts.examples.hepmc3") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class _Noop(acts.examples.IAlgorithm): + """Minimal algorithm whose only purpose is to serve as a handle parent.""" + + def __init__(self): + acts.examples.IAlgorithm.__init__(self, "_Noop", acts.logging.WARNING) + + def execute(self, context): + return acts.examples.ProcessCode.SUCCESS + + +def _make_sequencer(events=3): + """Sequencer with a particle-gun → HepMC3-converter chain.""" + rng = acts.examples.RandomNumbers(seed=42) + s = acts.examples.Sequencer(events=events, numThreads=1, logLevel=acts.logging.INFO) + + evGen = acts.examples.EventGenerator( + level=acts.logging.WARNING, + generators=[ + acts.examples.EventGenerator.Generator( + multiplicity=acts.examples.FixedMultiplicityGenerator(n=1), + vertex=acts.examples.GaussianVertexGenerator( + stddev=acts.Vector4(0, 0, 0, 0), + mean=acts.Vector4(0, 0, 0, 0), + ), + particles=acts.examples.ParametricParticleGenerator( + p=(1 * u.GeV, 10 * u.GeV), + eta=(-2, 2), + numParticles=4, + ), + ) + ], + outputEvent="particle_gun_event", + randomNumbers=rng, + ) + s.addReader(evGen) + + converter = hepmc3.HepMC3InputConverter( + level=acts.logging.WARNING, + inputEvent="particle_gun_event", + outputParticles="particles_generated", + outputVertices="vertices_generated", + mergePrimaries=False, + ) + s.addAlgorithm(converter) + + return s + + +# --------------------------------------------------------------------------- +# Unit-level tests (no sequencer run needed) +# --------------------------------------------------------------------------- + + +def test_unregistered_type_raises(): + """Constructing a handle for a type without a whiteboard registration fails.""" + dummy = _Noop() + with pytest.raises(TypeError, match="not registered"): + acts.examples.ReadDataHandle(dummy, acts.examples.IAlgorithm, "test") + + +def test_not_initialized_raises(): + """Calling a handle before initialize() raises RuntimeError.""" + dummy = _Noop() + handle = acts.examples.ReadDataHandle( + dummy, acts.examples.SimParticleContainer, "InputParticles" + ) + wb = acts.examples.WhiteBoard(acts.logging.WARNING) + with pytest.raises(RuntimeError, match="not initialized"): + handle(wb) + + +def test_wrong_key_raises(): + class WrongKeyInspector(acts.examples.IAlgorithm): + def __init__(self): + super().__init__(name="WrongKeyInspector", level=acts.logging.INFO) + self.particles = acts.examples.ReadDataHandle( + self, acts.examples.SimParticleContainer, "InputParticles" + ) + self.particles.initialize("wrong_key") + + def execute(self, context): + return acts.examples.ProcessCode.SUCCESS + + s = _make_sequencer(events=3) + inspector = WrongKeyInspector() + with pytest.raises(KeyError, match="does not exist"): + wb = acts.examples.WhiteBoard(acts.logging.WARNING) + inspector.particles(wb) + + with acts.logging.ScopedFailureThreshold(acts.logging.FATAL), pytest.raises( + RuntimeError, + match="Sequence configuration error: Missing data handle for key 'wrong_key'", + ): + s.addAlgorithm(inspector) + + +# --------------------------------------------------------------------------- +# Integration test +# --------------------------------------------------------------------------- + + +def test_read_particles_via_handle(): + """A Python IAlgorithm reads SimParticleContainer from the whiteboard.""" + + class ParticleInspector(acts.examples.IAlgorithm): + def __init__(self): + super().__init__(name="ParticleInspector", level=acts.logging.INFO) + self.particles = acts.examples.ReadDataHandle( + self, acts.examples.SimParticleContainer, "InputParticles" + ) + self.particles.initialize("particles_generated") + self.seen_events = 0 + + def execute(self, context): + particles = self.particles(context.eventStore) + assert isinstance(particles, acts.examples.SimParticleContainer) + + self.logger.info("Found {} particles", len(particles)) + + for particle in particles: + print(particle) + + self.seen_events += 1 + return acts.examples.ProcessCode.SUCCESS + + s = _make_sequencer(events=3) + inspector = ParticleInspector() + s.addAlgorithm(inspector) + s.run() + + assert inspector.seen_events == 3 diff --git a/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp b/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp new file mode 100644 index 00000000000..40a328309ef --- /dev/null +++ b/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp @@ -0,0 +1,104 @@ +// 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/HashedString.hpp" +#include "Acts/Utilities/TypeTag.hpp" + +#include +#include + +#include + +namespace ActsPython { + +/// The WhiteBoard is an event-store container that holds arbitrary C++ objects +/// by name. Python algorithms need to read these objects through pybind11, but +/// the WhiteBoard stores them in a type-erased form (`void*`). This registry +/// bridges that gap by mapping Python types to their C++ counterparts and +/// providing a downcast function that converts the stored pointer into a +/// properly reference-managed pybind11 object. +/// +/// Usage: +/// 1. When defining pybind11 bindings for a type T that will be stored on the +/// WhiteBoard, call `WhiteBoardRegistry::registerClass(pyClass)` or +/// `WhiteBoardRegistry::registerType(pyType)` immediately after the +/// py::class_ definition. +/// 2. When a Python algorithm creates a `ReadDataHandle` for that type, the +/// registry is consulted to find the type info and downcast function for +/// safe retrieval from the `WhiteBoard`. +class WhiteBoardRegistry { + public: + /// Function that converts a type-erased pointer from the WhiteBoard into a + /// pybind11 object. The wbPy argument is used for reference_internal + /// lifetime. + using DowncastFunction = std::function; + + /// Register a pybind11-bound type T for WhiteBoard read access. + /// Call this after the `py::class_` definition. + /// @tparam Ts The types to register. + /// @param pyClass The pybind11 class object to register. + template + static void registerClass(const pybind11::class_& pyClass) { + namespace py = pybind11; + using type = pybind11::class_::type; + registerType(pyClass); + } + + /// Register a C++ type `~T` with its pybind11 Python type for WhiteBoard + /// access. Use when the `py::class_` type cannot be deduced (e.g. for + /// template types). + /// @tparam T The C++ type to register. + /// @param pyType The pybind11 Python type object to register. + template + static void registerType(const pybind11::object& pyType) { + namespace py = pybind11; + + using type = T; + + instance()[pyType.ptr()] = { + .fn = [](const void* ptr, const py::object& wbPy) -> py::object { + // wb needed to ensure correct lifetime + return py::cast(*static_cast(ptr), + py::return_value_policy::reference_internal, wbPy); + }, + .typeinfo = &typeid(type), + .typeHash = Acts::typeHash(), + }; + } + + /// Per-type registry entry: downcast function and type metadata for lookups. + struct RegistryEntry { + DowncastFunction fn{ + nullptr}; ///< Converts `void*` + `WhiteBoard` -> `py::object` + const std::type_info* typeinfo{nullptr}; ///< C++ type for type checking + std::uint64_t typeHash{0}; ///< Hash for runtime type verification + }; + + /// Look up a registered type by its pybind11 Python type object. + /// @param pyType The pybind11 Python type object to look up. + /// @return Pointer to the `RegistryEntry`, or `nullptr` if not registered + static RegistryEntry* find(const pybind11::object& pyType) { + if (auto it = instance().find(pyType.ptr()); it != instance().end()) { + return &it->second; + } + return nullptr; + } + + private: + WhiteBoardRegistry() = default; + + static inline std::unordered_map& instance() { + static std::unordered_map map; + return map; + } +}; + +} // namespace ActsPython