From 5b1daedc97da3c3f795be03cc1af57e7d936ce81 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Mon, 23 Feb 2026 15:18:06 +0100 Subject: [PATCH 01/12] python access to whiteboard --- Core/include/Acts/Utilities/Any.hpp | 6 - Core/include/Acts/Utilities/HashedString.hpp | 7 + .../ActsExamples/Framework/DataHandle.hpp | 9 ++ .../ActsExamples/Framework/WhiteBoard.hpp | 16 ++- .../Framework/src/Framework/DataHandle.cpp | 8 +- .../Framework/src/Framework/WhiteBoard.cpp | 18 +++ .../Io/Podio/CollectionBaseWriteHandle.hpp | 2 + .../Podio/src/CollectionBaseWriteHandle.cpp | 4 + Python/Examples/src/Framework.cpp | 72 ++++++++++- Python/Examples/tests/test_fpe.py | 6 + .../Examples/tests/test_read_data_handle.py | 121 ++++++++++++++++++ .../Utilities/WhiteBoardTypeRegistry.hpp | 65 ++++++++++ 12 files changed, 321 insertions(+), 13 deletions(-) create mode 100644 Python/Examples/tests/test_read_data_handle.py create mode 100644 Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp 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..81e7671000f 100644 --- a/Core/include/Acts/Utilities/HashedString.hpp +++ b/Core/include/Acts/Utilities/HashedString.hpp @@ -76,4 +76,11 @@ constexpr HashedString operator""_hash(char const* s, std::size_t count) { } } // namespace HashedStringLiteral + +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..806f6af429f 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 @@ -28,14 +29,16 @@ namespace ActsExamples { /// /// This is an append-only container that takes ownership of the objects /// added to it. Once an object has been added, it can only be read but not -/// be modified. Trying to replace an existing object is considered an error. -/// Its lifetime is bound to the lifetime of the white board. +/// be modified. Trying to replace an existing object is considered an +/// error. Its lifetime is bound to the lifetime of the white board. class WhiteBoard { private: // type-erased value holder for move-constructible types 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 { @@ -82,6 +87,11 @@ class WhiteBoard { std::vector getKeys() const; + /// Returns a (value_ptr, type_info*) pair for use with Python bindings. + /// Returns {nullptr, nullptr} if the key does not exist. + // std::pair getTypeErased( + // const std::string& name) const; + private: /// Find similar names for suggestions with levenshtein-distance std::vector similarNames(const std::string_view& name, @@ -116,6 +126,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..acd61ad0f53 100644 --- a/Examples/Framework/src/Framework/WhiteBoard.cpp +++ b/Examples/Framework/src/Framework/WhiteBoard.cpp @@ -136,4 +136,22 @@ std::vector WhiteBoard::getKeys() const { return keys; } +// std::pair WhiteBoard::getTypeErased( +// const std::string &name) const { +// auto it = m_store.find(name); +// if (it == m_store.end()) { +// return {nullptr, nullptr}; +// } +// const IHolder *h = it->second.get(); +// return {h->data(), &h->type()}; +// } + +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..2a6ccccfdd3 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 @@ -87,6 +91,55 @@ class PyIAlgorithm : public IAlgorithm { 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->typeinfo == nullptr) { + throw py::type_error("Type '" + + pytype.attr("__qualname__").cast() + + "' is not registered for WhiteBoard access"); + } + + m_pytype = std::move(pytype); + // Can't use the main `typeHash` function here because it's not a template + 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() + "'. Exptected " + + 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 +185,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/tests/test_fpe.py b/Python/Examples/tests/test_fpe.py index 77fdc36ac67..4e9b64094ac 100644 --- a/Python/Examples/tests/test_fpe.py +++ b/Python/Examples/tests/test_fpe.py @@ -48,10 +48,16 @@ class FpeMaker(acts.examples.IAlgorithm): def __init__(self, name): acts.examples.IAlgorithm.__init__(self, name, acts.logging.INFO) + self.space_points = acts.examples.ReadDataHandle( + acts.SpacePoints, "space_points" + ) + self.space_points.initialize("space_points") def execute(self, context): i = context.eventNumber % 4 + sps: acts.SpacePoints = self.spacepoints(context.eventStore) + if i == 0 or i == 1: acts.examples.FpeMonitor._trigger_divbyzero() elif i == 2: 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..6efc66e771a --- /dev/null +++ b/Python/Examples/tests/test_read_data_handle.py @@ -0,0 +1,121 @@ +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) + + +# --------------------------------------------------------------------------- +# 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(f"Found {len(particles)} particles") + self.logger.info("Found {} particles <- this also works", 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..60944928adc --- /dev/null +++ b/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp @@ -0,0 +1,65 @@ +// 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 "ActsExamples/Framework/WhiteBoard.hpp" + +#include +#include +#include + +#include + +namespace ActsPython { + +class WhiteBoardRegistry { + public: + using DowncastFunction = std::function; + + /// Register a pybind11-bound type T for WhiteBoard read access. + /// Call this immediately after the py::class_ definition. + template + static void registerType(const pybind11::class_& pyType) { + namespace py = pybind11; + + using type = pybind11::class_::type; + + instance()[pyType.ptr()] = { + .fn = [](const void* ptr, const py::object& wbPy) -> py::object { + // wb py seems to be needed to ensure correct lifetime + return py::cast(*static_cast(ptr), + py::return_value_policy::reference_internal, wbPy); + }, + .typeinfo = &typeid(type), + .typeHash = Acts::typeHash(), + }; + } + + struct RegistryEntry { + DowncastFunction fn{nullptr}; + const std::type_info* typeinfo{nullptr}; + std::uint64_t typeHash{0}; + }; + + static RegistryEntry& find(const pybind11::object& pyType) { + return instance().at(pyType.ptr()); + } + + private: + WhiteBoardRegistry() = default; + + static inline std::unordered_map& instance() { + static std::unordered_map map; + return map; + } +}; + +} // namespace ActsPython From 4ff29731bd51bf4544f02b8552527ee37224580f Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Mon, 23 Feb 2026 18:20:22 +0100 Subject: [PATCH 02/12] fix type mismatch error --- Python/Examples/src/Framework.cpp | 7 ++++++- .../ActsPython/Utilities/WhiteBoardTypeRegistry.hpp | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Python/Examples/src/Framework.cpp b/Python/Examples/src/Framework.cpp index 2a6ccccfdd3..90f120534d5 100644 --- a/Python/Examples/src/Framework.cpp +++ b/Python/Examples/src/Framework.cpp @@ -96,7 +96,12 @@ class PyReadDataHandle : public ReadDataHandleBase { PyReadDataHandle(SequenceElement* parent, py::object pytype, const std::string& name) : ReadDataHandleBase(parent, name) { - m_entry = &WhiteBoardRegistry::find(pytype); + 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() + diff --git a/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp b/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp index 60944928adc..fa78e008bfe 100644 --- a/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp +++ b/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp @@ -49,8 +49,11 @@ class WhiteBoardRegistry { std::uint64_t typeHash{0}; }; - static RegistryEntry& find(const pybind11::object& pyType) { - return instance().at(pyType.ptr()); + static RegistryEntry* find(const pybind11::object& pyType) { + if (auto it = instance().find(pyType.ptr()); it != instance().end()) { + return &it->second; + } + return nullptr; } private: From 5adabc630387859923768d3efc5a63ff4aa5adbb Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Mon, 23 Feb 2026 18:24:17 +0100 Subject: [PATCH 03/12] test error types from python --- .../Examples/tests/test_read_data_handle.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Python/Examples/tests/test_read_data_handle.py b/Python/Examples/tests/test_read_data_handle.py index 6efc66e771a..0aa74d9bae3 100644 --- a/Python/Examples/tests/test_read_data_handle.py +++ b/Python/Examples/tests/test_read_data_handle.py @@ -83,6 +83,32 @@ def test_not_initialized_raises(): handle(wb) +@acts.with_log_threshold(acts.logging.FATAL) +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 pytest.raises( + RuntimeError, + match="Sequence configuration error: Missing data handle for key 'wrong_key'", + ): + s.addAlgorithm(inspector) + + # --------------------------------------------------------------------------- # Integration test # --------------------------------------------------------------------------- From 396d645ce8fe4f21473b3ae9ed83e7548c54c531 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 24 Feb 2026 14:20:02 +0100 Subject: [PATCH 04/12] diff cleanup --- .../include/ActsExamples/Framework/WhiteBoard.hpp | 9 ++------- Examples/Framework/src/Framework/WhiteBoard.cpp | 10 ---------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/Examples/Framework/include/ActsExamples/Framework/WhiteBoard.hpp b/Examples/Framework/include/ActsExamples/Framework/WhiteBoard.hpp index 806f6af429f..a69a85b967e 100644 --- a/Examples/Framework/include/ActsExamples/Framework/WhiteBoard.hpp +++ b/Examples/Framework/include/ActsExamples/Framework/WhiteBoard.hpp @@ -29,8 +29,8 @@ namespace ActsExamples { /// /// This is an append-only container that takes ownership of the objects /// added to it. Once an object has been added, it can only be read but not -/// be modified. Trying to replace an existing object is considered an -/// error. Its lifetime is bound to the lifetime of the white board. +/// be modified. Trying to replace an existing object is considered an error. +/// Its lifetime is bound to the lifetime of the white board. class WhiteBoard { private: // type-erased value holder for move-constructible types @@ -87,11 +87,6 @@ class WhiteBoard { std::vector getKeys() const; - /// Returns a (value_ptr, type_info*) pair for use with Python bindings. - /// Returns {nullptr, nullptr} if the key does not exist. - // std::pair getTypeErased( - // const std::string& name) const; - private: /// Find similar names for suggestions with levenshtein-distance std::vector similarNames(const std::string_view& name, diff --git a/Examples/Framework/src/Framework/WhiteBoard.cpp b/Examples/Framework/src/Framework/WhiteBoard.cpp index acd61ad0f53..16d9dfa8fc0 100644 --- a/Examples/Framework/src/Framework/WhiteBoard.cpp +++ b/Examples/Framework/src/Framework/WhiteBoard.cpp @@ -136,16 +136,6 @@ std::vector WhiteBoard::getKeys() const { return keys; } -// std::pair WhiteBoard::getTypeErased( -// const std::string &name) const { -// auto it = m_store.find(name); -// if (it == m_store.end()) { -// return {nullptr, nullptr}; -// } -// const IHolder *h = it->second.get(); -// return {h->data(), &h->type()}; -// } - WhiteBoard::IHolder *WhiteBoard::getHolder(const std::string &name) const { auto it = m_store.find(name); if (it == m_store.end()) { From 7c7595e002f473575989c234c59ea8106f0d6473 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 24 Feb 2026 21:03:51 +0100 Subject: [PATCH 05/12] add missing include in hashed string --- Core/include/Acts/Utilities/HashedString.hpp | 3 +-- Python/Examples/tests/test_read_data_handle.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Core/include/Acts/Utilities/HashedString.hpp b/Core/include/Acts/Utilities/HashedString.hpp index 81e7671000f..f082b196cd2 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 diff --git a/Python/Examples/tests/test_read_data_handle.py b/Python/Examples/tests/test_read_data_handle.py index 0aa74d9bae3..6038861b163 100644 --- a/Python/Examples/tests/test_read_data_handle.py +++ b/Python/Examples/tests/test_read_data_handle.py @@ -83,7 +83,6 @@ def test_not_initialized_raises(): handle(wb) -@acts.with_log_threshold(acts.logging.FATAL) def test_wrong_key_raises(): class WrongKeyInspector(acts.examples.IAlgorithm): def __init__(self): From b340ff2f091001a8d08e633e8ed695f504fc8ecf Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 24 Feb 2026 21:09:47 +0100 Subject: [PATCH 06/12] consistency, tests actually work --- Python/Examples/src/Generators.cpp | 11 ++++++++++- Python/Examples/tests/test_read_data_handle.py | 5 ++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Python/Examples/src/Generators.cpp b/Python/Examples/src/Generators.cpp index d5b57ce704d..9c0b0aeae81 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::registerType(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 index 6038861b163..5b4e678876c 100644 --- a/Python/Examples/tests/test_read_data_handle.py +++ b/Python/Examples/tests/test_read_data_handle.py @@ -101,7 +101,7 @@ def execute(self, context): wb = acts.examples.WhiteBoard(acts.logging.WARNING) inspector.particles(wb) - with pytest.raises( + with acts.logging.ScopedFailureThreshold(acts.logging.FATAL), pytest.raises( RuntimeError, match="Sequence configuration error: Missing data handle for key 'wrong_key'", ): @@ -129,8 +129,7 @@ def execute(self, context): particles = self.particles(context.eventStore) assert isinstance(particles, acts.examples.SimParticleContainer) - self.logger.info(f"Found {len(particles)} particles") - self.logger.info("Found {} particles <- this also works", len(particles)) + print(f"Found {len(particles)} particles") for particle in particles: print(particle) From 8b989193d566fdb075c3e49147b0423d6e39ceff Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 26 Feb 2026 14:23:13 +0100 Subject: [PATCH 07/12] spelling --- Python/Examples/src/Framework.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/Examples/src/Framework.cpp b/Python/Examples/src/Framework.cpp index 90f120534d5..202a829da91 100644 --- a/Python/Examples/src/Framework.cpp +++ b/Python/Examples/src/Framework.cpp @@ -133,7 +133,7 @@ class PyReadDataHandle : public ReadDataHandleBase { 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() + "'. Exptected " + + throw py::type_error("Type mismatch for key '" + key() + "'. Expected " + expected + " but got " + actual); } From 12c863100c5ea15b86925dc299665f05df444c6a Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 26 Feb 2026 14:25:03 +0100 Subject: [PATCH 08/12] doc for typehash --- Core/include/Acts/Utilities/HashedString.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Core/include/Acts/Utilities/HashedString.hpp b/Core/include/Acts/Utilities/HashedString.hpp index f082b196cd2..7588ae6f0f6 100644 --- a/Core/include/Acts/Utilities/HashedString.hpp +++ b/Core/include/Acts/Utilities/HashedString.hpp @@ -76,6 +76,10 @@ 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()); From 9cc4f03b2cd1be91a1171d2980b0f1b7c59476a7 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 25 Feb 2026 16:51:35 +0100 Subject: [PATCH 09/12] wb reg interface change --- Python/Examples/src/Generators.cpp | 2 +- .../Utilities/WhiteBoardTypeRegistry.hpp | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Python/Examples/src/Generators.cpp b/Python/Examples/src/Generators.cpp index 9c0b0aeae81..4cc74fb276f 100644 --- a/Python/Examples/src/Generators.cpp +++ b/Python/Examples/src/Generators.cpp @@ -145,7 +145,7 @@ void addGenerators(py::module& mex) { return py::make_iterator(self.begin(), self.end()); }); - WhiteBoardRegistry::registerType(simParticleContainer); + WhiteBoardRegistry::registerClass(simParticleContainer); { using Config = ParametricParticleGenerator::Config; diff --git a/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp b/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp index fa78e008bfe..eb0ed6ac0e4 100644 --- a/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp +++ b/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp @@ -9,10 +9,9 @@ #pragma once #include "Acts/Utilities/HashedString.hpp" -#include "ActsExamples/Framework/WhiteBoard.hpp" +#include "Acts/Utilities/TypeTag.hpp" #include -#include #include #include @@ -27,14 +26,21 @@ class WhiteBoardRegistry { /// Register a pybind11-bound type T for WhiteBoard read access. /// Call this immediately after the py::class_ definition. template - static void registerType(const pybind11::class_& pyType) { + static void registerClass(const pybind11::class_& pyClass) { namespace py = pybind11; - using type = pybind11::class_::type; + registerType(pyClass); + } + + 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 py seems to be needed to ensure correct lifetime + // wb needed to ensure correct lifetime return py::cast(*static_cast(ptr), py::return_value_policy::reference_internal, wbPy); }, From 2fa0cfdc64da793f3427a5a6f629062e25eabcbb Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 26 Feb 2026 14:50:10 +0100 Subject: [PATCH 10/12] cleanup, documentation --- Python/Examples/src/Framework.cpp | 1 - Python/Examples/tests/test_fpe.py | 6 --- .../Utilities/WhiteBoardTypeRegistry.hpp | 38 +++++++++++++++++-- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/Python/Examples/src/Framework.cpp b/Python/Examples/src/Framework.cpp index 202a829da91..5a68895576f 100644 --- a/Python/Examples/src/Framework.cpp +++ b/Python/Examples/src/Framework.cpp @@ -109,7 +109,6 @@ class PyReadDataHandle : public ReadDataHandleBase { } m_pytype = std::move(pytype); - // Can't use the main `typeHash` function here because it's not a template registerAsReadHandle(); } diff --git a/Python/Examples/tests/test_fpe.py b/Python/Examples/tests/test_fpe.py index 4e9b64094ac..77fdc36ac67 100644 --- a/Python/Examples/tests/test_fpe.py +++ b/Python/Examples/tests/test_fpe.py @@ -48,16 +48,10 @@ class FpeMaker(acts.examples.IAlgorithm): def __init__(self, name): acts.examples.IAlgorithm.__init__(self, name, acts.logging.INFO) - self.space_points = acts.examples.ReadDataHandle( - acts.SpacePoints, "space_points" - ) - self.space_points.initialize("space_points") def execute(self, context): i = context.eventNumber % 4 - sps: acts.SpacePoints = self.spacepoints(context.eventStore) - if i == 0 or i == 1: acts.examples.FpeMonitor._trigger_divbyzero() elif i == 2: diff --git a/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp b/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp index eb0ed6ac0e4..40a328309ef 100644 --- a/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp +++ b/Python/Utilities/include/ActsPython/Utilities/WhiteBoardTypeRegistry.hpp @@ -18,13 +18,33 @@ 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 immediately after the py::class_ definition. + /// 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; @@ -32,6 +52,11 @@ class WhiteBoardRegistry { 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; @@ -49,12 +74,17 @@ class WhiteBoardRegistry { }; } + /// Per-type registry entry: downcast function and type metadata for lookups. struct RegistryEntry { - DowncastFunction fn{nullptr}; - const std::type_info* typeinfo{nullptr}; - std::uint64_t typeHash{0}; + 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; From 8cfe74a6038f28f8450369f26d33235aea8f500d Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 24 Feb 2026 21:25:03 +0100 Subject: [PATCH 11/12] use logger in data handle test --- Python/Examples/tests/test_read_data_handle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/Examples/tests/test_read_data_handle.py b/Python/Examples/tests/test_read_data_handle.py index 5b4e678876c..0dd2edc9cc6 100644 --- a/Python/Examples/tests/test_read_data_handle.py +++ b/Python/Examples/tests/test_read_data_handle.py @@ -129,7 +129,7 @@ def execute(self, context): particles = self.particles(context.eventStore) assert isinstance(particles, acts.examples.SimParticleContainer) - print(f"Found {len(particles)} particles") + self.logger.info("Found {} particles", len(particles)) for particle in particles: print(particle) From c1e648f568769155c70de3cb5235297bfc9a8fc7 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 25 Feb 2026 11:34:22 +0100 Subject: [PATCH 12/12] fix issue with python init/finalize not being called --- Python/Examples/src/Framework.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Python/Examples/src/Framework.cpp b/Python/Examples/src/Framework.cpp index 5a68895576f..0967d335af8 100644 --- a/Python/Examples/src/Framework.cpp +++ b/Python/Examples/src/Framework.cpp @@ -86,6 +86,16 @@ 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(); }