diff --git a/Core/include/Acts/Geometry/BlueprintBuilder.hpp b/Core/include/Acts/Geometry/BlueprintBuilder.hpp new file mode 100644 index 00000000000..c276ce58491 --- /dev/null +++ b/Core/include/Acts/Geometry/BlueprintBuilder.hpp @@ -0,0 +1,1202 @@ +// 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/Definitions/Algebra.hpp" +#include "Acts/Geometry/BlueprintNode.hpp" +#include "Acts/Geometry/ContainerBlueprintNode.hpp" +#include "Acts/Geometry/Extent.hpp" +#include "Acts/Geometry/LayerBlueprintNode.hpp" +#include "Acts/Geometry/NavigationPolicyFactory.hpp" +#include "Acts/Geometry/VolumeAttachmentStrategy.hpp" +#include "Acts/Navigation/CylinderNavigationPolicy.hpp" +#include "Acts/Surfaces/Surface.hpp" +#include "Acts/Utilities/Logger.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Acts::Experimental { + +namespace detail { + +/// @brief Concept requiring @p BackendT to expose a nested `LayerSpec` type. +/// +/// A backend satisfying this concept must define a `LayerSpec` struct that +/// carries the configuration needed to construct a single detector layer. +template +concept HasLayerSpec = requires { typename BackendT::LayerSpec; }; + +/// @brief Concept requiring a backend to expose axis-definition support in its +/// `LayerSpec`. +/// +/// In addition to satisfying @ref HasLayerSpec, the backend must define an +/// `AxisDefinition` type and the corresponding `LayerSpec` must have +/// - `axes` : optional axes used to orient sensitive surfaces, and +/// - `layerAxes` : optional axes used to determine the layer transform from the +/// parent detector element shape. +template +concept HasAxisDefinition = + HasLayerSpec && requires { typename BackendT::AxisDefinition; } && + requires(typename BackendT::LayerSpec layerSpec, + typename BackendT::AxisDefinition axes, + std::optional layerAxes) { + layerSpec.axes = std::move(axes); + { layerSpec.axes.has_value() } -> std::convertible_to; + layerSpec.layerAxes = std::move(axes); + layerSpec.layerAxes = std::move(layerAxes); + }; + +using LayerNodePtr = std::shared_ptr; +using ContainerNodePtr = std::shared_ptr; +using SurfacePtr = std::shared_ptr; +using SurfaceVector = std::vector; + +/// @brief Callback type that can replace or wrap a @ref LayerBlueprintNode. +/// +/// Receives the source layer element (or @c std::nullopt when no element +/// context exists) and the newly created layer node, and returns the +/// (possibly replaced) node to be added to the container. +template +using LayerCustomizer = std::function( + const std::optional&, std::shared_ptr)>; + +/// @brief Callback type that can replace or wrap a +/// @ref CylinderContainerBlueprintNode. +template +using ContainerCustomizer = + std::function( + const ElementT&, std::shared_ptr)>; + +/// @brief Concept satisfied when @p CallableT can be called with an optional +/// element and a @ref LayerBlueprintNode shared pointer and returns a (possibly +/// different) @ref LayerBlueprintNode shared pointer. +/// +/// Used to constrain the returning form of the `onLayer` callback accepted by +/// the assembler builders. +template +concept LayerNodeReturningCallable = + std::invocable&, LayerNodePtr> && + std::same_as&, LayerNodePtr>, + LayerNodePtr>; + +/// @brief Concept satisfied when @p CallableT can be called with an optional +/// element and a mutable @ref LayerBlueprintNode reference and returns `void`. +/// +/// Used to constrain the in-place (mutating) form of the `onLayer` callback. +template +concept LayerNodeReplacingCallable = + std::invocable&, + LayerBlueprintNode&> && + std::same_as< + std::invoke_result_t&, + LayerBlueprintNode&>, + void>; + +/// @brief Concept satisfied when @p CallableT can be called with an element and +/// a @ref ContainerBlueprintNode shared pointer and returns a (possibly +/// different) @ref ContainerBlueprintNode shared pointer. +/// +/// Used to constrain the returning form of the `onContainer` callback. +template +concept ContainerNodeReturningCallable = + std::invocable && + std::same_as< + std::invoke_result_t, + ContainerNodePtr>; + +/// @brief Concept satisfied when @p CallableT can be called with an element and +/// a mutable @ref ContainerBlueprintNode reference and returns `void`. +/// +/// Used to constrain the in-place (mutating) form of the `onContainer` +/// callback. +template +concept ContainerNodeReplacingCallable = + std::invocable && + std::same_as, + void>; + +/// @brief Concept requiring a backend to provide a surface-construction method. +/// +/// The method must accept a span of sensitive child elements plus a `LayerSpec` +/// and return the corresponding ACTS surfaces. +template +concept HasSurfaceFactory = + HasLayerSpec && + requires(const BackendT& backend, + std::span sensitives, + const typename BackendT::LayerSpec& layerSpec) { + { + backend.makeSurfaces(sensitives, layerSpec) + } -> std::same_as; + }; + +/// @brief Optional backend capability to extract a layer transform from one +/// context element and a layer specification. +/// +/// Backends that satisfy this concept may provide automatic layer-transform +/// extraction (for example from detector-element geometry). The interface layer +/// applies this only when a context element is available. +template +concept HasLayerTransformLookup = + HasLayerSpec && + requires(const BackendT& backend, const typename BackendT::Element& elem, + const typename BackendT::LayerSpec& layerSpec) { + { + backend.lookupLayerTransform(elem, layerSpec) + } -> std::same_as>; + }; + +/// @brief Concept requiring `LayerSpec` to carry an optional `layerName` field. +/// +/// The `layerName` member, when set, overrides the name derived from the +/// detector element hierarchy when constructing a layer node. +template +concept HasLayerNameMember = + HasLayerSpec && requires(typename BackendT::LayerSpec layerSpec, + std::optional layerName) { + layerSpec.layerName = std::move(layerName); + { layerSpec.layerName.has_value() } -> std::convertible_to; + }; + +/// @brief Optional backend capability for barrel/endcap assembly discovery. +template +concept HasBarrelEndcapClassifier = requires( + const BackendT& backend, const typename BackendT::Element& element) { + { backend.isBarrel(element) } -> std::same_as; + { backend.isEndcap(element) } -> std::same_as; + { backend.isTracker(element) } -> std::same_as; +}; + +/// @brief Concept that fully constrains a geometry backend usable with +/// @ref BlueprintBuilder. +/// +/// A conforming backend must: +/// - satisfy @ref HasSurfaceFactory and @ref HasLayerNameMember, +/// - be constructible from a `Config` object and an `Acts::Logger` reference, +/// - expose the element-hierarchy query interface (`world`, `nameOf`, +/// `children`, `parent`), +/// - expose `isSensitive()`, and +/// - support equality comparison between `Element` instances, and +/// - define `static constexpr std::string_view kIdentifier`. +template +concept BlueprintBackend = + HasSurfaceFactory && HasLayerNameMember && + requires(const typename BackendT::Config& cfg, const Acts::Logger& logger, + const BackendT& backend, + const typename BackendT::Element& element) { + BackendT{cfg, logger}; + { BackendT::kIdentifier } -> std::convertible_to; + { backend.world() } -> std::same_as; + { backend.nameOf(element) } -> std::same_as; + { + backend.children(element) + } -> std::same_as>; + { backend.parent(element) } -> std::same_as; + { backend.isSensitive(element) } -> std::same_as; + requires requires(const typename BackendT::Element& a, + const typename BackendT::Element& b) { + { a == b } -> std::convertible_to; + }; + }; + +} // namespace detail + +template +class BlueprintBuilder; + +template +class SensorLayerAssembler; + +template +class SensorLayer; + +/// @brief Fluent builder that assembles a flat collection of cylindrical or +/// disc-like detector layers from layer-representative detector elements into a +/// @ref CylinderContainerBlueprintNode. +/// +/// Each supplied element represents one layer: its hierarchy path is used as +/// the layer name and (optionally) its geometry drives the layer transform. +/// Obtained from @ref BlueprintBuilder::layers(). +/// +/// ```cpp +/// builder.layers() +/// .barrel() +/// .setSensorAxes(myAxes) +/// .setLayerFilter(layerPattern) +/// .setContainer(containerElement) +/// .addTo(parentNode); +/// ``` +/// +/// @tparam BackendT Geometry backend that provides detector elements, layer +/// specifications, hierarchy traversal, sensitive-element +/// classification, and surface construction. +template +class ElementLayerAssembler { + public: + /// The associated @ref BlueprintBuilder type. + using Builder = BlueprintBuilder; + /// Distinguishes barrel (Cylinder) from endcap (Disc) layer geometry. + using LayerType = Acts::Experimental::LayerBlueprintNode::LayerType; + /// Backend detector element handle type. + using Element = typename BackendT::Element; + /// Backend layer-specification type. + using LayerSpec = typename BackendT::LayerSpec; + /// Axis definition type, or `std::monostate` when the backend does not + /// support axis definitions. + using AxisDefinition = + std::conditional_t, + typename BackendT::AxisDefinition, std::monostate>; + /// Callback type that can replace or wrap a @ref LayerBlueprintNode. + using LayerCustomizer = detail::LayerCustomizer; + + /// @brief Set the layer geometry type explicitly. + /// @param layerType `LayerType::Cylinder` for barrel, `LayerType::Disc` for + /// endcap. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& setLayerType(LayerType layerType) &&; + + /// @brief Shorthand for `setLayerType(LayerType::Disc)`. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& endcap() &&; + + /// @brief Shorthand for `setLayerType(LayerType::Cylinder)`. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& barrel() &&; + + /// @brief Shorthand for `setLayerType(LayerType::Plane)`. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& planar() &&; + + /// @brief Set the axis definition used to orient sensitive surfaces. + /// + /// Only available when the backend defines an @ref AxisDefinition type and + /// stores optional surface-axis information in `LayerSpec`. + /// @param axes Axis definition forwarded to `LayerSpec::axes`. + /// @return `*this` (rvalue). + template + [[nodiscard]] ElementLayerAssembler&& setSensorAxes( + typename B::AxisDefinition axes) && + requires(detail::HasAxisDefinition) { + m_layerSpec.axes = std::move(axes); + return std::move(*this); + } + + /// @brief Set the axis definition used to derive the layer transform from the + /// parent element shape. + /// + /// Only available when the backend defines an @ref AxisDefinition type and + /// stores optional layer-axis information in `LayerSpec`. + /// When set, the layer transform is extracted automatically from the + /// geometry of the enclosing detector element. + /// @param layerAxes Axis definition forwarded to `LayerSpec::layerAxes`. + /// @return `*this` (rvalue). + template + [[nodiscard]] ElementLayerAssembler&& setLayerAxes( + typename B::AxisDefinition layerAxes) && + requires(detail::HasAxisDefinition) { + m_layerSpec.layerAxes = std::move(layerAxes); + return std::move(*this); + } + + /// @brief Set the regex filter used to select layer elements inside the + /// container by name string. + /// @param pattern Regular-expression string; converted to `std::regex` + /// internally. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& setLayerFilter( + const std::string& pattern) &&; + + /// @brief Set the regex filter used to select layer elements inside the + /// container. + /// @param pattern Pre-compiled regular expression matched against each + /// child element name. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& setLayerFilter( + const std::regex& pattern) &&; + + /// @brief Set the detector element that acts as the containing volume for the + /// layer search. + /// @param container Element whose subtree is searched for layers matching the + /// filter. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& setContainer( + const Element& container) &&; + + /// @brief Override the output container node name. + /// + /// If set, this name is used verbatim for the produced + /// @ref CylinderContainerBlueprintNode. Otherwise, the name is taken from the + /// configured container element (when @ref setContainer is used). + /// @param containerName Explicit container-node name to use. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& setContainerName( + std::string containerName) &&; + + /// @brief Set an explicit suffix for produced layer node names. + /// + /// The final node name is `"|"`. Use `std::nullopt` to + /// clear a previously configured suffix. + /// @param layerNameSuffix Optional suffix appended to each produced layer + /// name. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& setLayerNameSuffix( + const std::optional& layerNameSuffix) &&; + + /// @brief Set an explicit list of layer-representative elements. + /// + /// When set, the assembler skips subtree discovery via `setContainer()` and + /// uses these elements directly as layer representatives. `setLayerFilter()` + /// still applies as an optional post-filter on this list. Set either + /// @ref setContainerName or @ref setContainer to define the output container + /// name. + /// @param layerElements Layer-representative elements; one layer per element. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& setLayerElements( + std::vector layerElements) &&; + + /// @brief Set the container element by name, searching from the world root. + /// + /// @throws std::runtime_error if no element with @p name is found. + /// @param name Name of the detector element to use as the container. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& setContainer( + const std::string& name) &&; + + /// @brief Set an envelope to be applied to every layer node produced. + /// @param envelope Envelope margins added around each layer's extent. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& setEnvelope( + const Acts::ExtentEnvelope& envelope) &&; + + /// @brief Control whether an empty layer collection is an error. + /// + /// When @p emptyOk is `false` (the default) and no layers are found in the + /// container, @ref build() throws. Setting it to `true` downgrades the + /// failure to an informational log message. + /// @param emptyOk If `true`, silently accept an empty result. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& setEmptyOk(bool emptyOk) &&; + + /// @brief Register a callback invoked for each created layer node. + /// + /// The callback may either: + /// - return a (possibly replaced/wrapped) @ref LayerBlueprintNode, or + /// - mutate a @ref LayerBlueprintNode in-place and return `void`. + /// + /// In both cases, the first argument is the source layer element. + /// @param customizer Callback applied to each created layer node. + /// @return `*this` (rvalue). + template + [[nodiscard]] ElementLayerAssembler&& onLayer(CustomizerT customizer) && + requires( + detail::LayerNodeReturningCallable> || + detail::LayerNodeReplacingCallable>) + { + if constexpr (detail::LayerNodeReturningCallable< + Element, std::decay_t>) { + m_onLayer = std::move(customizer); + } else { + m_onLayer = [customizer = std::move(customizer)]( + const std::optional& layerElement, + std::shared_ptr layer) mutable { + customizer(layerElement, *layer); + return layer; + }; + } + return std::move(*this); + } + + /// @brief Override the attachment strategy for the container node. + /// + /// When unset the backend's default strategy is used. + /// @param strategy Optional attachment strategy; pass `std::nullopt` to + /// reset to the default. + /// @return `*this` (rvalue). + [[nodiscard]] ElementLayerAssembler&& setAttachmentStrategy( + std::optional strategy) &&; + + /// @brief Build and return the assembled container node. + /// + /// Each resolved layer element becomes exactly one @ref LayerBlueprintNode. + /// The layer name is derived from the element's full path in the hierarchy + /// (plus an optional suffix). The layer transform is deduced from the element + /// when `setLayerAxes()` is configured. + /// + /// @throws std::runtime_error if the layer type has not been set, if the + /// backend requires axes and none were provided, if neither a layer + /// filter nor explicit layer elements have been provided, if no + /// container name is resolvable, or if the container yields no + /// matching elements and @p emptyOk is `false`. + /// @return Shared pointer to the fully assembled container node. + [[nodiscard]] std::shared_ptr + build() const; + + /// @brief Build the container node and attach it as a child of @p node. + /// + /// Equivalent to `node.addChild(build())`. + /// @param node Blueprint node that will receive the built container as a + /// child. + void addTo(Acts::Experimental::BlueprintNode& node) const&&; + + private: + friend class BlueprintBuilder; + + /// @brief Construct an @ref ElementLayerAssembler bound to @p builder. + /// @param builder The owning @ref BlueprintBuilder; must outlive this object. + explicit ElementLayerAssembler(const Builder& builder); + + const Builder* m_builder = nullptr; + std::optional m_layerType; + LayerSpec m_layerSpec{}; + std::optional m_filter; + std::optional m_container; + std::optional m_containerName; + std::optional> m_layerElements; + std::optional m_envelope; + std::optional m_attachmentStrategy; + bool m_emptyOk = false; + LayerCustomizer m_onLayer; +}; + +/// @brief Fluent builder that assembles multiple cylindrical or disc-like +/// detector layers directly from sensor elements into a +/// @ref CylinderContainerBlueprintNode. +/// +/// Unlike @ref ElementLayerAssembler, no layer-representative element is +/// assumed to exist. Names and transforms are never deduced from the element +/// hierarchy; they come solely from @ref groupBy keys. +/// Obtained from @ref BlueprintBuilder::layersFromSensors(). +/// +/// A `groupBy` function is required: sensors mapped to the same key are merged +/// into one layer, and the key becomes the layer name. +/// +/// For a single layer (no grouping), use @ref SensorLayer via +/// @ref BlueprintBuilder::layerFromSensors() instead. +/// +/// ```cpp +/// builder.layersFromSensors() +/// .barrel() +/// .setSensorAxes(myAxes) +/// .setSensors(sensorElements) +/// .groupBy(keyExtractor) +/// .setContainerName("MyBarrel") +/// .addTo(parentNode); +/// ``` +/// +/// @tparam BackendT Geometry backend that provides detector elements, layer +/// specifications, hierarchy traversal, sensitive-element +/// classification, and surface construction. +template +class SensorLayerAssembler { + public: + /// The associated @ref BlueprintBuilder type. + using Builder = BlueprintBuilder; + /// Distinguishes barrel (Cylinder) from endcap (Disc) layer geometry. + using LayerType = LayerBlueprintNode::LayerType; + /// Backend detector element handle type. + using Element = typename BackendT::Element; + /// Backend layer-specification type. + using LayerSpec = typename BackendT::LayerSpec; + /// Axis definition type, or `std::monostate` when the backend does not + /// support axis definitions. + using AxisDefinition = + std::conditional_t, + typename BackendT::AxisDefinition, std::monostate>; + /// Callback type that can replace or wrap a @ref LayerBlueprintNode. + using LayerCustomizer = detail::LayerCustomizer; + /// Callable that maps a sensor element to a string group key. + using LayerGrouper = std::function; + + /// @brief Set the layer geometry type explicitly. + /// @param layerType `LayerType::Cylinder` for barrel, `LayerType::Disc` for + /// endcap. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayerAssembler&& setLayerType(LayerType layerType) &&; + + /// @brief Shorthand for `setLayerType(LayerType::Disc)`. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayerAssembler&& endcap() &&; + + /// @brief Shorthand for `setLayerType(LayerType::Cylinder)`. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayerAssembler&& barrel() &&; + + /// @brief Shorthand for `setLayerType(LayerType::Plane)`. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayerAssembler&& planar() &&; + + /// @brief Set the axis definition used to orient sensitive surfaces. + /// + /// Only available when the backend defines an @ref AxisDefinition type and + /// stores optional surface-axis information in `LayerSpec`. + /// @param axes Axis definition forwarded to `LayerSpec::axes`. + /// @return `*this` (rvalue). + template + [[nodiscard]] SensorLayerAssembler&& setSensorAxes( + typename B::AxisDefinition axes) && + requires(detail::HasAxisDefinition) { + m_layerSpec.axes = std::move(axes); + return std::move(*this); + } + + /// @brief Set the sensor elements to assemble into layers. + /// @param sensors Sensor elements (leaf-level sensitives). + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayerAssembler&& setSensors( + std::vector sensors) &&; + + /// @brief Group sensors into layers by key (required). + /// + /// Sensors mapped to the same key are merged into one layer. The key becomes + /// the layer name. + /// @param grouper Callable `std::string(const Element& sensor)`. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayerAssembler&& groupBy(LayerGrouper grouper) &&; + + /// @brief Set the output container node name (required). + /// @param containerName Name of the produced + /// @ref CylinderContainerBlueprintNode. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayerAssembler&& setContainerName( + std::string containerName) &&; + + /// @brief Set an envelope applied to every produced layer node. + /// @param envelope Envelope margins added around each layer's extent. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayerAssembler&& setEnvelope( + const Acts::ExtentEnvelope& envelope) &&; + + /// @brief Override the attachment strategy for the container node. + /// + /// When unset the backend's default strategy is used. + /// @param strategy Optional attachment strategy; pass `std::nullopt` to + /// reset to the default. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayerAssembler&& setAttachmentStrategy( + std::optional strategy) &&; + + /// @brief Register a callback invoked for each created layer node. + /// + /// The callback may either return a (possibly replaced/wrapped) layer node, + /// or mutate a layer node in-place and return `void`. + /// @param customizer Callback applied to each created layer node. + /// @return `*this` (rvalue). + template + [[nodiscard]] SensorLayerAssembler&& onLayer(CustomizerT customizer) && + requires( + detail::LayerNodeReturningCallable> || + detail::LayerNodeReplacingCallable>) + { + if constexpr (detail::LayerNodeReturningCallable< + Element, std::decay_t>) { + m_onLayer = std::move(customizer); + } else { + m_onLayer = [customizer = std::move(customizer)]( + const std::optional& elem, + std::shared_ptr layer) mutable { + customizer(elem, *layer); + return layer; + }; + } + return std::move(*this); + } + + /// @brief Build and return the assembled container node. + /// + /// @throws std::runtime_error if the layer type is not set, if the backend + /// requires axes and none were provided, if sensors are not set, + /// if + /// the container name is not set, or if @ref groupBy has not been + /// configured. + /// @return Shared pointer to the assembled container node. + [[nodiscard]] std::shared_ptr build() const; + + /// @brief Build the container node and attach it as a child of @p node. + /// + /// Equivalent to `node.addChild(build())`. + /// @param node Blueprint node that will receive the built container as a + /// child. + void addTo(BlueprintNode& node) const&&; + + private: + friend class BlueprintBuilder; + + /// @brief Construct a @ref SensorLayerAssembler bound to @p builder. + /// @param builder The owning @ref BlueprintBuilder; must outlive this object. + explicit SensorLayerAssembler(const Builder& builder); + + const Builder* m_builder = nullptr; + std::optional m_layerType; + LayerSpec m_layerSpec{}; + std::optional> m_sensors; + LayerGrouper m_groupBy; + std::optional m_containerName; + std::optional m_envelope; + std::optional m_attachmentStrategy; + LayerCustomizer m_onLayer; +}; + +/// @brief Fluent builder that assembles a single cylindrical or disc-like +/// detector layer directly from sensor elements, returning a +/// @ref LayerBlueprintNode (no container wrapper). +/// +/// Unlike @ref SensorLayerAssembler, this builder produces exactly one layer. +/// The layer name must be provided explicitly via @ref setLayerName. No +/// grouping function is required or supported. +/// Obtained from @ref BlueprintBuilder::layerFromSensors(). +/// +/// ```cpp +/// builder.layerFromSensors() +/// .barrel() +/// .setSensorAxes(myAxes) +/// .setSensors(sensorElements) +/// .setLayerName("MyLayer") +/// .addTo(parentNode); +/// ``` +/// +/// @tparam BackendT Geometry backend that provides detector elements, layer +/// specifications, hierarchy traversal, sensitive-element +/// classification, and surface construction. +template +class SensorLayer { + public: + /// The associated @ref BlueprintBuilder type. + using Builder = BlueprintBuilder; + /// Distinguishes barrel (Cylinder) from endcap (Disc) layer geometry. + using LayerType = LayerBlueprintNode::LayerType; + /// Backend detector element handle type. + using Element = typename BackendT::Element; + /// Backend layer-specification type. + using LayerSpec = typename BackendT::LayerSpec; + /// Axis definition type, or `std::monostate` when the backend does not + /// support axis definitions. + using AxisDefinition = + std::conditional_t, + typename BackendT::AxisDefinition, std::monostate>; + /// Callback type that can replace or wrap a @ref LayerBlueprintNode. + using LayerCustomizer = detail::LayerCustomizer; + + /// @brief Set the layer geometry type explicitly. + /// @param layerType `LayerType::Cylinder` for barrel, `LayerType::Disc` for + /// endcap. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayer&& setLayerType(LayerType layerType) &&; + + /// @brief Shorthand for `setLayerType(LayerType::Disc)`. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayer&& endcap() &&; + + /// @brief Shorthand for `setLayerType(LayerType::Cylinder)`. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayer&& barrel() &&; + + /// @brief Shorthand for `setLayerType(LayerType::Plane)`. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayer&& planar() &&; + + /// @brief Set the axis definition used to orient sensitive surfaces. + /// + /// Only available when the backend defines an @ref AxisDefinition type and + /// stores optional surface-axis information in `LayerSpec`. + /// @param axes Axis definition forwarded to `LayerSpec::axes`. + /// @return `*this` (rvalue). + template + [[nodiscard]] SensorLayer&& setSensorAxes( + typename B::AxisDefinition axes) && + requires(detail::HasAxisDefinition) { + m_layerSpec.axes = std::move(axes); + return std::move(*this); + } + + /// @brief Set the sensor elements to assemble into the layer. + /// @param sensors Sensor elements (leaf-level sensitives). + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayer&& setSensors(std::vector sensors) &&; + + /// @brief Set the name for the produced layer node (required). + /// @param name Layer node name. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayer&& setLayerName(std::string name) &&; + + /// @brief Set an envelope applied to the produced layer node. + /// @param envelope Envelope margins added around the layer's extent. + /// @return `*this` (rvalue). + [[nodiscard]] SensorLayer&& setEnvelope( + const Acts::ExtentEnvelope& envelope) &&; + + /// @brief Register a callback invoked for the created layer node. + /// + /// The callback may either return a (possibly replaced/wrapped) layer node, + /// or mutate a layer node in-place and return `void`. + /// @param customizer Callback applied to the created layer node. + /// @return `*this` (rvalue). + template + [[nodiscard]] SensorLayer&& onLayer(CustomizerT customizer) && + requires( + detail::LayerNodeReturningCallable> || + detail::LayerNodeReplacingCallable>) + { + if constexpr (detail::LayerNodeReturningCallable< + Element, std::decay_t>) { + m_onLayer = std::move(customizer); + } else { + m_onLayer = [customizer = std::move(customizer)]( + const std::optional& elem, + std::shared_ptr layer) mutable { + customizer(elem, *layer); + return layer; + }; + } + return std::move(*this); + } + + /// @brief Build and return the assembled layer node. + /// + /// @throws std::runtime_error if the layer type is not set, if the backend + /// requires axes and none were provided, if sensors are not set, + /// or + /// if @ref setLayerName has not been called. + /// @return Shared pointer to the assembled @ref LayerBlueprintNode. + [[nodiscard]] std::shared_ptr build() const; + + /// @brief Build the layer node and attach it as a child of @p node. + /// + /// Equivalent to `node.addChild(build())`. + /// @param node Blueprint node that will receive the built layer as a child. + void addTo(BlueprintNode& node) const&&; + + private: + friend class BlueprintBuilder; + + /// @brief Construct a @ref SensorLayer bound to @p builder. + /// @param builder The owning @ref BlueprintBuilder; must outlive this object. + explicit SensorLayer(const Builder& builder); + + const Builder* m_builder = nullptr; + std::optional m_layerType; + LayerSpec m_layerSpec{}; + std::optional> m_sensors; + std::optional m_layerName; + std::optional m_envelope; + LayerCustomizer m_onLayer; +}; + +/// @brief Fluent builder that assembles a combined barrel + endcap subdetector +/// into a @ref CylinderContainerBlueprintNode arranged along the Z axis. +/// +/// Instances are obtained from @ref BlueprintBuilder::barrelEndcap(). The +/// builder inspects the subtree of the provided assembly element for barrel and +/// endcap children (using the backend's `isBarrel` / `isEndcap` / `isTracker` +/// predicates, when available) and delegates individual layer assembly to +/// @ref ElementLayerAssembler internally. +/// +/// Typical usage: +/// @code +/// builder.barrelEndcap() +/// .setAssembly(innerTrackerElement) +/// .setSensorAxes(barrelAxes, endcapAxes) +/// .setLayerFilter(layerPattern) +/// .addTo(rootNode); +/// @endcode +/// +/// This builder requires backend predicates that classify elements as barrel, +/// endcap, and tracker components. +/// @tparam BackendT Geometry backend that provides detector elements, layer +/// specifications, hierarchy traversal, sensitive-element +/// classification, and surface construction. +template +class BarrelEndcapAssembler { + public: + /// The associated @ref BlueprintBuilder type. + using Builder = BlueprintBuilder; + /// Backend detector element handle type. + using Element = typename BackendT::Element; + /// Axis definition type, or `std::monostate` when the backend does not + /// support axis definitions. + using AxisDefinition = + std::conditional_t, + typename BackendT::AxisDefinition, std::monostate>; + /// The @ref ElementLayerAssembler specialisation for this backend. + using ElementLayerAssembler = + ::Acts::Experimental::ElementLayerAssembler; + /// Callback type that can replace or wrap a + /// @ref CylinderContainerBlueprintNode. + using ContainerCustomizer = detail::ContainerCustomizer; + + /// @brief Construct a @ref BarrelEndcapAssembler bound to @p builder. + /// @param builder The owning @ref BlueprintBuilder; must outlive this object. + explicit BarrelEndcapAssembler(const Builder& builder); + + /// @brief Build and return the assembled barrel+endcap container node. + /// + /// Locates barrel and endcap sub-elements inside the assembly, creates one + /// @ref ElementLayerAssembler -based barrel container and one or more endcap + /// containers, then returns a Z-axis @ref CylinderContainerBlueprintNode + /// holding them all. + /// + /// @throws std::runtime_error if the assembly element has not been set, if + /// axes are required by the backend but not provided, if the layer + /// filter has not been set, or if more than one barrel element is + /// found inside the assembly. + /// @return Shared pointer to the assembled Z-axis container node. + [[nodiscard]] std::shared_ptr build() const + requires(detail::HasBarrelEndcapClassifier); + + /// @brief Build the container node and attach it as a child of @p node. + /// + /// Equivalent to `node.addChild(build())`. + /// @param node Blueprint node that will receive the built container as a + /// child. + void addTo(BlueprintNode& node) const&& + requires(detail::HasBarrelEndcapClassifier); + + /// @brief Register a layer callback forwarded to each inner + /// @ref ElementLayerAssembler. + /// + /// The callback may either return a (possibly replaced/wrapped) layer node, + /// or mutate a layer node in-place and return `void`. + /// @param customizer Callback applied to each created layer node. + /// @return `*this` (rvalue). + template + [[nodiscard]] BarrelEndcapAssembler&& onLayer(CustomizerT customizer) && + requires( + detail::LayerNodeReturningCallable> || + detail::LayerNodeReplacingCallable>) + { + if constexpr (detail::LayerNodeReturningCallable< + Element, std::decay_t>) { + m_onLayer = std::move(customizer); + } else { + m_onLayer = [customizer = std::move(customizer)]( + const std::optional& elem, + std::shared_ptr layer) mutable { + customizer(elem, *layer); + return layer; + }; + } + return std::move(*this); + } + + /// @brief Register a callback invoked for each barrel or endcap container + /// node. + /// + /// The callback may either return a (possibly replaced/wrapped) container + /// node, or mutate a container node in-place and return `void`. + /// @param customizer Callback applied to each created barrel or endcap + /// container node. + /// @return `*this` (rvalue). + template + [[nodiscard]] BarrelEndcapAssembler&& onContainer(CustomizerT customizer) && + requires(detail::ContainerNodeReturningCallable< + Element, std::decay_t> || + detail::ContainerNodeReplacingCallable>) + { + if constexpr (detail::ContainerNodeReturningCallable< + Element, std::decay_t>) { + m_onContainer = std::move(customizer); + } else { + m_onContainer = + [customizer = std::move(customizer)]( + const Element& elem, + std::shared_ptr node) mutable { + customizer(elem, *node); + return node; + }; + } + return std::move(*this); + } + + /// @brief Set the top-level detector element whose subtree is searched for + /// barrel and endcap elements. + /// @param assembly Root element of the barrel+endcap sub-detector. + /// @return `*this` (rvalue). + [[nodiscard]] BarrelEndcapAssembler&& setAssembly(const Element& assembly) &&; + + /// @brief Set the axis definitions for both barrel and endcap layers at once. + /// + /// Only available when the backend defines an @ref AxisDefinition type and + /// stores optional surface-axis information in `LayerSpec`. + /// @param barrel Axis definition forwarded to barrel + /// @ref ElementLayerAssembler s. + /// @param endcap Axis definition forwarded to endcap + /// @ref ElementLayerAssembler s. + /// @return `*this` (rvalue). + [[nodiscard]] BarrelEndcapAssembler&& setSensorAxes(AxisDefinition barrel, + AxisDefinition endcap) && + requires(detail::HasAxisDefinition); + + /// @brief Set the axis definition used for endcap layers only. + /// + /// Only available when the backend defines an @ref AxisDefinition type and + /// stores optional surface-axis information in `LayerSpec`. + /// @param axes Axis definition forwarded to endcap + /// @ref ElementLayerAssembler s. + /// @return `*this` (rvalue). + [[nodiscard]] BarrelEndcapAssembler&& setEndcapAxes(AxisDefinition axes) && + requires(detail::HasAxisDefinition); + + /// @brief Set the regex filter used to select individual layer elements + /// within each barrel or endcap container. + /// @param pattern Regular expression matched against child element names. + /// @return `*this` (rvalue). + [[nodiscard]] BarrelEndcapAssembler&& setLayerFilter( + const std::regex& pattern) &&; + + private: + typename ElementLayerAssembler::LayerCustomizer m_onLayer; + ContainerCustomizer m_onContainer = + [](const Element&, std::shared_ptr node) { + return node; + }; + + std::optional m_assembly; + std::optional m_barrelAxes; + std::optional m_endcapAxes; + std::optional m_layerFilter; + const Builder* m_builder = nullptr; +}; + +/// @brief High-level builder that converts a backend detector element hierarchy +/// into a blueprint node tree. +/// +/// @ref BlueprintBuilder provides the entry points for blueprint construction: +/// - @ref BlueprintBuilder::layers() returns an @ref ElementLayerAssembler +/// for building a layer stack from layer-representative detector elements, +/// - @ref BlueprintBuilder::layersFromSensors() returns a +/// @ref SensorLayerAssembler for building multiple layers directly from +/// sensor elements (`groupBy` required), +/// - @ref BlueprintBuilder::layerFromSensors() returns a @ref SensorLayer for +/// building a single layer directly from sensor elements, +/// - @ref BlueprintBuilder::barrelEndcap() returns a +/// @ref BarrelEndcapAssembler for combined barrel+endcap sub-detectors. +/// +/// It also exposes helpers for traversing and querying the detector element +/// hierarchy, which are used internally by the assembler classes. +/// +/// The builder is parameterised on a backend type @p BackendT. The backend +/// must be constructible from its `Config` plus an `Acts::Logger`, expose +/// `Element` and `LayerSpec` types, traverse the detector hierarchy via +/// `world()` / `children()` / `parent()` / `nameOf()`, classify sensitive +/// elements with `isSensitive()`, and build ACTS surfaces from sensitive +/// elements and a layer specification. It encapsulates all +/// geometry-framework-specific knowledge (e.g. DD4hep `DetElement` +/// navigation, type-flag interpretation, surface conversion). +/// +/// @tparam BackendT Geometry backend that provides detector elements, layer +/// specifications, hierarchy traversal, sensitive-element +/// classification, and surface construction. +template +class BlueprintBuilder { + public: + /// The backend type. + using Backend = BackendT; + /// Backend detector element handle type. + using Element = typename Backend::Element; + /// Backend layer-specification type. + using LayerSpec = typename Backend::LayerSpec; + /// Axis definition type, or `std::monostate` when the backend does not + /// support axis definitions. + using AxisDefinition = + std::conditional_t, + typename Backend::AxisDefinition, std::monostate>; + /// The @ref ElementLayerAssembler specialisation for this backend. + using ElementLayerAssembler = + ::Acts::Experimental::ElementLayerAssembler; + /// The @ref SensorLayerAssembler specialisation for this backend. + using SensorLayerAssembler = + ::Acts::Experimental::SensorLayerAssembler; + /// The @ref SensorLayer specialisation for this backend. + using SensorLayer = ::Acts::Experimental::SensorLayer; + /// The @ref BarrelEndcapAssembler specialisation for this backend. + using BarrelEndcapAssembler = + ::Acts::Experimental::BarrelEndcapAssembler; + + /// @brief Construct a `BlueprintBuilder` from a backend configuration. + /// + /// @param cfg Backend-specific configuration object passed directly to the + /// backend constructor. + /// @param logger_ Optional logger; defaults to an `INFO`-level logger named + /// `"BlueprintBuilder"`. + explicit BlueprintBuilder(const typename Backend::Config& cfg, + std::unique_ptr logger_ = + Acts::getDefaultLogger("BlueprintBuilder", + Acts::Logging::INFO)); + + /// @brief Create a @ref LayerBlueprintNode from a single detector element. + /// + /// Recursively collects all sensitive descendants of @p layerElement and + /// delegates to the two-argument overload. + /// @param layerElement Layer element whose subtree is scanned for + /// sensitive volumes. + /// @param layerSpec Specification controlling surface axes and optional + /// name/transform overrides. + /// @return Shared pointer to the constructed @ref LayerBlueprintNode. + std::shared_ptr makeLayer( + const Element& layerElement, const LayerSpec& layerSpec) const; + + /// @brief Create a @ref LayerBlueprintNode from an explicit list of sensitive + /// elements. + /// + /// This is a low-level forwarding API: @p layerSpec is passed to the backend + /// as-is. + /// @param parent Detector element that contextualises the layer + /// (used for naming and transform extraction). + /// @param sensitives Span of sensitive detector elements to be converted to + /// surfaces and assigned to the node. + /// @param layerSpec Specification controlling surface axes and optional + /// name/transform overrides. + /// @return Shared pointer to the constructed @ref LayerBlueprintNode. + std::shared_ptr makeLayer( + const Element& parent, std::span sensitives, + const LayerSpec& layerSpec) const; + + /// @brief Create a @ref LayerBlueprintNode from sensitive elements without + /// a parent/context element. + /// + /// This variant cannot perform parent-based transform extraction. + /// `layerSpec.layerName` must be set and is used verbatim. + /// @param sensitives Span of sensitive detector elements to be converted to + /// surfaces and assigned to the node. + /// @param layerSpec Specification controlling surface axes and name. + /// @throws std::runtime_error if `layerSpec.layerName` is not set. + /// @return Shared pointer to the constructed @ref LayerBlueprintNode. + std::shared_ptr makeLayer( + std::span sensitives, const LayerSpec& layerSpec) const; + + /// @brief Create an @ref ElementLayerAssembler bound to this builder. + /// + /// Use when layer-representative detector elements exist in the hierarchy. + /// Each element becomes one layer; name and transform are deduced from it. + /// @return A new @ref ElementLayerAssembler instance. + [[nodiscard]] ElementLayerAssembler layers() const; + + /// @brief Create a @ref SensorLayerAssembler bound to this builder. + /// + /// Use when sensor elements are supplied directly and no layer-representative + /// element exists. A @ref SensorLayerAssembler::groupBy function is required; + /// sensors sharing the same key are merged into one layer per key. For a + /// single layer without grouping, use @ref layerFromSensors() instead. + /// @return A new @ref SensorLayerAssembler instance. + [[nodiscard]] SensorLayerAssembler layersFromSensors() const; + + /// @brief Create a @ref SensorLayer bound to this builder. + /// + /// Use when all sensor elements belong to exactly one layer and no grouping + /// is needed. The layer name must be set explicitly via + /// @ref SensorLayer::setLayerName. The result is a single + /// @ref LayerBlueprintNode (no container wrapper). + /// @return A new @ref SensorLayer instance. + [[nodiscard]] SensorLayer layerFromSensors() const; + + /// @brief Create a @ref BarrelEndcapAssembler bound to this builder. + /// + /// The returned assembler must be configured (assembly element, axes, layer + /// filter) and then finalised via @ref BarrelEndcapAssembler::build() or + /// @ref BarrelEndcapAssembler::addTo(). + /// @return A new @ref BarrelEndcapAssembler instance. + [[nodiscard]] BarrelEndcapAssembler barrelEndcap() const + requires(detail::HasBarrelEndcapClassifier); + + /// @brief Search for a detector element by exact name within a subtree. + /// + /// Performs a depth-first search starting at @p parent. + /// @param parent Starting element for the search. + /// @param name Exact element name to match. + /// @return The first matching element, or `std::nullopt` if not found. + std::optional findDetElementByName(const Element& parent, + const std::string& name) const; + + /// @brief Search for a detector element by exact name starting from the world + /// root. + /// @param name Exact element name to match. + /// @return The first matching element, or `std::nullopt` if not found. + std::optional findDetElementByName(const std::string& name) const; + + /// @brief Build a separator-joined path string from the world root down to + /// @p elem. + /// + /// The path consists of element names at each level of the hierarchy, from + /// the immediate child of the world element down to @p elem, joined by + /// @p separator. + /// @param elem Target element. + /// @param separator String inserted between successive name components + /// (default: `"|"`). + /// @return The assembled path string. + std::string getPathToElementName(const Element& elem, + std::string_view separator = "|") const; + + /// @brief Collect all elements in a subtree whose names match a regex. + /// + /// Performs a depth-first traversal of the subtree rooted at @p parent and + /// returns every element whose name fully matches @p pattern + /// (`std::regex_match`). + /// @param parent Root of the subtree to search. + /// @param pattern Regular expression matched against each element name. + /// @return Vector of matching elements in depth-first order. + std::vector findDetElementByNamePattern( + const Element& parent, const std::regex& pattern) const; + + /// @brief Collect all barrel tracker elements within an assembly subtree. + /// + /// An element is included when the backend reports it as both a tracker + /// element and a barrel element. + /// @param assembly Root element of the assembly subtree to search. + /// @return Vector of barrel elements in depth-first order. + std::vector findBarrelElements(const Element& assembly) const + requires(detail::HasBarrelEndcapClassifier); + + /// @brief Collect all endcap tracker elements within an assembly subtree. + /// + /// An element is included when the backend reports it as both a tracker + /// element and an endcap element. + /// @param assembly Root element of the assembly subtree to search. + /// @return Vector of endcap elements in depth-first order. + std::vector findEndcapElements(const Element& assembly) const + requires(detail::HasBarrelEndcapClassifier); + + /// @brief Return the logger associated with this builder. + /// @return Reference to the logger instance. + const Acts::Logger& logger() const; + + /// @brief Return the backend associated with this builder. + /// @return Const reference to the backend instance. + const Backend& backend() const; + + /// @brief Recursively collect all sensitive descendant elements. + /// + /// Traverses the subtree rooted at @p detElement and returns every element + /// for which the backend's `isSensitive()` predicate returns `true`. + /// @param detElement Root of the subtree to scan. + /// @return Vector of sensitive elements in depth-first order. + std::vector resolveSensitives(const Element& detElement) const; + + private: + std::unique_ptr m_logger; + Backend m_backend; +}; + +} // namespace Acts::Experimental diff --git a/Core/include/Acts/Geometry/detail/BlueprintBuilder_impl.hpp b/Core/include/Acts/Geometry/detail/BlueprintBuilder_impl.hpp new file mode 100644 index 00000000000..1e2f39c5220 --- /dev/null +++ b/Core/include/Acts/Geometry/detail/BlueprintBuilder_impl.hpp @@ -0,0 +1,881 @@ +// 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/. + +/// Implementation of BlueprintBuilder template methods. +/// Include this header only in TUs that perform explicit template instantiation +/// (e.g. geometry plugin .cpp files). Do not include from the main +/// BlueprintBuilder.hpp. + +#pragma once + +#include "Acts/Geometry/BlueprintBuilder.hpp" +#include "Acts/Utilities/FunctionComposition.hpp" + +namespace Acts::Experimental { + +namespace detail { + +template +struct LayerBuildInputs { + std::vector layerElements; + std::optional deducedContainerName; +}; + +template +LayerBuildInputs resolveLayerBuildInputs( + const BlueprintBuilder& builder, + const std::optional& container, + const std::optional>& + explicitLayerElements, + const std::optional& filter) { + using Element = typename BackendT::Element; + LayerBuildInputs inputs; + + if (container.has_value()) { + inputs.deducedContainerName = builder.backend().nameOf(container.value()); + } + + if (explicitLayerElements.has_value()) { + inputs.layerElements = *explicitLayerElements; + + if (filter.has_value()) { + std::vector filteredLayerElements; + filteredLayerElements.reserve(inputs.layerElements.size()); + for (const auto& layerElement : inputs.layerElements) { + const std::string layerElementName = + builder.backend().nameOf(layerElement); + if (std::regex_match(layerElementName, filter.value())) { + filteredLayerElements.push_back(layerElement); + } + } + inputs.layerElements = std::move(filteredLayerElements); + } + return inputs; + } + + if (!container.has_value()) { + throw std::runtime_error("Container not set in ElementLayerAssembler"); + } + if (!filter.has_value()) { + throw std::runtime_error("Pattern not set in ElementLayerAssembler"); + } + + inputs.layerElements = + builder.findDetElementByNamePattern(container.value(), filter.value()); + return inputs; +} + +// Both ElementLayerAssembler and SensorLayerAssembler use the same underlying +// LayerCustomizer type (a std::function with identical signature), so a single +// template deducing CustomizerT covers both. +template +std::shared_ptr finalizeLayer( + const std::optional& layerElement, + std::shared_ptr layer, + const std::optional& envelope, + const CustomizerT& onLayer) { + if (envelope.has_value()) { + layer->setEnvelope(envelope.value()); + } + if (onLayer) { + layer = onLayer(layerElement, std::move(layer)); + } + return layer; +} + +} // namespace detail + +// ElementLayerAssembler +template +ElementLayerAssembler::ElementLayerAssembler(const Builder& builder) + : m_builder{&builder} {} + +template +ElementLayerAssembler&& ElementLayerAssembler::setLayerType( + LayerType layerType) && { + m_layerType = layerType; + return std::move(*this); +} + +template +ElementLayerAssembler&& ElementLayerAssembler::endcap() && { + return std::move(*this).setLayerType(LayerType::Disc); +} + +template +ElementLayerAssembler&& ElementLayerAssembler::barrel() && { + return std::move(*this).setLayerType(LayerType::Cylinder); +} + +template +ElementLayerAssembler&& ElementLayerAssembler::planar() && { + return std::move(*this).setLayerType(LayerType::Plane); +} + +template +ElementLayerAssembler&& +ElementLayerAssembler::setLayerFilter(const std::string& pattern) && { + return std::move(*this).setLayerFilter(std::regex{pattern}); +} + +template +ElementLayerAssembler&& +ElementLayerAssembler::setLayerFilter(const std::regex& pattern) && { + m_filter = pattern; + return std::move(*this); +} + +template +ElementLayerAssembler&& ElementLayerAssembler::setContainer( + const Element& container) && { + m_container = container; + return std::move(*this); +} + +template +ElementLayerAssembler&& +ElementLayerAssembler::setContainerName( + std::string containerName) && { + m_containerName = std::move(containerName); + return std::move(*this); +} + +template +ElementLayerAssembler&& +ElementLayerAssembler::setLayerNameSuffix( + const std::optional& layerNameSuffix) && { + m_layerSpec.layerName = layerNameSuffix; + return std::move(*this); +} + +template +ElementLayerAssembler&& +ElementLayerAssembler::setLayerElements( + std::vector layerElements) && { + m_layerElements = std::move(layerElements); + return std::move(*this); +} + +template +ElementLayerAssembler&& ElementLayerAssembler::setContainer( + const std::string& name) && { + m_container = m_builder->findDetElementByName(name); + if (!m_container.has_value()) { + throw std::runtime_error("Could not find DetElement with name " + name + + " in ElementLayerAssembler"); + } + return std::move(*this); +} + +template +ElementLayerAssembler&& ElementLayerAssembler::setEnvelope( + const Acts::ExtentEnvelope& envelope) && { + m_envelope = envelope; + return std::move(*this); +} + +template +ElementLayerAssembler&& ElementLayerAssembler::setEmptyOk( + bool emptyOk) && { + m_emptyOk = emptyOk; + return std::move(*this); +} + +template +ElementLayerAssembler&& +ElementLayerAssembler::setAttachmentStrategy( + std::optional strategy) && { + m_attachmentStrategy = strategy; + return std::move(*this); +} + +template +void ElementLayerAssembler::addTo( + Acts::Experimental::BlueprintNode& node) const&& { + node.addChild(build()); +} + +template +std::shared_ptr +ElementLayerAssembler::build() const { + const auto& logger = m_builder->logger(); + + if (!m_layerType.has_value()) { + throw std::runtime_error("Layer type not set in ElementLayerAssembler"); + } + + if constexpr (detail::HasAxisDefinition) { + if (!m_layerSpec.axes.has_value()) { + throw std::runtime_error("Axes not set in ElementLayerAssembler"); + } + } + + if (!m_filter.has_value() && !m_layerElements.has_value()) { + throw std::runtime_error( + "Neither filter nor layer elements set in ElementLayerAssembler"); + } + + // Resolve the concrete layer-element set and optional deduced container name. + auto inputs = detail::resolveLayerBuildInputs( + *m_builder, m_container, m_layerElements, m_filter); + + // Resolve the final output container name (manual override wins). + std::string containerName; + if (m_containerName.has_value()) { + containerName = m_containerName.value(); + } else if (inputs.deducedContainerName.has_value()) { + containerName = inputs.deducedContainerName.value(); + } else { + throw std::runtime_error( + "Container name is not set in ElementLayerAssembler. Provide " + "setContainerName() or setContainer()."); + } + + if (inputs.layerElements.empty()) { + ACTS_LOG(m_emptyOk ? Acts::Logging::INFO : Acts::Logging::ERROR, + "No layers found in container " << containerName + << " matching pattern"); + if (!m_emptyOk) { + throw std::runtime_error(std::format( + "No layers found in container {} matching pattern", containerName)); + } + } + + std::shared_ptr node; + if (m_layerType != LayerType::Plane) { + const Acts::AxisDirection axisDir = m_layerType == LayerType::Cylinder + ? Acts::AxisDirection::AxisR + : Acts::AxisDirection::AxisZ; + node = std::make_shared( + containerName, axisDir); + } else { + node = std::make_shared( + containerName, Acts::AxisDirection::AxisZ); + } + + if (m_attachmentStrategy.has_value()) { + node->setAttachmentStrategy(m_attachmentStrategy.value()); + } + + for (const auto& layerElement : inputs.layerElements) { + LayerSpec resolvedLayerSpec = m_layerSpec; + std::string fullLayerName = m_builder->getPathToElementName(layerElement); + if (resolvedLayerSpec.layerName.has_value() && + !resolvedLayerSpec.layerName->empty()) { + fullLayerName += "|" + *resolvedLayerSpec.layerName; + } + resolvedLayerSpec.layerName = std::move(fullLayerName); + auto layer = m_builder->makeLayer(layerElement, resolvedLayerSpec); + layer->setLayerType(m_layerType.value()); + node->addChild(detail::finalizeLayer( + std::optional{layerElement}, std::move(layer), m_envelope, + m_onLayer)); + } + + return node; +} + +// SensorLayerAssembler +template +SensorLayerAssembler::SensorLayerAssembler(const Builder& builder) + : m_builder{&builder} {} + +template +SensorLayerAssembler&& SensorLayerAssembler::setLayerType( + LayerType layerType) && { + m_layerType = layerType; + return std::move(*this); +} + +template +SensorLayerAssembler&& SensorLayerAssembler::endcap() && { + return std::move(*this).setLayerType(LayerType::Disc); +} + +template +SensorLayerAssembler&& SensorLayerAssembler::barrel() && { + return std::move(*this).setLayerType(LayerType::Cylinder); +} + +template +SensorLayerAssembler&& SensorLayerAssembler::planar() && { + return std::move(*this).setLayerType(LayerType::Plane); +} + +template +SensorLayerAssembler&& SensorLayerAssembler::setSensors( + std::vector sensors) && { + m_sensors = std::move(sensors); + return std::move(*this); +} + +template +SensorLayerAssembler&& SensorLayerAssembler::groupBy( + typename SensorLayerAssembler::LayerGrouper grouper) && { + m_groupBy = std::move(grouper); + return std::move(*this); +} + +template +SensorLayerAssembler&& +SensorLayerAssembler::setContainerName(std::string containerName) && { + m_containerName = std::move(containerName); + return std::move(*this); +} + +template +SensorLayerAssembler&& SensorLayerAssembler::setEnvelope( + const Acts::ExtentEnvelope& envelope) && { + m_envelope = envelope; + return std::move(*this); +} + +template +SensorLayerAssembler&& +SensorLayerAssembler::setAttachmentStrategy( + std::optional strategy) && { + m_attachmentStrategy = strategy; + return std::move(*this); +} + +template +void SensorLayerAssembler::addTo( + Acts::Experimental::BlueprintNode& node) const&& { + node.addChild(build()); +} + +template +std::shared_ptr +SensorLayerAssembler::build() const { + using enum Acts::AxisDirection; + + if (!m_layerType.has_value()) { + throw std::runtime_error("Layer type not set in SensorLayerAssembler"); + } + + if constexpr (detail::HasAxisDefinition) { + if (!m_layerSpec.axes.has_value()) { + throw std::runtime_error( + std::format("Axes not set in SensorLayerAssembler (backend: {})", + BackendT::kIdentifier)); + } + } + + if (!m_sensors.has_value()) { + throw std::runtime_error("Sensors not set in SensorLayerAssembler"); + } + if (!m_containerName.has_value()) { + throw std::runtime_error( + "Container name not set in SensorLayerAssembler. " + "Call setContainerName()."); + } + if (!m_groupBy) { + throw std::runtime_error( + "SensorLayerAssembler requires groupBy(). For a single layer without " + "grouping, use BlueprintBuilder::layerFromSensors() instead."); + } + + std::shared_ptr node; + if (m_layerType != LayerType::Plane) { + const AxisDirection axisDir = + m_layerType == LayerType::Cylinder ? AxisR : AxisZ; + node = std::make_shared( + m_containerName.value(), axisDir); + } else { + node = std::make_shared( + m_containerName.value(), AxisZ); + } + + if (m_attachmentStrategy.has_value()) { + node->setAttachmentStrategy(m_attachmentStrategy.value()); + } + + struct GroupData { + std::string key; + std::vector sensors; + }; + + // Keep first-seen group order deterministic. + std::vector groups; + for (const auto& sensor : *m_sensors) { + const std::string key = m_groupBy(sensor); + auto it = std::ranges::find_if( + groups, [&](const GroupData& g) { return g.key == key; }); + if (it == groups.end()) { + groups.push_back(GroupData{.key = key, .sensors = {}}); + it = std::prev(groups.end()); + } + it->sensors.push_back(sensor); + } + + for (const auto& group : groups) { + if (group.key.empty()) { + throw std::runtime_error( + "groupBy() key must be non-empty for all sensors in " + "SensorLayerAssembler"); + } + LayerSpec layerSpec = m_layerSpec; + layerSpec.layerName = group.key; + auto layer = m_builder->makeLayer(std::span{group.sensors}, + layerSpec); + layer->setLayerType(m_layerType.value()); + node->addChild(detail::finalizeLayer( + std::nullopt, std::move(layer), m_envelope, m_onLayer)); + } + + return node; +} + +// SensorLayer +template +SensorLayer::SensorLayer(const Builder& builder) + : m_builder{&builder} {} + +template +SensorLayer&& SensorLayer::setLayerType( + LayerType layerType) && { + m_layerType = layerType; + return std::move(*this); +} + +template +SensorLayer&& SensorLayer::endcap() && { + return std::move(*this).setLayerType(LayerType::Disc); +} + +template +SensorLayer&& SensorLayer::barrel() && { + return std::move(*this).setLayerType(LayerType::Cylinder); +} + +template +SensorLayer&& SensorLayer::planar() && { + return std::move(*this).setLayerType(LayerType::Plane); +} + +template +SensorLayer&& SensorLayer::setSensors( + std::vector sensors) && { + m_sensors = std::move(sensors); + return std::move(*this); +} + +template +SensorLayer&& SensorLayer::setLayerName( + std::string name) && { + m_layerName = std::move(name); + return std::move(*this); +} + +template +SensorLayer&& SensorLayer::setEnvelope( + const Acts::ExtentEnvelope& envelope) && { + m_envelope = envelope; + return std::move(*this); +} + +template +void SensorLayer::addTo( + Acts::Experimental::BlueprintNode& node) const&& { + node.addChild(build()); +} + +template +std::shared_ptr +SensorLayer::build() const { + if (!m_layerType.has_value()) { + throw std::runtime_error("Layer type not set in SensorLayer"); + } + + if constexpr (detail::HasAxisDefinition) { + if (!m_layerSpec.axes.has_value()) { + throw std::runtime_error("Axes not set in SensorLayer"); + } + } + + if (!m_sensors.has_value()) { + throw std::runtime_error("Sensors not set in SensorLayer"); + } + if (!m_layerName.has_value() || m_layerName->empty()) { + throw std::runtime_error( + "Layer name not set in SensorLayer. Call setLayerName()."); + } + + LayerSpec layerSpec = m_layerSpec; + layerSpec.layerName = m_layerName; + auto layer = + m_builder->makeLayer(std::span{*m_sensors}, layerSpec); + layer->setLayerType(m_layerType.value()); + return detail::finalizeLayer(std::nullopt, std::move(layer), + m_envelope, m_onLayer); +} + +// BarrelEndcapAssembler +template +BarrelEndcapAssembler::BarrelEndcapAssembler(const Builder& builder) + : m_builder{&builder} {} + +template +void BarrelEndcapAssembler::addTo( + Acts::Experimental::BlueprintNode& node) const&& + requires(detail::HasBarrelEndcapClassifier) +{ + node.addChild(build()); +} + +template +BarrelEndcapAssembler&& BarrelEndcapAssembler::setAssembly( + const Element& assembly) && { + m_assembly = assembly; + return std::move(*this); +} + +template +BarrelEndcapAssembler&& +BarrelEndcapAssembler::setSensorAxes( + typename BarrelEndcapAssembler::AxisDefinition barrel, + typename BarrelEndcapAssembler::AxisDefinition endcap) && + requires(detail::HasAxisDefinition) +{ + m_barrelAxes = std::move(barrel); + m_endcapAxes = std::move(endcap); + return std::move(*this); +} + +template +BarrelEndcapAssembler&& +BarrelEndcapAssembler::setEndcapAxes( + typename BarrelEndcapAssembler::AxisDefinition axes) && + requires(detail::HasAxisDefinition) +{ + m_endcapAxes = std::move(axes); + return std::move(*this); +} + +template +BarrelEndcapAssembler&& +BarrelEndcapAssembler::setLayerFilter(const std::regex& pattern) && { + m_layerFilter = pattern; + return std::move(*this); +} + +template +std::shared_ptr +BarrelEndcapAssembler::build() const + requires(detail::HasBarrelEndcapClassifier) +{ + using enum Acts::AxisDirection; + + const auto& logger = m_builder->logger(); + + if (!m_assembly.has_value()) { + throw std::runtime_error( + "Assembly detector element not set in BarrelEndcapAssembler"); + } + + if constexpr (detail::HasAxisDefinition) { + if (!m_barrelAxes.has_value()) { + throw std::runtime_error("Barrel axes not set in BarrelEndcapAssembler"); + } + + if (!m_endcapAxes.has_value()) { + throw std::runtime_error("Endcap axes not set in BarrelEndcapAssembler"); + } + } + + if (!m_layerFilter.has_value()) { + throw std::runtime_error("Layer pattern not set in BarrelEndcapAssembler"); + } + + const auto& assembly = m_assembly.value(); + const std::string assemblyName = m_builder->backend().nameOf(assembly); + + ACTS_INFO("Converting barrel-endcap assembly from element: " << assemblyName); + auto barrels = m_builder->findBarrelElements(assembly); + + ACTS_DEBUG("Have " << barrels.size() << " barrel elements in assembly " + << assemblyName); + if (barrels.size() > 1) { + ACTS_ERROR("Expected exactly zero or one barrel in assembly " + << assemblyName << ", found " << barrels.size()); + throw std::runtime_error(std::format( + "Expected exactly zero or one barrel in assembly {}", assemblyName)); + } + + auto endcaps = m_builder->findEndcapElements(assembly); + + ACTS_DEBUG("Have " << endcaps.size() << " endcap elements in assembly " + << assemblyName); + + auto node = + std::make_shared( + assemblyName, Acts::AxisDirection::AxisZ); + + auto maybeAddAxes = [](const auto& axes) { + return [&axes](T&& assembler) { + if constexpr (detail::HasAxisDefinition) { + return std::forward(assembler).setSensorAxes(axes.value()); + } else { + return std::forward(assembler); + } + }; + }; + + auto build = [](T&& assembler) { + return std::forward(assembler).build(); + }; + + auto addTo = + std::bind_front(&Acts::Experimental::BlueprintNode::addChild, node.get()); + + for (const auto& barrel : barrels) { + auto compose = Acts::compose(addTo, std::bind_front(m_onContainer, barrel), + build, maybeAddAxes(m_barrelAxes)); + + compose(m_builder->layers() + .barrel() + .setLayerFilter(m_layerFilter.value()) + .setContainer(barrel) + .onLayer(m_onLayer)); + } + + for (const auto& endcap : endcaps) { + auto compose = Acts::compose(addTo, std::bind_front(m_onContainer, endcap), + build, maybeAddAxes(m_endcapAxes)); + + compose(m_builder->layers() + .endcap() + .setLayerFilter(m_layerFilter.value()) + .setContainer(endcap) + .onLayer(m_onLayer)); + } + + return node; +} + +// BlueprintBuilder +template +BlueprintBuilder::BlueprintBuilder( + const typename Backend::Config& cfg, + std::unique_ptr logger_) + : m_logger(logger_ ? std::move(logger_) + : Acts::getDefaultLogger("BlueprintBuilder", + Acts::Logging::INFO)), + m_backend(cfg, *m_logger) {} + +template +std::shared_ptr +BlueprintBuilder::makeLayer(const Element& layerElement, + const LayerSpec& layerSpec) const { + auto sensitives = resolveSensitives(layerElement); + return makeLayer(layerElement, sensitives, layerSpec); +} + +template +typename BlueprintBuilder::ElementLayerAssembler +BlueprintBuilder::layers() const { + return ElementLayerAssembler(*this); +} + +template +typename BlueprintBuilder::SensorLayerAssembler +BlueprintBuilder::layersFromSensors() const { + return SensorLayerAssembler(*this); +} + +template +typename BlueprintBuilder::SensorLayer +BlueprintBuilder::layerFromSensors() const { + return SensorLayer(*this); +} + +template +typename BlueprintBuilder::BarrelEndcapAssembler +BlueprintBuilder::barrelEndcap() const + requires(detail::HasBarrelEndcapClassifier) +{ + return BarrelEndcapAssembler(*this); +} + +template +const Acts::Logger& BlueprintBuilder::logger() const { + return *m_logger; +} + +template +const typename BlueprintBuilder::Backend& +BlueprintBuilder::backend() const { + return m_backend; +} + +template +std::optional::Element> +BlueprintBuilder::findDetElementByName( + const Element& parent, const std::string& name) const { + if (m_backend.nameOf(parent) == name) { + return parent; + } + + for (const auto& child : m_backend.children(parent)) { + auto result = findDetElementByName(child, name); + if (result.has_value()) { + return result; + } + } + + return std::nullopt; +} + +template +std::optional::Element> +BlueprintBuilder::findDetElementByName( + const std::string& name) const { + return findDetElementByName(m_backend.world(), name); +} + +template +std::string BlueprintBuilder::getPathToElementName( + const Element& elem, std::string_view separator) const { + std::vector names; + names.emplace_back(m_backend.nameOf(elem)); + + const auto world = m_backend.world(); + auto current = elem; + while (current != world) { + current = m_backend.parent(current); + if (current == world) { + break; + } + names.emplace_back(m_backend.nameOf(current)); + } + + std::ranges::reverse(names); + std::string path; + for (std::size_t i = 0; i < names.size(); ++i) { + if (i > 0) { + path += separator; + } + path += names[i]; + } + return path; +} + +template +std::vector::Element> +BlueprintBuilder::findDetElementByNamePattern( + const Element& parent, const std::regex& pattern) const { + std::vector matches; + + std::function visit = [&](const Element& elem) { + if (const std::string elemName = m_backend.nameOf(elem); + std::regex_match(elemName, pattern)) { + matches.push_back(elem); + } + for (const auto& child : m_backend.children(elem)) { + visit(child); + } + }; + visit(parent); + + return matches; +} + +template +std::vector::Element> +BlueprintBuilder::findBarrelElements(const Element& assembly) const + requires(detail::HasBarrelEndcapClassifier) +{ + std::vector barrels; + + std::function visit = [&](const Element& elem) { + if (m_backend.isTracker(elem) && m_backend.isBarrel(elem)) { + barrels.push_back(elem); + } + for (const auto& child : m_backend.children(elem)) { + visit(child); + } + }; + visit(assembly); + return barrels; +} + +template +std::vector::Element> +BlueprintBuilder::findEndcapElements(const Element& assembly) const + requires(detail::HasBarrelEndcapClassifier) +{ + std::vector endcaps; + + std::function visit = [&](const Element& elem) { + if (m_backend.isTracker(elem) && m_backend.isEndcap(elem)) { + endcaps.push_back(elem); + } + for (const auto& child : m_backend.children(elem)) { + visit(child); + } + }; + visit(assembly); + return endcaps; +} + +template +std::shared_ptr +BlueprintBuilder::makeLayer(const Element& parent, + std::span sensitives, + const LayerSpec& layerSpec) const { + const std::string nodeName = + layerSpec.layerName.value_or(m_backend.nameOf(parent)); + auto node = + std::make_shared(nodeName); + node->setSurfaces(m_backend.makeSurfaces(sensitives, layerSpec)); + + if constexpr (detail::HasLayerTransformLookup) { + if (const auto transform = + m_backend.lookupLayerTransform(parent, layerSpec); + transform.has_value()) { + node->setTransform(transform.value()); + } + } + + return node; +} + +template +std::shared_ptr +BlueprintBuilder::makeLayer(std::span sensitives, + const LayerSpec& layerSpec) const { + if (!layerSpec.layerName.has_value() || layerSpec.layerName->empty()) { + throw std::runtime_error( + "BlueprintBuilder::makeLayer(sensitives, layerSpec): " + "layerSpec.layerName must be set"); + } + + auto node = std::make_shared( + layerSpec.layerName.value()); + node->setSurfaces(m_backend.makeSurfaces(sensitives, layerSpec)); + return node; +} + +template +std::vector::Element> +BlueprintBuilder::resolveSensitives(const Element& detElement) const { + std::vector sensitives; + + std::function visit = [&](const Element& elem) { + if (m_backend.isSensitive(elem)) { + sensitives.push_back(elem); + } + for (const auto& child : m_backend.children(elem)) { + visit(child); + } + }; + visit(detElement); + return sensitives; +} + +} // namespace Acts::Experimental diff --git a/Core/include/Acts/Utilities/FunctionComposition.hpp b/Core/include/Acts/Utilities/FunctionComposition.hpp new file mode 100644 index 00000000000..86994d29906 --- /dev/null +++ b/Core/include/Acts/Utilities/FunctionComposition.hpp @@ -0,0 +1,61 @@ +// 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 +#include + +namespace Acts { + +namespace detail { + +/// @brief Base case: apply a single function to a value. +/// @param x The input value +/// @param f The function to apply +/// @return The result of invoking @p f with @p x +template +decltype(auto) applyRight(T&& x, const F& f) { + return std::invoke(f, std::forward(x)); +} + +/// @brief Recursive case: apply the last function first, then fold right. +/// @param x The input value +/// @param f The outermost function to apply last +/// @param rest The remaining functions, applied right-to-left +/// @return The result of the right-to-left function chain +template +decltype(auto) applyRight(T&& x, const F& f, const Rest&... rest) { + return std::invoke(f, applyRight(std::forward(x), rest...)); +} + +} // namespace detail + +/// @brief Compose multiple callables into a single callable, applied +/// right-to-left (mathematical convention). +/// +/// Given `compose(f, g, h)`, the returned callable computes `f(g(h(x)))`. +/// All callables are stored exactly once in a tuple, avoiding the nested-copy +/// overhead of a recursive lambda approach. +/// +/// @tparam Fs The callable types +/// @param fs The callables to compose +/// @return A callable that applies @p fs right-to-left +template +auto compose(Fs&&... fs) { + return [tup = std::make_tuple(std::forward(fs)...)]( + T&& x) -> decltype(auto) { + return std::apply( + [&](const auto&... fns) -> decltype(auto) { + return detail::applyRight(std::forward(x), fns...); + }, + tup); + }; +} + +} // namespace Acts diff --git a/Examples/Detectors/DD4hepDetector/include/ActsExamples/DD4hepDetector/OpenDataDetector.hpp b/Examples/Detectors/DD4hepDetector/include/ActsExamples/DD4hepDetector/OpenDataDetector.hpp index 5bff8f538f8..bb00baad89b 100644 --- a/Examples/Detectors/DD4hepDetector/include/ActsExamples/DD4hepDetector/OpenDataDetector.hpp +++ b/Examples/Detectors/DD4hepDetector/include/ActsExamples/DD4hepDetector/OpenDataDetector.hpp @@ -8,17 +8,55 @@ #pragma once +#include "Acts/Geometry/Extent.hpp" +#include "Acts/Utilities/AxisDefinitions.hpp" #include "ActsExamples/DD4hepDetector/DD4hepDetector.hpp" namespace Acts { class GeometryContext; } +namespace ActsPlugins { +class DD4hepDetectorElement; +} + namespace ActsExamples { class OpenDataDetector final : public DD4hepDetectorBase { public: - struct Config : public DD4hepDetectorBase::Config {}; + struct Config : public DD4hepDetectorBase::Config { + enum class ConstructionMethod { + BarrelEndcap, + DirectLayer, + DirectLayerGrouped + }; + + using ElementFactory = + std::function( + const dd4hep::DetElement& element, ActsPlugins::TGeoAxes axes, + double scale)>; + + ElementFactory detectorElementFactory = defaultDetectorElementFactory; + + /// Select the conversion style used to construct the Gen3 ODD geometry. + ConstructionMethod constructionMethod = ConstructionMethod::BarrelEndcap; + + /// Envelope for the blueprint root (world volume). Values in mm. + Acts::ExtentEnvelope blueprintEnvelope = + Acts::ExtentEnvelope::Zero() + .set(Acts::AxisDirection::AxisZ, {20., 20.}) + .set(Acts::AxisDirection::AxisR, {0., 20.}); + + /// Envelope for layer volumes. Values in mm. + Acts::ExtentEnvelope layerEnvelope = + Acts::ExtentEnvelope::Zero() + .set(Acts::AxisDirection::AxisZ, {2., 2.}) + .set(Acts::AxisDirection::AxisR, {2., 2.}); + }; + + static std::shared_ptr + defaultDetectorElementFactory(const dd4hep::DetElement& element, + ActsPlugins::TGeoAxes axes, double scale); explicit OpenDataDetector(const Config& cfg, const Acts::GeometryContext& gctx); @@ -28,6 +66,18 @@ class OpenDataDetector final : public DD4hepDetectorBase { private: void construct(const Acts::GeometryContext& gctx); + /// Construction path using BarrelEndcapAssembler (wraps + /// ElementLayerAssembler). + void constructBarrelEndcap(const Acts::GeometryContext& gctx); + + /// Construction path using ElementLayerAssembler directly. For illustration. + void constructDirectLayer(const Acts::GeometryContext& gctx); + + /// Construction path using SensorLayerAssembler with groupBy. For + /// illustration: sensors are collected directly and grouped by walking the + /// parent chain to find the enclosing layer element. + void constructDirectLayerGrouped(const Acts::GeometryContext& gctx); + Config m_cfg; }; diff --git a/Examples/Detectors/DD4hepDetector/src/OpenDataDetector.cpp b/Examples/Detectors/DD4hepDetector/src/OpenDataDetector.cpp index 5ef0714c4e8..c46f41b3bcb 100644 --- a/Examples/Detectors/DD4hepDetector/src/OpenDataDetector.cpp +++ b/Examples/Detectors/DD4hepDetector/src/OpenDataDetector.cpp @@ -8,9 +8,11 @@ #include "ActsExamples/DD4hepDetector/OpenDataDetector.hpp" -#include "Acts/Geometry/Blueprint.hpp" -#include "Acts/Geometry/BlueprintOptions.hpp" -#include "Acts/Geometry/CylinderVolumeBounds.hpp" +#include "ActsPlugins/DD4hep/DD4hepDetectorElement.hpp" +#include "ActsPlugins/DD4hep/OpenDataDetectorBuilder.hpp" +#include "ActsPlugins/Root/TGeoAxes.hpp" + +#include namespace ActsExamples { @@ -18,32 +20,35 @@ OpenDataDetector::OpenDataDetector(const Config& cfg, const Acts::GeometryContext& gctx) : DD4hepDetectorBase{cfg}, m_cfg{cfg} { ACTS_INFO("OpenDataDetector construct"); - construct(gctx); + switch (m_cfg.constructionMethod) { + case Config::ConstructionMethod::BarrelEndcap: + m_trackingGeometry = + ActsPlugins::DD4hep::buildOpenDataDetectorBarrelEndcap( + dd4hepDetector(), gctx, logger()); + break; + case Config::ConstructionMethod::DirectLayer: + m_trackingGeometry = + ActsPlugins::DD4hep::buildOpenDataDetectorDirectLayer( + dd4hepDetector(), gctx, logger()); + break; + case Config::ConstructionMethod::DirectLayerGrouped: + m_trackingGeometry = + ActsPlugins::DD4hep::buildOpenDataDetectorDirectLayerGrouped( + dd4hepDetector(), gctx, logger()); + break; + } } auto OpenDataDetector::config() const -> const Config& { return m_cfg; } -void OpenDataDetector::construct(const Acts::GeometryContext& gctx) { - using namespace Acts::Experimental; - using namespace Acts; - using namespace Acts::UnitLiterals; - - Blueprint::Config cfg; - cfg.envelope[AxisDirection::AxisZ] = {20_mm, 20_mm}; - cfg.envelope[AxisDirection::AxisR] = {0_mm, 20_mm}; - Blueprint root{cfg}; - - auto volBounds = std::make_shared(0_mm, 100_mm, 1_m); - auto vol = - std::make_unique(Transform3::Identity(), volBounds); - - root.addStaticVolume(std::move(vol)); - - BlueprintOptions options; - - m_trackingGeometry = root.construct(options, gctx, logger()); +std::shared_ptr +OpenDataDetector::defaultDetectorElementFactory( + const dd4hep::DetElement& element, ActsPlugins::TGeoAxes axes, + double scale) { + return std::make_shared(element, axes, + scale); } } // namespace ActsExamples diff --git a/Examples/Scripts/Python/geometry.py b/Examples/Scripts/Python/geometry.py index c7dc75725f2..9713a414aaa 100755 --- a/Examples/Scripts/Python/geometry.py +++ b/Examples/Scripts/Python/geometry.py @@ -56,10 +56,15 @@ def runGeometry( writer.write(context) if outputObj: - writer = ObjTrackingGeometryWriter( - level=acts.logging.INFO, outputDir=outputDir / "obj" + vis = acts.ObjVisualization3D() + trackingGeometry.visualize( + vis, + context.geoContext, + portalViewConfig=acts.ViewConfig(visible=False), + sensitiveViewConfig=acts.ViewConfig(visible=True), + viewConfig=acts.ViewConfig(visible=False), ) - writer.write(context, trackingGeometry) + vis.write(outputDir / "obj" / "geometry.obj") if outputJson: # if not os.path.isdir(outputDir / "json"): diff --git a/Plugins/DD4hep/CMakeLists.txt b/Plugins/DD4hep/CMakeLists.txt index 24c93ca6163..45914435f21 100644 --- a/Plugins/DD4hep/CMakeLists.txt +++ b/Plugins/DD4hep/CMakeLists.txt @@ -10,6 +10,8 @@ acts_add_library( src/DD4hepLayerBuilder.cpp src/DD4hepVolumeBuilder.cpp src/DD4hepFieldAdapter.cpp + src/BlueprintBuilder.cpp + src/OpenDataDetectorBuilder.cpp ACTS_INCLUDE_FOLDER include/ActsPlugins ) diff --git a/Plugins/DD4hep/include/ActsPlugins/DD4hep/BlueprintBuilder.hpp b/Plugins/DD4hep/include/ActsPlugins/DD4hep/BlueprintBuilder.hpp new file mode 100644 index 00000000000..90b12e6c393 --- /dev/null +++ b/Plugins/DD4hep/include/ActsPlugins/DD4hep/BlueprintBuilder.hpp @@ -0,0 +1,195 @@ +// 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/Geometry/BlueprintBuilder.hpp" +#include "Acts/Geometry/StaticBlueprintNode.hpp" +#include "Acts/Utilities/Logger.hpp" +#include "ActsPlugins/DD4hep/DD4hepDetectorElement.hpp" +#include "ActsPlugins/Root/TGeoAxes.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace dd4hep { +class DetElement; +} // namespace dd4hep + +namespace ActsPlugins::DD4hep { + +/// Backend adapter that maps a DD4hep detector hierarchy onto the ACTS +/// experimental blueprint-builder interface. +/// +/// The backend exposes DD4hep detector elements as blueprint input elements +/// and provides the conversions needed to create ACTS surfaces, detector +/// elements, and optional layer transforms. +class DD4hepBackend { + public: + /// Identifier string used in diagnostics emitted by the generic blueprint + /// builder. + static constexpr std::string_view kIdentifier = "DD4hepBackend"; + + /// DD4hep detector-element handle type consumed by the generic builder. + using Element = dd4hep::DetElement; + /// Axis-definition type forwarded to ROOT-based surface conversion helpers. + using AxisDefinition = TGeoAxes; + /// Layer-specific configuration forwarded from the generic builder to the + /// DD4hep backend. + struct LayerSpec { + /// Sensitive-surface axis convention. + std::optional axes; + /// Optional axis convention used to derive the layer transform. + std::optional layerAxes; + /// Optional explicit layer name override. + std::optional layerName; + }; + /// Concrete ACTS detector-element implementation used for DD4hep geometry. + using DetectorElement = DD4hepDetectorElement; + /// Shared pointer to a DD4hep-backed ACTS detector element. + using DetectorElementPtr = std::shared_ptr; + /// Factory that creates detector elements for converted DD4hep sensitives. + using DetectorElementFactory = std::function; + + /// Default detector-element factory used when @ref Config::elementFactory is + /// not overridden. + /// @param detElement DD4hep sensitive detector element to wrap. + /// @param axes Axis convention used for surface conversion. + /// @param lengthScale Unit scale applied during geometry conversion. + /// @return Newly created DD4hep-backed ACTS detector element. + static DetectorElementPtr defaultElementFactory(const Element& detElement, + AxisDefinition axes, + double lengthScale); + + /// Configuration of the DD4hep backend instance. + struct Config { + /// Factory used to create ACTS detector elements from DD4hep sensitives. + DetectorElementFactory elementFactory = defaultElementFactory; + /// DD4hep detector description that owns the world hierarchy. + const dd4hep::Detector* dd4hepDetector; + /// Unit scale applied when converting DD4hep lengths to ACTS units. + double lengthScale = 1.0; + /// Geometry context used when constructing ACTS detector elements. + std::reference_wrapper gctx; + }; + + /// Construct the DD4hep backend. + /// @param cfg Backend configuration and DD4hep detector handle. + /// @param logger Logger used for diagnostics. + explicit DD4hepBackend(const Config& cfg, const Acts::Logger& logger); + + /// Create an ACTS detector element from a DD4hep sensitive element. + /// @param detElement DD4hep sensitive element to convert. + /// @param axes Axis convention used for the converted surface. + /// @return Shared pointer to the created detector element. + DetectorElementPtr createDetectorElement(const Element& detElement, + AxisDefinition axes) const; + + /// Convert a set of DD4hep sensitive elements into ACTS surfaces. + /// @param sensitives Sensitive DD4hep elements belonging to one layer. + /// @param layerSpec Layer configuration controlling axes and naming. + /// @return Converted ACTS surfaces for the given sensitives. + std::vector> makeSurfaces( + std::span sensitives, const LayerSpec& layerSpec) const; + + /// Derive the layer transform from a DD4hep detector element when possible. + /// @param element DD4hep element providing the geometric context. + /// @param layerSpec Layer configuration controlling the transform lookup. + /// @return The derived layer transform, or `std::nullopt` if none is + /// available. + std::optional lookupLayerTransform( + const Element& element, const LayerSpec& layerSpec) const; + + /// Create a static beampipe blueprint node from the DD4hep world geometry. + /// @return Shared pointer to the generated beampipe node. + std::shared_ptr makeBeampipe() const; + + /// Return the DD4hep world detector element. + /// @return Root detector element of the DD4hep hierarchy. + Element world() const; + /// Return the fully qualified DD4hep name of an element. + /// @param element DD4hep detector element to inspect. + /// @return Full element name. + std::string nameOf(const Element& element) const; + /// Return the direct DD4hep child elements of a parent element. + /// @param parent Parent DD4hep detector element. + /// @return Direct children of @p parent. + std::vector children(const Element& parent) const; + /// Return the direct parent DD4hep element of a child element. + /// @param element Child DD4hep detector element. + /// @return Parent detector element. + Element parent(const Element& element) const; + + /// Check whether a DD4hep element represents a sensitive detector element. + /// @param element DD4hep detector element to classify. + /// @return `true` if the element is sensitive. + bool isSensitive(const Element& element) const; + /// Check whether a DD4hep element represents a barrel sub-detector. + /// @param element DD4hep detector element to classify. + /// @return `true` if the element is tagged as barrel. + bool isBarrel(const Element& element) const; + /// Check whether a DD4hep element represents an endcap sub-detector. + /// @param element DD4hep detector element to classify. + /// @return `true` if the element is tagged as endcap. + bool isEndcap(const Element& element) const; + /// Check whether a DD4hep element is part of the tracking detector. + /// @param element DD4hep detector element to classify. + /// @return `true` if the element belongs to the tracker. + bool isTracker(const Element& element) const; + + /// Retrieves a named integer constant from the DD4hep detector description. + /// The name is constructed by formatting @p fmt with @p args. + /// @tparam Args Types used to format the constant name. + /// @param fmt Format string used to construct the DD4hep constant name. + /// @param args Format arguments substituted into @p fmt. + /// @return Integer constant value stored in the DD4hep detector description. + template + int constant(std::format_string fmt, Args&&... args) const { + return m_cfg.dd4hepDetector->constant( + std::format(fmt, std::forward(args)...)); + } + + /// Return the logger associated with this backend. + /// @return Logger used for diagnostics. + const Acts::Logger& logger() const { return *m_logger; } + + private: + Config m_cfg; + const Acts::Logger* m_logger; +}; + +using BlueprintBuilder = Acts::Experimental::BlueprintBuilder; +using ElementLayerAssembler = + Acts::Experimental::ElementLayerAssembler; +using SensorLayerAssembler = + Acts::Experimental::SensorLayerAssembler; +using SensorLayer = Acts::Experimental::SensorLayer; +using BarrelEndcapAssembler = + Acts::Experimental::BarrelEndcapAssembler; + +} // namespace ActsPlugins::DD4hep + +// Explicit instantiation: suppress implicit instantiation in TUs that include +// this header. Definitions are instantiated in BlueprintBuilder.cpp. +// Placed at global scope so we open ::Acts::Experimental, not a nested Acts. +namespace Acts::Experimental { +extern template class BlueprintBuilder; +extern template class ElementLayerAssembler; +extern template class SensorLayerAssembler; +extern template class SensorLayer; +extern template class BarrelEndcapAssembler; +} // namespace Acts::Experimental diff --git a/Plugins/DD4hep/include/ActsPlugins/DD4hep/OpenDataDetectorBuilder.hpp b/Plugins/DD4hep/include/ActsPlugins/DD4hep/OpenDataDetectorBuilder.hpp new file mode 100644 index 00000000000..71b43334f36 --- /dev/null +++ b/Plugins/DD4hep/include/ActsPlugins/DD4hep/OpenDataDetectorBuilder.hpp @@ -0,0 +1,108 @@ +// 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/Geometry/Extent.hpp" +#include "Acts/Utilities/AxisDefinitions.hpp" + +#include +#include +#include +#include + +namespace dd4hep { +class Detector; +} + +namespace Acts { +class GeometryContext; +class Logger; +class TrackingGeometry; +} // namespace Acts + +namespace ActsPlugins::DD4hep { + +namespace detail { + +inline const std::regex kPixelLayerFilter{ + "(?:PixelLayer|PixelEndcap[NP])(\\d)"}; +inline const std::regex kShortStripLayerFilter{ + "(?:ShortStripLayer|ShortStripEndcap[NP])(\\d)"}; +inline const std::regex kLongStripLayerFilter{ + "(?:LongStripLayer|LongStripEndcap[NP])(\\d)"}; +inline const std::regex kPixelBarrelLayerFilter{"PixelLayer\\d"}; +inline const std::regex kPixelNegativeEndcapLayerFilter{"PixelEndcapN\\d"}; +inline const std::regex kPixelPositiveEndcapLayerFilter{"PixelEndcapP\\d"}; +inline const std::regex kShortStripBarrelLayerFilter{"ShortStripLayer\\d"}; +inline const std::regex kShortStripNegativeEndcapLayerFilter{ + "ShortStripEndcapN\\d"}; +inline const std::regex kShortStripPositiveEndcapLayerFilter{ + "ShortStripEndcapP\\d"}; +inline const std::regex kLongStripBarrelLayerFilter{"LongStripLayer\\d"}; +inline const std::regex kLongStripNegativeEndcapLayerFilter{ + "LongStripEndcapN\\d"}; +inline const std::regex kLongStripPositiveEndcapLayerFilter{ + "LongStripEndcapP\\d"}; + +inline const Acts::ExtentEnvelope kBlueprintEnvelope = + Acts::ExtentEnvelope::Zero() + .set(Acts::AxisDirection::AxisZ, {20., 20.}) + .set(Acts::AxisDirection::AxisR, {0., 20.}); + +inline const Acts::ExtentEnvelope kLayerEnvelope = + Acts::ExtentEnvelope::Zero() + .set(Acts::AxisDirection::AxisZ, {2., 2.}) + .set(Acts::AxisDirection::AxisR, {2., 2.}); + +inline int layerIndexFromName(std::string_view elemName, + const std::regex& layerFilter) { + std::cmatch match; + if (std::regex_search(elemName.begin(), elemName.end(), match, layerFilter) && + match.size() > 1) { + return std::stoi(match[1].str()); + } + + if (std::regex groupedLayerNameFilter{"layer(\\d+)"}; + std::regex_search(elemName.begin(), elemName.end(), match, + groupedLayerNameFilter) && + match.size() > 1) { + return std::stoi(match[1].str()); + } + + return 0; +} + +} // namespace detail + +/// Build the Open Data Detector tracking geometry using the BarrelEndcap +/// construction path (BarrelEndcapAssembler wrapping ElementLayerAssembler). +std::unique_ptr buildOpenDataDetectorBarrelEndcap( + const dd4hep::Detector& detector, const Acts::GeometryContext& gctx, + const Acts::Logger& logger); + +/// Build the Open Data Detector tracking geometry using the TGeo backend with +/// metadata extracted from DD4hep and explicit ODD layer-name patterns. +std::unique_ptr +buildOpenDataDetectorBarrelEndcapViaTGeo(const dd4hep::Detector& detector, + const Acts::GeometryContext& gctx, + const Acts::Logger& logger); + +/// Build the Open Data Detector tracking geometry using the DirectLayer +/// construction path (ElementLayerAssembler directly). +std::unique_ptr buildOpenDataDetectorDirectLayer( + const dd4hep::Detector& detector, const Acts::GeometryContext& gctx, + const Acts::Logger& logger); + +/// Build the Open Data Detector tracking geometry using the DirectLayerGrouped +/// construction path (SensorLayerAssembler with groupBy). +std::unique_ptr buildOpenDataDetectorDirectLayerGrouped( + const dd4hep::Detector& detector, const Acts::GeometryContext& gctx, + const Acts::Logger& logger); + +} // namespace ActsPlugins::DD4hep diff --git a/Plugins/DD4hep/src/BlueprintBuilder.cpp b/Plugins/DD4hep/src/BlueprintBuilder.cpp new file mode 100644 index 00000000000..c3844a07c29 --- /dev/null +++ b/Plugins/DD4hep/src/BlueprintBuilder.cpp @@ -0,0 +1,204 @@ +// 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 "ActsPlugins/DD4hep/BlueprintBuilder.hpp" + +#include "Acts/Definitions/Algebra.hpp" +#include "Acts/Geometry/TrackingVolume.hpp" +// Needed for explicit instantiation of template methods. +#include "Acts/Geometry/detail/BlueprintBuilder_impl.hpp" +#include "Acts/Surfaces/CylinderBounds.hpp" +#include "Acts/Surfaces/Surface.hpp" +#include "ActsPlugins/DD4hep/DD4hepDetectorElement.hpp" +#include "ActsPlugins/Root/TGeoSurfaceConverter.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace ActsPlugins::DD4hep { + +using ActsPlugins::TGeoAxes; + +DD4hepBackend::DetectorElementPtr DD4hepBackend::defaultElementFactory( + const Element& detElement, AxisDefinition axes, double lengthScale) { + return std::make_shared(detElement, axes, lengthScale); +} + +DD4hepBackend::DD4hepBackend(const Config& cfg, const Acts::Logger& logger) + : m_cfg(cfg), m_logger(&logger) { + if (m_cfg.dd4hepDetector == nullptr) { + throw std::invalid_argument("DD4hepBackend: dd4hepDetector is null"); + } +} + +DD4hepBackend::DetectorElementPtr DD4hepBackend::createDetectorElement( + const Element& detElement, AxisDefinition axes) const { + auto elem = m_cfg.elementFactory(detElement, axes, m_cfg.lengthScale); + + detElement.addExtension( + new dd4hep::rec::StructExtension(DD4hepDetectorElementExtension(elem))); + + return elem; +} + +namespace { +void visitSubtree( + const dd4hep::DetElement& detElement, + const std::function& visitor) { + visitor(detElement); + + for (const auto& [name, child] : detElement.children()) { + (void)name; + visitSubtree(child, visitor); + } +} +} // namespace + +DD4hepBackend::Element DD4hepBackend::world() const { + return m_cfg.dd4hepDetector->world(); +} + +std::string DD4hepBackend::nameOf(const Element& element) const { + return element.name(); +} + +std::vector DD4hepBackend::children( + const Element& parent) const { + std::vector result; + result.reserve(parent.children().size()); + for (const auto& [name, child] : parent.children()) { + (void)name; + result.push_back(child); + } + return result; +} + +DD4hepBackend::Element DD4hepBackend::parent(const Element& element) const { + return element.parent(); +} + +bool DD4hepBackend::isSensitive(const Element& element) const { + return element.volume().isSensitive(); +} + +bool DD4hepBackend::isBarrel(const Element& element) const { + return dd4hep::DetType{element.typeFlag()}.is(dd4hep::DetType::BARREL); +} + +bool DD4hepBackend::isEndcap(const Element& element) const { + return dd4hep::DetType{element.typeFlag()}.is(dd4hep::DetType::ENDCAP); +} + +bool DD4hepBackend::isTracker(const Element& element) const { + return dd4hep::DetType{element.typeFlag()}.is(dd4hep::DetType::TRACKER); +} + +std::vector> DD4hepBackend::makeSurfaces( + std::span sensitives, + const LayerSpec& layerSpec) const { + if (!layerSpec.axes.has_value()) { + throw std::runtime_error("DD4hepBackend::makeSurfaces: axes not set"); + } + + ACTS_DEBUG("Using " << sensitives.size() << " sensitive elements."); + + std::vector> surfaces; + surfaces.reserve(sensitives.size()); + + for (const auto& sensitive : sensitives) { + auto elem = createDetectorElement(sensitive, layerSpec.axes.value()); + surfaces.push_back(elem->surface().getSharedPtr()); + } + + return surfaces; +} + +std::optional DD4hepBackend::lookupLayerTransform( + const dd4hep::DetElement& element, const LayerSpec& layerSpec) const { + if (layerSpec.layerAxes.has_value()) { + ACTS_DEBUG("Finding layer transform automatically using layer axes: " + << layerSpec.layerAxes.value()); + Acts::Transform3 layerTransform = TGeoSurfaceConverter::transformFromShape( + *element.placement().ptr()->GetVolume()->GetShape(), + element.nominal().worldTransformation(), layerSpec.layerAxes.value(), + m_cfg.lengthScale); + + ACTS_VERBOSE(" -> Layer transform:\n" << layerTransform.matrix()); + return layerTransform; + } + + return std::nullopt; +} + +std::shared_ptr +DD4hepBackend::makeBeampipe() const { + std::optional beampipeElement = std::nullopt; + + visitSubtree(world(), [this, + &beampipeElement](const dd4hep::DetElement& elem) { + if (!dd4hep::DetType{elem.typeFlag()}.is(dd4hep::DetType::BEAMPIPE)) { + return; + } + if (beampipeElement.has_value()) { + ACTS_WARNING("Multiple beampipe elements found, using first: " + << beampipeElement->name() << ", ignoring: " << elem.name()); + return; + } + beampipeElement = elem; + }); + + if (!beampipeElement.has_value()) { + ACTS_ERROR("No beampipe element found in DD4hep detector."); + throw std::runtime_error("No beampipe element found in DD4hep detector."); + } + + ACTS_INFO("Beampipe element found: " << beampipeElement->name()); + + const auto tgTransform = beampipeElement->nominal().worldTransformation(); + auto [bounds, transform, thickness] = + ActsPlugins::TGeoSurfaceConverter::cylinderComponents( + *beampipeElement->placement().ptr()->GetVolume()->GetShape(), + tgTransform.GetRotationMatrix(), tgTransform.GetTranslation(), "XYZ", + m_cfg.lengthScale); + (void)thickness; + + if (bounds == nullptr) { + ACTS_ERROR("Beampipe element shape could not be converted to cylinder."); + throw std::runtime_error( + "Beampipe element shape could not be converted to cylinder."); + } + + auto volumeBounds = std::make_shared( + 0, bounds->get(Acts::CylinderBounds::eR), + bounds->get(Acts::CylinderBounds::eHalfLengthZ)); + auto volume = std::make_unique(transform, volumeBounds, + beampipeElement->name()); + return std::make_shared( + std::move(volume)); +} + +} // namespace ActsPlugins::DD4hep + +// Explicit template instantiation for DD4hepBackend. Ensures all template +// code is compiled in this TU; other TUs use extern template and link here. +// Must be in ::Acts::Experimental (at global scope) to match the template defs. +namespace Acts::Experimental { +template class BlueprintBuilder; +template class ElementLayerAssembler; +template class SensorLayerAssembler; +template class SensorLayer; +template class BarrelEndcapAssembler; +} // namespace Acts::Experimental diff --git a/Plugins/DD4hep/src/OpenDataDetectorBuilder.cpp b/Plugins/DD4hep/src/OpenDataDetectorBuilder.cpp new file mode 100644 index 00000000000..74f7685b62a --- /dev/null +++ b/Plugins/DD4hep/src/OpenDataDetectorBuilder.cpp @@ -0,0 +1,330 @@ +// 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 "ActsPlugins/DD4hep/OpenDataDetectorBuilder.hpp" + +#include "Acts/Definitions/Units.hpp" +#include "Acts/Geometry/Blueprint.hpp" +#include "Acts/Geometry/BlueprintOptions.hpp" +#include "Acts/Geometry/ContainerBlueprintNode.hpp" +#include "Acts/Geometry/Extent.hpp" +#include "Acts/Geometry/NavigationPolicyFactory.hpp" +#include "Acts/Geometry/VolumeAttachmentStrategy.hpp" +#include "Acts/Geometry/VolumeResizeStrategy.hpp" +#include "Acts/Navigation/CylinderNavigationPolicy.hpp" +#include "Acts/Navigation/SurfaceArrayNavigationPolicy.hpp" +#include "Acts/Utilities/AxisDefinitions.hpp" +#include "ActsPlugins/DD4hep/BlueprintBuilder.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ActsPlugins::DD4hep { + +namespace { + +auto makeLayerCustomizer(const BlueprintBuilder& builder, std::string det, + std::regex layerFilter) { + return [&builder, det = std::move(det), layerFilter = std::move(layerFilter)]( + const std::optional& elem, + Acts::Experimental::LayerBlueprintNode& layer) { + layer.setEnvelope(detail::kLayerEnvelope); + + const std::string elemName = + elem.has_value() ? std::string{builder.backend().nameOf(*elem)} + : layer.name(); + const int layerIdx = detail::layerIndexFromName(elemName, layerFilter); + + using SrfArrayNavPol = Acts::SurfaceArrayNavigationPolicy; + using enum SrfArrayNavPol::LayerType; + + SrfArrayNavPol::Config navCfg; + + if (layer.layerType() == + Acts::Experimental::LayerBlueprintNode::LayerType::Cylinder) { + // Barrel layer + navCfg.layerType = Cylinder; + navCfg.bins = { + builder.backend().constant("{}_b{}_sf_b_phi", det, layerIdx), + builder.backend().constant("{}_b_sf_b_z", det)}; + } else { + // Endcap layer + navCfg.layerType = Disc; + navCfg.bins = {builder.backend().constant("{}_e_sf_b_r", det), + builder.backend().constant("{}_e_sf_b_phi", det)}; + } + + layer.setNavigationPolicyFactory(Acts::NavigationPolicyFactory{} + .add() + .add(navCfg) + .asUniquePtr()); + }; +} + +void addDirectLayerSubsystem(const BlueprintBuilder& builder, + Acts::Experimental::ContainerBlueprintNode& outer, + std::string assembly, std::string det, + const std::regex& layerFilter) { + const auto assemblyElement = builder.findDetElementByName(assembly); + if (!assemblyElement.has_value()) { + throw std::runtime_error( + std::format("Could not find assembly '{}'", assembly)); + } + + auto barrels = builder.findBarrelElements(*assemblyElement); + auto endcaps = builder.findEndcapElements(*assemblyElement); + + const std::string assemblyName{builder.backend().nameOf(*assemblyElement)}; + auto containerNode = + std::make_shared( + assemblyName, Acts::AxisDirection::AxisZ); + + auto layerCustomizer = + makeLayerCustomizer(builder, std::move(det), layerFilter); + + auto addLayerChildren = [&](const auto& elements, auto makeNode) { + for (const auto& element : elements) { + auto node = makeNode(element); + node->setAttachmentStrategy(Acts::VolumeAttachmentStrategy::Gap); + node->setResizeStrategies(Acts::VolumeResizeStrategy::Gap, + Acts::VolumeResizeStrategy::Gap); + containerNode->addChild(std::move(node)); + } + }; + + addLayerChildren(barrels, [&](const auto& barrel) { + return builder.layers() + .barrel() + .setSensorAxes("XYZ") + .setLayerFilter(layerFilter) + .setContainer(barrel) + .onLayer(layerCustomizer) + .build(); + }); + + addLayerChildren(endcaps, [&](const auto& endcap) { + return builder.layers() + .endcap() + .setSensorAxes("XZY") + .setLayerFilter(layerFilter) + .setContainer(endcap) + .onLayer(layerCustomizer) + .build(); + }); + + outer.addChild(std::move(containerNode)); +} + +void addBarrelEndcapSubsystem(const BlueprintBuilder& builder, + Acts::Experimental::ContainerBlueprintNode& outer, + std::string assembly, std::string det, + const std::regex& layerFilter) { + const auto assemblyElement = builder.findDetElementByName(assembly); + if (!assemblyElement.has_value()) { + throw std::runtime_error( + std::format("Could not find assembly '{}'", assembly)); + } + + builder.barrelEndcap() + .setAssembly(*assemblyElement) + .setSensorAxes("XYZ", "XZY") + .setLayerFilter(layerFilter) + .onLayer(makeLayerCustomizer(builder, std::move(det), layerFilter)) + .onContainer( + [](const auto&, Acts::Experimental::ContainerBlueprintNode& node) { + node.setAttachmentStrategy(Acts::VolumeAttachmentStrategy::Gap); + node.setResizeStrategies(Acts::VolumeResizeStrategy::Gap, + Acts::VolumeResizeStrategy::Gap); + }) + .addTo(outer); +} + +void addDirectLayerGroupedSubsystem( + const BlueprintBuilder& builder, + Acts::Experimental::ContainerBlueprintNode& outer, std::string assembly, + std::string det, const std::regex& layerFilter) { + const auto assemblyElement = builder.findDetElementByName(assembly); + if (!assemblyElement.has_value()) { + throw std::runtime_error( + std::format("Could not find assembly '{}'", assembly)); + } + + auto barrels = builder.findBarrelElements(*assemblyElement); + auto endcaps = builder.findEndcapElements(*assemblyElement); + + const std::string assemblyName{builder.backend().nameOf(*assemblyElement)}; + auto containerNode = + std::make_shared( + assemblyName, Acts::AxisDirection::AxisZ); + + auto layerCustomizer = + makeLayerCustomizer(builder, std::move(det), layerFilter); + + auto sensorToLayerKey = [&](const dd4hep::DetElement& elem) { + auto current = elem; + const auto world = builder.backend().world(); + while (!(current == world)) { + std::cmatch match; + if (const std::string name{builder.backend().nameOf(current)}; + std::regex_search(name.c_str(), match, layerFilter) && + match.size() > 1) { + return builder.getPathToElementName(current); + } + current = builder.backend().parent(current); + } + return builder.getPathToElementName(elem); + }; + + for (const auto& barrel : barrels) { + auto sensors = builder.resolveSensitives(barrel); + auto barrelNode = builder.layersFromSensors() + .barrel() + .setSensorAxes("XYZ") + .setSensors(std::move(sensors)) + .setContainerName(builder.backend().nameOf(barrel)) + .groupBy(sensorToLayerKey) + .onLayer(layerCustomizer) + .build(); + barrelNode->setAttachmentStrategy(Acts::VolumeAttachmentStrategy::Gap); + barrelNode->setResizeStrategies(Acts::VolumeResizeStrategy::Gap, + Acts::VolumeResizeStrategy::Gap); + containerNode->addChild(std::move(barrelNode)); + } + + for (const auto& endcap : endcaps) { + auto sensors = builder.resolveSensitives(endcap); + auto endcapNode = builder.layersFromSensors() + .endcap() + .setSensorAxes("XZY") + .setSensors(std::move(sensors)) + .setContainerName(builder.backend().nameOf(endcap)) + .groupBy(sensorToLayerKey) + .onLayer(layerCustomizer) + .build(); + endcapNode->setAttachmentStrategy(Acts::VolumeAttachmentStrategy::Gap); + endcapNode->setResizeStrategies(Acts::VolumeResizeStrategy::Gap, + Acts::VolumeResizeStrategy::Gap); + containerNode->addChild(std::move(endcapNode)); + } + + outer.addChild(std::move(containerNode)); +} + +} // namespace + +std::unique_ptr buildOpenDataDetectorBarrelEndcap( + const dd4hep::Detector& detector, const Acts::GeometryContext& gctx, + const Acts::Logger& logger) { + using namespace Acts::Experimental; + using namespace Acts; + using enum AxisDirection; + + BlueprintBuilder builder{{ + .dd4hepDetector = &detector, + .lengthScale = Acts::UnitConstants::cm, + .gctx = gctx, + }, + logger.cloneWithSuffix("BlpBld")}; + + Blueprint::Config blueprintCfg; + blueprintCfg.envelope = ActsPlugins::DD4hep::detail::kBlueprintEnvelope; + Blueprint root{blueprintCfg}; + + auto& outer = root.addCylinderContainer("OpenDataDetector", AxisR); + outer.setAttachmentStrategy(VolumeAttachmentStrategy::Gap); + + outer.addChild(builder.backend().makeBeampipe()); + + addBarrelEndcapSubsystem(builder, outer, "Pixels", "pix", + ActsPlugins::DD4hep::detail::kPixelLayerFilter); + addBarrelEndcapSubsystem(builder, outer, "ShortStrips", "ss", + ActsPlugins::DD4hep::detail::kShortStripLayerFilter); + addBarrelEndcapSubsystem(builder, outer, "LongStrips", "ls", + ActsPlugins::DD4hep::detail::kLongStripLayerFilter); + + return root.construct(BlueprintOptions{}, gctx, logger); +} + +std::unique_ptr buildOpenDataDetectorDirectLayer( + const dd4hep::Detector& detector, const Acts::GeometryContext& gctx, + const Acts::Logger& logger) { + using namespace Acts::Experimental; + using namespace Acts; + using enum AxisDirection; + + BlueprintBuilder builder{{ + .dd4hepDetector = &detector, + .lengthScale = Acts::UnitConstants::cm, + .gctx = gctx, + }, + logger.cloneWithSuffix("BlpBld")}; + + Blueprint::Config blueprintCfg; + blueprintCfg.envelope = ActsPlugins::DD4hep::detail::kBlueprintEnvelope; + Blueprint root{blueprintCfg}; + + auto& outer = root.addCylinderContainer("OpenDataDetector", AxisR); + outer.setAttachmentStrategy(VolumeAttachmentStrategy::Gap); + + outer.addChild(builder.backend().makeBeampipe()); + + addDirectLayerSubsystem(builder, outer, "Pixels", "pix", + ActsPlugins::DD4hep::detail::kPixelLayerFilter); + addDirectLayerSubsystem(builder, outer, "ShortStrips", "ss", + ActsPlugins::DD4hep::detail::kShortStripLayerFilter); + addDirectLayerSubsystem(builder, outer, "LongStrips", "ls", + ActsPlugins::DD4hep::detail::kLongStripLayerFilter); + + return root.construct(BlueprintOptions{}, gctx, logger); +} + +std::unique_ptr buildOpenDataDetectorDirectLayerGrouped( + const dd4hep::Detector& detector, const Acts::GeometryContext& gctx, + const Acts::Logger& logger) { + using namespace Acts::Experimental; + using namespace Acts; + using enum AxisDirection; + + BlueprintBuilder builder{{ + .dd4hepDetector = &detector, + .lengthScale = Acts::UnitConstants::cm, + .gctx = gctx, + }, + logger.cloneWithSuffix("BlpBld")}; + + Blueprint::Config blueprintCfg; + blueprintCfg.envelope = ActsPlugins::DD4hep::detail::kBlueprintEnvelope; + Blueprint root{blueprintCfg}; + + auto& outer = root.addCylinderContainer("OpenDataDetector", AxisR); + outer.setAttachmentStrategy(VolumeAttachmentStrategy::Gap); + + outer.addChild(builder.backend().makeBeampipe()); + + addDirectLayerGroupedSubsystem( + builder, outer, "Pixels", "pix", + ActsPlugins::DD4hep::detail::kPixelLayerFilter); + addDirectLayerGroupedSubsystem( + builder, outer, "ShortStrips", "ss", + ActsPlugins::DD4hep::detail::kShortStripLayerFilter); + addDirectLayerGroupedSubsystem( + builder, outer, "LongStrips", "ls", + ActsPlugins::DD4hep::detail::kLongStripLayerFilter); + + return root.construct(BlueprintOptions{}, gctx, logger); +} + +} // namespace ActsPlugins::DD4hep diff --git a/Plugins/Root/include/ActsPlugins/Root/TGeoSurfaceConverter.hpp b/Plugins/Root/include/ActsPlugins/Root/TGeoSurfaceConverter.hpp index 7effa832aa0..8f7521e132b 100644 --- a/Plugins/Root/include/ActsPlugins/Root/TGeoSurfaceConverter.hpp +++ b/Plugins/Root/include/ActsPlugins/Root/TGeoSurfaceConverter.hpp @@ -98,6 +98,19 @@ struct TGeoSurfaceConverter { const TGeoShape& tgShape, const TGeoMatrix& tgMatrix, TGeoAxes axes, double scalor = 10.) noexcept(false); + /// Extract the transform from a TGeoShape by trying cylinder, disc, then + /// plane conversion. + /// + /// @param tgShape The TGeoShape + /// @param tgMatrix The matrix representing the transform + /// @param axes The axes definition + /// @param scalor The unit scalor between TGeo and Acts + /// @return The Acts transform + /// @throws std::runtime_error if the shape cannot be converted + static Acts::Transform3 transformFromShape( + const TGeoShape& tgShape, const TGeoMatrix& tgMatrix, TGeoAxes axes, + double scalor = 10.) noexcept(false); + /// Translate TGeo degree [0, 360) to radian /// * will correct to [-pi,pi) /// * it will return any multiple of 360.0 to 2pi diff --git a/Plugins/Root/src/TGeoSurfaceConverter.cpp b/Plugins/Root/src/TGeoSurfaceConverter.cpp index db6be568ada..d65928a0f36 100644 --- a/Plugins/Root/src/TGeoSurfaceConverter.cpp +++ b/Plugins/Root/src/TGeoSurfaceConverter.cpp @@ -484,3 +484,32 @@ ActsPlugins::TGeoSurfaceConverter::toSurface(const TGeoShape& tgShape, return {nullptr, 0.}; } + +Acts::Transform3 ActsPlugins::TGeoSurfaceConverter::transformFromShape( + const TGeoShape& tgShape, const TGeoMatrix& tgMatrix, TGeoAxes axes, + double scalor) noexcept(false) { + const Double_t* rotation = tgMatrix.GetRotationMatrix(); + const Double_t* translation = tgMatrix.GetTranslation(); + + auto [cBounds, cTransform, cThickness] = + cylinderComponents(tgShape, rotation, translation, axes, scalor); + if (cBounds != nullptr) { + return cTransform; + } + + auto [dBounds, dTransform, dThickness] = + discComponents(tgShape, rotation, translation, axes, scalor); + if (dBounds != nullptr) { + return dTransform; + } + + auto [pBounds, pTransform, pThickness] = + planeComponents(tgShape, rotation, translation, axes, scalor); + if (pBounds != nullptr) { + return pTransform; + } + + throw std::runtime_error( + "Could not extract transform from TGeoShape of type " + + std::string(tgShape.ClassName())); +} diff --git a/Python/Core/src/Geometry.cpp b/Python/Core/src/Geometry.cpp index c03562cc10c..8d9bcacf328 100644 --- a/Python/Core/src/Geometry.cpp +++ b/Python/Core/src/Geometry.cpp @@ -275,12 +275,17 @@ void addGeometry(py::module_& m) { } { - py::class_>(m, "Volume"); + py::class_>(m, "Volume") + .def_property_readonly( + "volumeBounds", + py::overload_cast<>(&Volume::volumeBounds, py::const_), + py::return_value_policy::reference_internal); py::class_>( m, "TrackingVolume") .def(py::init, - std::string>()); + std::string>()) + .def_property_readonly("volumeName", &TrackingVolume::volumeName); } { diff --git a/Python/Examples/python/odd.py b/Python/Examples/python/odd.py index 8f10e3b12ce..1e4725f2718 100644 --- a/Python/Examples/python/odd.py +++ b/Python/Examples/python/odd.py @@ -24,6 +24,7 @@ def getOpenDataDetector( odd_dir: Optional[Path] = None, logLevel=acts.logging.INFO, gen3=False, + constructionMethod=None, ): """This function sets up the open data detector. Requires DD4hep. Parameters @@ -31,6 +32,8 @@ def getOpenDataDetector( materialDecorator: Material Decorator, take RootMaterialDecorator if non is given odd_dir: if not given, try to get via ODD_PATH environment variable logLevel: logging level + constructionMethod: Gen3 conversion method enum value of + OpenDataDetector.Config.ConstructionMethod """ import acts.examples.dd4hep @@ -89,6 +92,8 @@ def getOpenDataDetector( logLevel=customLogLevel(), dd4hepLogLevel=customLogLevel(minLevel=acts.logging.WARNING), ) + if constructionMethod is not None: + oddConfig.constructionMethod = constructionMethod # Use default constructed geometry context. This will have to change if DD4hep gains alignment awareness. gctx = acts.GeometryContext.dangerouslyDefaultConstruct() with warnings.catch_warnings(): diff --git a/Python/Examples/src/plugins/DD4hep.cpp b/Python/Examples/src/plugins/DD4hep.cpp index 68d3f4d0e25..c257b690f46 100644 --- a/Python/Examples/src/plugins/DD4hep.cpp +++ b/Python/Examples/src/plugins/DD4hep.cpp @@ -74,7 +74,17 @@ PYBIND11_MODULE(ActsExamplesPythonBindingsDD4hep, m) { auto c = py::class_( odd, "Config") .def(py::init<>()); - // ACTS_PYTHON_STRUCT(c, ); + py::enum_( + c, "ConstructionMethod") + .value("BarrelEndcap", + OpenDataDetector::Config::ConstructionMethod::BarrelEndcap) + .value("DirectLayer", + OpenDataDetector::Config::ConstructionMethod::DirectLayer) + .value( + "DirectLayerGrouped", + OpenDataDetector::Config::ConstructionMethod::DirectLayerGrouped); + ACTS_PYTHON_STRUCT(c, detectorElementFactory, blueprintEnvelope, + layerEnvelope, constructionMethod); patchKwargsConstructor(c); } diff --git a/Python/Examples/tests/test_geometry.py b/Python/Examples/tests/test_geometry.py index 784a94a38c6..ba85bb28ec6 100644 --- a/Python/Examples/tests/test_geometry.py +++ b/Python/Examples/tests/test_geometry.py @@ -11,7 +11,7 @@ @pytest.mark.parametrize( "detectorFactory,aligned,nobj", [ - (functools.partial(GenericDetector, gen3=False), True, 450), + (functools.partial(GenericDetector, gen3=False), True, 2), pytest.param( functools.partial(GenericDetector, gen3=True), True, @@ -20,7 +20,7 @@ pytest.param( getOpenDataDetector, True, - 540, + 2, marks=[ pytest.mark.skipif(not dd4hepEnabled, reason="DD4hep not set up"), pytest.mark.slow, @@ -143,15 +143,37 @@ def test_odd_gen1(): @pytest.mark.skipif(not dd4hepEnabled, reason="DD4hep not set up") @pytest.mark.odd -def test_odd_gen3(): - with getOpenDataDetector(gen3=True) as detector: +@pytest.mark.parametrize( + "constructionMethod", + [ + pytest.param(None, id="default"), + pytest.param("BarrelEndcap", id="barrel-endcap"), + pytest.param("DirectLayer", id="direct-layer"), + pytest.param("DirectLayerGrouped", id="direct-layer-grouped"), + ], +) +def test_odd_gen3(constructionMethod): + import acts.examples.dd4hep as dd4hep + + cm = None + if constructionMethod is not None: + cm = getattr( + dd4hep.OpenDataDetector.Config.ConstructionMethod, constructionMethod + ) + + with getOpenDataDetector(gen3=True, constructionMethod=cm) as detector: trackingGeometry = detector.trackingGeometry() visitor = CountingVisitor() trackingGeometry.apply(visitor) - assert visitor.num_surfaces == 9 + # Gen3 invariants that hold regardless of construction method assert visitor.num_layers == 0 # Gen3: no layers - assert visitor.num_volumes == 2 - assert visitor.num_portals == 9 # Gen3: will have portals assert visitor.num_boundary_surfaces == 0 # Gen3: no boundary surfaces + assert visitor.num_portals > 0 # Gen3: uses portals instead + assert visitor.num_surfaces > 0 + assert visitor.num_volumes > 0 + + assert visitor.num_surfaces == 19261 + assert visitor.num_volumes == 109 + assert visitor.num_portals == 437 diff --git a/Tests/UnitTests/Core/Utilities/CMakeLists.txt b/Tests/UnitTests/Core/Utilities/CMakeLists.txt index 039ccdb315f..86ced89a154 100644 --- a/Tests/UnitTests/Core/Utilities/CMakeLists.txt +++ b/Tests/UnitTests/Core/Utilities/CMakeLists.txt @@ -13,6 +13,7 @@ add_unittest(BoundingBox BoundingBoxTest.cpp) add_unittest(DBScan DBScanTests.cpp) add_unittest(Extendable ExtendableTests.cpp) add_unittest(FiniteStateMachine FiniteStateMachineTests.cpp) +add_unittest(FunctionComposition FunctionCompositionTests.cpp) add_unittest(Frustum FrustumTest.cpp) add_unittest(Grid GridTests.cpp) add_unittest(GridAccessHelpers GridAccessHelpersTests.cpp) diff --git a/Tests/UnitTests/Core/Utilities/FunctionCompositionTests.cpp b/Tests/UnitTests/Core/Utilities/FunctionCompositionTests.cpp new file mode 100644 index 00000000000..768a6dfc105 --- /dev/null +++ b/Tests/UnitTests/Core/Utilities/FunctionCompositionTests.cpp @@ -0,0 +1,50 @@ +// 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 + +#include "Acts/Utilities/FunctionComposition.hpp" + +#include +#include + +namespace ActsTests { + +BOOST_AUTO_TEST_SUITE(UtilitiesSuite) + +BOOST_AUTO_TEST_CASE(ComposeTwoFunctions) { + auto addOne = [](int x) { return x + 1; }; + auto doubleValue = [](int x) { return 2 * x; }; + + auto composed = Acts::compose(doubleValue, addOne); + + BOOST_CHECK_EQUAL(composed(3), 8); +} + +BOOST_AUTO_TEST_CASE(ComposeMultipleFunctions) { + auto decorate = [](const std::string& s) { return "[" + s + "]"; }; + auto appendB = [](const std::string& s) { return s + "b"; }; + auto appendA = [](const std::string& s) { return s + "a"; }; + + auto composed = Acts::compose(decorate, appendB, appendA); + + BOOST_CHECK_EQUAL(composed(std::string{"x"}), "[xab]"); +} + +BOOST_AUTO_TEST_CASE(ComposeForwardsMoveOnlyInput) { + auto timesTwo = [](int x) { return x * 2; }; + auto takeOwnership = [](std::unique_ptr ptr) { return *ptr + 1; }; + + auto composed = Acts::compose(timesTwo, takeOwnership); + + BOOST_CHECK_EQUAL(composed(std::make_unique(4)), 10); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace ActsTests