diff --git a/Core/include/Acts/Geometry/Blueprint.hpp b/Core/include/Acts/Geometry/Blueprint.hpp new file mode 100644 index 00000000000..db96654218e --- /dev/null +++ b/Core/include/Acts/Geometry/Blueprint.hpp @@ -0,0 +1,105 @@ +// 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/BlueprintNode.hpp" +#include "Acts/Geometry/PortalShell.hpp" +#include "Acts/Geometry/TrackingGeometry.hpp" +#include "Acts/Geometry/TrackingVolume.hpp" + +namespace Acts { + +class GeometryContext; + +/// This class is the top-level entry point to build a tracking geometry using +/// the blueprint building mechanism. It forms the root of a tree of nodes where +/// each node performs a portion of the construction. This top-level class has +/// the main construction methods that execute the construction of the geometry. +/// +/// ``` +/// +---------------+ +-----------+ +/// | | | | +/// | Root | | v +/// | | | +---------------+ +/// +---------------+ | | | +/// | | | Child 1 | +/// +----------+ | | | +/// v +----------+ +---------------+ +/// +---------------+ | +/// | | +--------------+ +/// | Child 2 | v +----------+ +/// | | .---------. | | +/// +---------------+ / \ | v +/// ( Proc node ) | +---------------+ +/// `. ,' | | | +/// `-------' | | Child 3 | +/// | | | | +/// | | +---------------+ +/// +---------+ +/// ``` +/// +/// The construction phases are documented in @c BlueprintNode, which is the +/// base class for all nodes in the tree. +/// @note This class inherits from @c BlueprintNode, but hides the main +/// blueprint construction phase overloads. The @c Blueprint class is +/// only ever intended to be the top-level node, and not anywhere else +/// in the tree. +class Blueprint : public BlueprintNode { + public: + struct Config { + /// Determine how much envelope space to produce from the highest volume + /// in the geometry hierarchy and the world volume. + ExtentEnvelope envelope = ExtentEnvelope::Zero(); + + /// The geometry identifier hook, passed through the `TrackingGeometry` + /// constructor. This will be superseded by a more integrated approach to + /// the identification scheme + GeometryIdentifierHook geometryIdentifierHook = {}; + }; + + /// Constructor from a config object + /// @param config The configuration object + explicit Blueprint(const Config& config); + + /// Construct the tracking geometry from the blueprint tree + /// @param options The construction options, see @c BlueprintOptions + /// @param gctx The geometry context for construction. In almost all cases, + /// this should be the *nominal* geometry context + /// @param logger The logger to use for output during construction + std::unique_ptr construct( + const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()); + + protected: + /// The name of the blueprint node, always "Root" + /// @return The name + const std::string& name() const override; + + /// @copydoc BlueprintNode::build + Volume& build(const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()) override; + + /// @copydoc BlueprintNode::connect + PortalShellBase& connect( + const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()) override; + + /// @copydoc BlueprintNode::finalize + void finalize(const BlueprintOptions& options, const GeometryContext& gctx, + TrackingVolume& parent, + const Logger& logger = Acts::getDummyLogger()) override; + + /// @copydoc BlueprintNode::addToGraphviz + void addToGraphviz(std::ostream& os) const override; + + private: + Config m_cfg; +}; + +} // namespace Acts diff --git a/Core/include/Acts/Geometry/BlueprintNode.hpp b/Core/include/Acts/Geometry/BlueprintNode.hpp new file mode 100644 index 00000000000..e159c463067 --- /dev/null +++ b/Core/include/Acts/Geometry/BlueprintNode.hpp @@ -0,0 +1,295 @@ +// 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/BlueprintOptions.hpp" +#include "Acts/Geometry/GeometryContext.hpp" +#include "Acts/Geometry/NavigationPolicyFactory.hpp" +#include "Acts/Utilities/BinningType.hpp" +#include "Acts/Utilities/Logger.hpp" +#include "Acts/Utilities/TransformRange.hpp" + +#include +#include +#include +#include + +namespace Acts { + +class Volume; +class TrackingVolume; +class VolumeBounds; +class PortalShellBase; +class CylinderContainerBlueprintNode; +class MaterialDesignatorBlueprintNode; +class StaticBlueprintNode; +class LayerBlueprintNode; + +/// Base class for all nodes in the blueprint tree. This class defines the +/// three-phase construction process. The three phases are +/// +/// -# **Build**: Construct volume representation + compute final sizing +/// -# **Connect**: Create and connect portals at volume boundaries +/// -# **Finalize**: Register portals with volumes + create acceleration +/// structures +/// +/// During the *build* phase, the `build` method of all nodes in the tree are +/// called recursively. Some nodes, like @ref Acts::CylinderContainerBlueprintNode, +/// will take action on the volumes returns from its children, and perform +/// sizing to connect them. See the @ref Acts::CylinderContainerBlueprintNode and @ref +/// Acts::CylinderVolumeStack documentation for details on how the sizing is +/// carried out. +class BlueprintNode { + public: + /// Virtual destructor to ensure correct cleanup + virtual ~BlueprintNode() = default; + + /// Get the name of this node + virtual const std::string& name() const = 0; + + /// @anchor construction + /// @name Construction methods + /// These methods constitute the primary interface of the node that + /// participates in the geometry construction. + /// @{ + + /// This method is called during the *build* phase of the blueprint tree + /// construction. It returns a single @ref Acts::Volume which represents transform + /// and bounds of the entire subtree. This does not have to correspond to the + /// final @ref Acts::TrackingVolume, some node types will produce temporary volume + /// representations. Lifetime of the returned volume is managed by the source + /// node! + /// Nodes higher in the hierarchy will issue resizes down the tree hierarchy. + /// This is not done through a direct hierarchy, but coordinated by the + /// respective node type, by internally consulting its children. + /// + /// @note Generally, you should not need to to call this method directly. + /// The construction should usually be done through the special + /// @ref Acts::Blueprint class. + /// + /// @param options The global construction options + /// @param gctx The geometry context for construction (usually nominal) + /// @param logger The logger to use for output during construction + /// @return The volume used for communicating transform and size up the hierarchy + virtual Volume& build(const BlueprintOptions& options, + const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()) = 0; + + /// This method is called during the *connect* phase. This phase handles the + /// creation and connection of *portals* (instances of @ref Acts::PortalLinkBase). + /// After the build-phase has completed, the volume sizes are **final**. Each + /// node will consult its fully sized volume to produce *boundary surfaces*. + /// Each boundary surface is then turned into a @ref Acts::TrivialPortalLink, which + /// in turn produces a one-sided portal (see @ref Acts::Portal documentation) + /// + /// Some nodes (like @ref Acts::CylinderContainerBlueprintNode) will take action on + /// their children, and unify the connected portals. + /// + /// After a node's processing has completed, it returns a reference to a @ref + /// Acts::PortalShellBase, which represents a set of portals in a specific + /// geometry arrangement. The returned object lifetime is managed by the + /// returning node. + /// + /// @param options The global construction options + /// @param gctx The geometry context for construction (usually nominal) + /// @param logger The logger to use for output during construction + virtual PortalShellBase& connect( + const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()) = 0; + + /// This method is called during the *finalize* phase. This phase handles: + /// + /// - Registering portals into their final volumes + /// - Registering volumes into their parents + /// - Creating navigation policies + /// - (In future) Handle geometry identification assignment + /// + /// At the end of this phase, each node will have transferred any temporary + /// resources created during the build, that need to be retained, into the + /// final @ref Acts::TrackingGeometry, and can be safely destroyed. + /// + /// @note The @p parent for volumes, portals, etc to be registered in is passed in **as an + /// argument**, rather than being implicitly determined from the + /// **parent node**. This is done so that nodes can remove themselves + /// from the final volume hierarchy, like container nodes or the + /// @ref Acts::MaterialDesignatorBlueprintNode. + /// + /// @param options The global construction options + /// @param gctx The geometry context for construction (usually nominal) + /// @param parent The parent volume to register in + /// @param logger The logger to use for output during construction + virtual void finalize(const BlueprintOptions& options, + const GeometryContext& gctx, TrackingVolume& parent, + const Logger& logger = Acts::getDummyLogger()) = 0; + + /// @} + + /// @anchor convenience + /// @name Convenience methods + /// These methods are meant to make the construction of a blueprint tree in + /// code more ergonomic. + /// They usually take an optional `callback` parameter. The primary use for + /// this parameter is structural, as it facilitates introducing scopes to + /// indicate in code that objects are nested. + /// + /// ```cpp + /// Blueprint::Config cfg; + /// auto root = std::make_unique(cfg); + /// root->addStaticVolume( + /// base, std::make_shared(50_mm, 400_mm, 1000_mm), + /// "PixelWrapper", [&](auto& wrapper) { + /// // This scope can be used to equip `wrapper` + /// }); + /// ``` + /// + /// Alternatively, they can also be used without a callback, as the newly + /// created node is also returned by reference: + /// + /// ``` + /// auto& wrapper = root->addStaticVolume( + /// base, std::make_shared(50_mm, 400_mm, 1000_mm), + /// "PixelWrapper"); + /// ``` + /// + /// In both cases, it's not necessary to register the newly created node + /// with a parent node. + /// + /// @{ + + /// This method creates a new @ref Acts::StaticBlueprintNode wrapping @p + /// volume and adds it to this node as a child. + /// @param volume The volume to add + /// @param callback An optional callback that receives the node as an argument + /// @return A reference to the created node + StaticBlueprintNode& addStaticVolume( + std::unique_ptr volume, + const std::function& callback = {}); + + /// Alternative overload for creating a @ref Acts::StaticBlueprintNode. This + /// overload will invoke the constructor of @ref Acts::TrackingVolume and use + /// that volume to create the node. + /// @param transform The transform of the volume + /// @param volumeBounds The bounds of the volume + /// @param volumeName The name of the volume + /// @param callback An optional callback that receives the node as an argument + StaticBlueprintNode& addStaticVolume( + const Transform3& transform, std::shared_ptr volumeBounds, + const std::string& volumeName = "undefined", + const std::function& callback = {}); + + /// Convenience method for creating a @ref Acts::CylinderContainerBlueprintNode. + /// @param name The name of the container node. This name is only reflected + /// in the node tree for debugging, as no extra volumes is created + /// for the container. + /// @param direction The direction of the stack configuration. See + /// @ref Acts::CylinderVolumeStack for details. + /// @param callback An optional callback that receives the node as an argument + CylinderContainerBlueprintNode& addCylinderContainer( + const std::string& name, BinningValue direction, + const std::function& + callback = {}); + + /// Convenience method for creating a @ref Acts::MaterialDesignatorBlueprintNode. + /// @param name The name of the material designator node. Used for debugging + /// the node tree only. + /// @param callback An optional callback that receives the node as an argument + MaterialDesignatorBlueprintNode& addMaterial( + const std::string& name, + const std::function& + callback = {}); + + /// Convenience method for creating a @ref Acts::LayerBlueprintNode. + /// @param name The name of the layer node. + /// @param callback An optional callback that receives the node as an argument + LayerBlueprintNode& addLayer( + const std::string& name, + const std::function& callback = {}); + + /// @} + + /// Register a @p child to this node. + /// @warning This method throws if adding the child would create a + /// cycle in the blueprint tree! + /// @param child The child node to add + /// @return A reference this node (not the child!) + BlueprintNode& addChild(std::shared_ptr child); + + /// A range-like object that allows range based for loops and index access. + /// This type's iterators and accessors return mutable references when + /// dereferenced. + using MutableChildRange = + detail::TransformRange>>; + + /// A range-like object that allows range based for loops and index access. + /// This type's iterators and accessors return const references when + /// dereferenced. + using ChildRange = + detail::TransformRange>>; + + /// Return a @ref MutableChildRange to the children of this node. + /// @return A range-like object to the children + MutableChildRange children(); + + /// Return a @ref ChildRange to the children of this node. + /// @return A range-like object to the children + ChildRange children() const; + + /// Remove all children from this node + void clearChildren(); + + /// Return the depth of this node in the blueprint tree. A depth of zero means + /// this node does not have a parent. + /// @return The depth of this node + std::size_t depth() const; + + /// Print the node tree starting from this node to graphviz format + /// @param os The stream to print to + void graphviz(std::ostream& os) const; + + /// Method that writes a representatiohn of **this node only** to graphviz. + /// This should generally not be called on its own, but through the @ref + /// BlueprintNode::graphviz method. + /// @param os The stream to print to + virtual void addToGraphviz(std::ostream& os) const; + + /// Print a representation of this node to the stream + /// @param os The stream to print to + /// @param node The node to print + /// @return The output stream + friend std::ostream& operator<<(std::ostream& os, const BlueprintNode& node) { + node.toStream(os); + return os; + } + + protected: + /// Virtual method to determine stream representation. + /// @note This method is called by the stream operator. + virtual void toStream(std::ostream& os) const; + + /// Set the depth to @p depth and update children recursively + void setDepth(std::size_t depth); + + /// Printing helper returning a prefix including an indent depending on the + /// depth. + /// @return The prefix string + std::string prefix() const; + + /// An indentation depending on the depth of this node. + /// @return The indentation string + std::string indent() const; + + private: + std::size_t m_depth{0}; + std::vector> m_children{}; +}; + +} // namespace Acts diff --git a/Core/include/Acts/Geometry/BlueprintOptions.hpp b/Core/include/Acts/Geometry/BlueprintOptions.hpp new file mode 100644 index 00000000000..305ff5d399d --- /dev/null +++ b/Core/include/Acts/Geometry/BlueprintOptions.hpp @@ -0,0 +1,28 @@ +// 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/NavigationPolicyFactory.hpp" + +#include + +namespace Acts { + +struct BlueprintOptions { + std::shared_ptr defaultNavigationPolicyFactory{ + makeDefaultNavigationPolicyFactory()}; + + void validate() const; + + private: + static std::unique_ptr + makeDefaultNavigationPolicyFactory(); +}; + +} // namespace Acts diff --git a/Core/include/Acts/Geometry/CompositePortalLink.hpp b/Core/include/Acts/Geometry/CompositePortalLink.hpp index 6586d875b79..231af047b6c 100644 --- a/Core/include/Acts/Geometry/CompositePortalLink.hpp +++ b/Core/include/Acts/Geometry/CompositePortalLink.hpp @@ -23,6 +23,7 @@ class Surface; /// Composite portal links can graft together other portal link instances, for /// example grids that could not be merged due to invalid binnings. /// +/// ``` /// +-------+ +-------+ /// | | | | /// | | | | @@ -36,6 +37,7 @@ class Surface; /// | | +-------+ /// | | | | /// +-------+ +-------+ +/// ``` /// /// During resolution, it will consult each of it's children and return /// the result on the first surface where the lookup position is within diff --git a/Core/include/Acts/Geometry/CylinderContainerBlueprintNode.hpp b/Core/include/Acts/Geometry/CylinderContainerBlueprintNode.hpp new file mode 100644 index 00000000000..637f1594c0c --- /dev/null +++ b/Core/include/Acts/Geometry/CylinderContainerBlueprintNode.hpp @@ -0,0 +1,157 @@ +// 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/BlueprintNode.hpp" +#include "Acts/Geometry/CylinderVolumeStack.hpp" +#include "Acts/Geometry/PortalShell.hpp" +#include "Acts/Utilities/BinningType.hpp" +#include "Acts/Utilities/Logger.hpp" + +#include + +namespace Acts { + +/// This class handles the case of wrapping a set of cylinder-shaped children +/// and stacking them in a configured direction. +/// The stacking is done using @ref CylinderVolumeStack. +/// The container does not result in an extra volume in the hierarchy, as all +/// input volumes and any gap volumes produced are directly registered in the +/// volume of the parent of this node. +/// @note This node assumes all children produce only cylinder volumes! It throws +/// if this is not the case. +class CylinderContainerBlueprintNode final : public BlueprintNode { + public: + /// Main constructor for the cylinder container node. + /// @param name The name of the node (for debug only) + /// @param direction The stacking direction + /// @param attachmentStrategy The attachment strategy for the stack + /// @param resizeStrategy The resize strategy + /// @note The parameters are passed through to @ref CylinderVolumeStack, + /// see documentation of that class for more information + CylinderContainerBlueprintNode( + const std::string& name, BinningValue direction, + CylinderVolumeStack::AttachmentStrategy attachmentStrategy = + CylinderVolumeStack::AttachmentStrategy::Midpoint, + CylinderVolumeStack::ResizeStrategy resizeStrategy = + CylinderVolumeStack::ResizeStrategy::Expand); + + /// @copydoc BlueprintNode::name + const std::string& name() const override; + + /// This participates in the construction of the geometry via the blueprint + /// tree. The steps are approximately as follows: + /// -# Collect all child volumes + /// -# Package them into a @ref Acts::CylinderVolumeStack, which performs + /// sizing and/or gap creation + /// -# Return the @ref Acts::CylinderVolumeStack as a volume up the tree + /// + /// @param options The global blueprint options + /// @param gctx The geometry context (nominal usually) + /// @param logger The logger to use + /// @return The combined @ref Acts::CylinderVolumeStack + Volume& build(const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()) override; + + /// This participates in the construction of the geometry via the blueprint + /// tree. The steps are approximately as follows: + /// -# Walk through all volumes that were created by the build phase + /// -# Check if they are: *real* child volumes or gap volumes + /// - If gap volume: produce a @ref Acts::TrackingVolume, and wrap it in a single use shell + /// - If child volume: locate the right child node it came from, call + /// ` connect` and collect the returned shell + /// -# Produce a combined @ref Acts::CylinderStackPortalShell from all the shells + /// -# Return that shell representation + /// + /// @param options The global blueprint options + /// @param gctx The geometry context (nominal usually) + /// @param logger The logger to use + /// @return The combined @ref Acts::CylinderStackPortalShell + CylinderStackPortalShell& connect( + const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()) override; + + /// This participates in the construction of the geometry via the blueprint + /// tree. The steps are approximately as follows: + /// -# Register portals created for gap volumes, as they're not handled by + /// dedicated nodes + /// -# Register gap volumes in the @p parent volume + /// -# Create a configured @ref Acts::INavigationPolicy for the gap + /// -# Call `finalize` on all children while passing through @p parent. + /// + /// @param options The global blueprint options + /// @param gctx The geometry context (nominal usually) + /// @param parent The parent volume + /// @param logger The logger to use + void finalize(const BlueprintOptions& options, const GeometryContext& gctx, + TrackingVolume& parent, const Logger& logger) override; + + /// Setter for the stacking direction + /// @param direction The stacking direction + /// @return This node for chaining + CylinderContainerBlueprintNode& setDirection(BinningValue direction); + + /// Setter for the attachment strategy + /// @param attachmentStrategy The attachment strategy + /// @return This node for chaining + CylinderContainerBlueprintNode& setAttachmentStrategy( + CylinderVolumeStack::AttachmentStrategy attachmentStrategy); + + /// Setter for the resize strategy + /// @param resizeStrategy The resize strategy + /// @return This node for chaining + CylinderContainerBlueprintNode& setResizeStrategy( + CylinderVolumeStack::ResizeStrategy resizeStrategy); + + /// Accessor to the stacking direction + /// @return The stacking direction + BinningValue direction() const; + + /// Accessor to the attachment strategy + /// @return The attachment strategy + CylinderVolumeStack::AttachmentStrategy attachmentStrategy() const; + + /// Accessor to the resize strategy + /// @return The resize strategy + CylinderVolumeStack::ResizeStrategy resizeStrategy() const; + + private: + /// @copydoc BlueprintNode::addToGraphviz + void addToGraphviz(std::ostream& os) const override; + + /// Helper function to check if a volume was created as a gap volume. + /// @param volume The volume to check + /// @return True if the volume is a gap volume, false otherwise + bool isGapVolume(const Volume& volume) const; + + std::vector collectChildShells( + const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger); + + std::string m_name; + + BinningValue m_direction = BinningValue::binZ; + + CylinderVolumeStack::AttachmentStrategy m_attachmentStrategy{ + CylinderVolumeStack::AttachmentStrategy::Midpoint}; + + CylinderVolumeStack::ResizeStrategy m_resizeStrategy{ + CylinderVolumeStack::ResizeStrategy::Expand}; + + // Is only initialized during `build` + std::vector m_childVolumes; + std::unique_ptr m_stack{nullptr}; + std::map m_volumeToNode; + std::vector, + std::unique_ptr>> + m_gaps; + std::unique_ptr m_shell{nullptr}; +}; + +} // namespace Acts diff --git a/Core/include/Acts/Geometry/GridPortalLink.hpp b/Core/include/Acts/Geometry/GridPortalLink.hpp index 8bf1d06ee08..94e98867905 100644 --- a/Core/include/Acts/Geometry/GridPortalLink.hpp +++ b/Core/include/Acts/Geometry/GridPortalLink.hpp @@ -111,6 +111,7 @@ class GridPortalLink : public PortalLinkBase { /// /// 1D merge scenarios: /// + /// ``` /// +----------------------------------------------------------+ /// |Colinear | /// | | @@ -126,9 +127,11 @@ class GridPortalLink : public PortalLinkBase { /// | +-------+-------+-------+-------+-------+-------+ | /// | | /// +----------------------------------------------------------+ + /// ``` /// /// Two grid along a shared direction are merged along their shared direction /// + /// ``` /// +-------------------------------------------------+ /// |Parallel | /// | | @@ -147,10 +150,12 @@ class GridPortalLink : public PortalLinkBase { /// | +-------+ +-------+ +-------+-------+ | /// | | /// +-------------------------------------------------+ + /// ``` /// /// Two grids along a shared direction a merged in the direction that is /// orthogonal to their shared direction. /// + /// ``` /// +-------------------------------------------+ /// |Perpendicular | /// | | @@ -180,6 +185,7 @@ class GridPortalLink : public PortalLinkBase { /// | +-------+-------+-------+ | /// | | /// +-------------------------------------------+ + /// ``` /// /// Two grids whose directions are not shared are merged (ordering does not /// matter here). The routine will expand one of the grids to match the @@ -192,6 +198,7 @@ class GridPortalLink : public PortalLinkBase { /// side. The 1D grid is expanded to match the binning in the as-of-yet /// unbinned direction with the binning taken from the 2D grid. /// + /// ``` /// +-----------------------------------------+ /// |2D + 1D | /// | | @@ -215,7 +222,9 @@ class GridPortalLink : public PortalLinkBase { /// | | | | | /// | +-------+-------+ | /// +-----------------------------------------+ + /// ``` /// + /// ``` /// +--------------------------------------------------------------+ /// |2D + 1D | /// | | @@ -234,6 +243,7 @@ class GridPortalLink : public PortalLinkBase { /// | +-------+-------+ +-------+ +-------+-------+-------+ | /// | | /// +--------------------------------------------------------------+ + /// ``` /// /// 2D merges /// The grids need to already share a common axis. If that is not the case, @@ -241,6 +251,7 @@ class GridPortalLink : public PortalLinkBase { /// merging as a fallback if needed. /// Ordering and direction does not matter here. /// + /// ``` /// +-----------------------------------------+ /// |2D + 2D | /// | | @@ -273,6 +284,7 @@ class GridPortalLink : public PortalLinkBase { /// | +-------+-------+-------+-------+ | /// | | /// +-----------------------------------------+ + /// ``` /// /// @param a The first grid portal link /// @param b The second grid portal link diff --git a/Core/include/Acts/Geometry/LayerBlueprintNode.hpp b/Core/include/Acts/Geometry/LayerBlueprintNode.hpp new file mode 100644 index 00000000000..5eb2e747df8 --- /dev/null +++ b/Core/include/Acts/Geometry/LayerBlueprintNode.hpp @@ -0,0 +1,141 @@ +// 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/StaticBlueprintNode.hpp" + +#include + +namespace Acts { + +/// The layer node is essentially an auto-sizing wrapper around a set of +/// surfaces. +/// @note This implementation is **preliminary** and will likely change +/// in the future. +/// It defers most of the functionality to @ref Acts::StaticBlueprintNode, +/// after the initial volume creation is completed. +/// +/// The layer volume is created to wrap around the surfaces registered with +/// this node. The orientation of the resulting volume defaults to the identity +/// matrix. If another orientation is desired, this can be set with the @ref +/// Acts::LayerBlueprintNode::setTransform. See @ref Acts::ProtoLayer for +/// details on the auto-sizing from surfaces. +/// +class LayerBlueprintNode : public StaticBlueprintNode { + public: + /// Enum that lists out the supported layer types. + enum class LayerType { + /// A cylinder layer + Cylinder, + + /// A disc layer + Disc, + + /// A plane layer + /// @note This is not yet implemented + Plane + }; + + /// Constructor for a layer node. + /// @param name The name of the layer + explicit LayerBlueprintNode(const std::string& name) + : StaticBlueprintNode{nullptr}, m_name(name) {} + + /// @copydoc BlueprintNode::name + const std::string& name() const override; + + /// This function participates in the geometry construction. + /// It will: + /// -# Analyze the surfaces provided and produce a wrapping volume + /// -# Register the surfaces with the volume + /// -# Return the volume + /// @note At least one surfaces needs to be registered via + /// @ref Acts::LayerBlueprintNode::setSurfaces before + /// geometry construction. + Volume& build(const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()) override; + + /// Register a set of surfaces with the layer node. + /// @param surfaces The surfaces to register + /// @return Reference to this node for chaining + LayerBlueprintNode& setSurfaces( + std::vector> surfaces); + + /// Access the registered surfaces. + /// @return The registered surfaces + const std::vector>& surfaces() const; + + /// Set the transformation of the layer node. + /// This can be used to specifically orient the resulting layer volume. + /// @param transform The transformation to set + /// @return Reference to this node for chaining + LayerBlueprintNode& setTransform(const Transform3& transform); + + /// Access the transformation of the layer node. + /// @return The transformation + const Transform3& transform() const; + + /// Set the envelope of the layer node. This configures the amount of space to + /// add around the contained surfaces. + /// @param envelope The envelope to set + /// @return Reference to this node for chaining + LayerBlueprintNode& setEnvelope(const ExtentEnvelope& envelope); + + /// Access the envelope of the layer node. + /// @return The envelope + const ExtentEnvelope& envelope() const; + + /// Set the layer type of the layer node. + /// @param layerType The layer type to set + /// @return Reference to this node for chaining + LayerBlueprintNode& setLayerType(LayerType layerType); + + /// Access the layer type of the layer node. + /// @return The layer type + const LayerType& layerType() const; + + /// Output operator for the layer type enum. + /// @param os The output stream + /// @param type The layer type + friend std::ostream& operator<<(std::ostream& os, + LayerBlueprintNode::LayerType type) { + switch (type) { + using enum LayerBlueprintNode::LayerType; + case Cylinder: + os << "Cylinder"; + break; + case Disc: + os << "Disc"; + break; + case Plane: + os << "Plane"; + break; + } + return os; + } + + private: + /// @copydoc Acts::BlueprintNode::addToGraphviz + void addToGraphviz(std::ostream& os) const override; + + /// Helper method that performs the volume creation from the configured + /// surfaces. It converts from an @p extent object to an instance of @ref + /// Acts::VolumeBounds. + /// @param extent The extent to use for the volume creation + /// @param logger The logger to use + void buildVolume(const Extent& extent, const Logger& logger); + + std::string m_name; + std::vector> m_surfaces{}; + Transform3 m_transform = Transform3::Identity(); + ExtentEnvelope m_envelope = ExtentEnvelope::Zero(); + LayerType m_layerType = LayerType::Cylinder; +}; + +} // namespace Acts diff --git a/Core/include/Acts/Geometry/MaterialDesignatorBlueprintNode.hpp b/Core/include/Acts/Geometry/MaterialDesignatorBlueprintNode.hpp new file mode 100644 index 00000000000..dbb4663fe71 --- /dev/null +++ b/Core/include/Acts/Geometry/MaterialDesignatorBlueprintNode.hpp @@ -0,0 +1,101 @@ +// 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/Detector/ProtoBinning.hpp" +#include "Acts/Geometry/BlueprintNode.hpp" +#include "Acts/Geometry/PortalShell.hpp" + +#include + +namespace Acts { + +/// This node type registers material proxies into its child volume during the +/// blueprint construction. It is configured ahead of time which volume faces to +/// mark up, and how do to so. +/// @note This node can only have a single child. This is not an error during +/// tree building, but during geometry construction. +/// @note This currently only supports a cylinder volume child +class MaterialDesignatorBlueprintNode final : public BlueprintNode { + public: + // @TODO: This needs cuboid volume storage as well + // @TODO: I don't love the type + using BinningConfig = std::variant>>; + + /// Main constructor for the material designator node. + /// @param name The name of the node (for debug only) + explicit MaterialDesignatorBlueprintNode(const std::string& name) + : m_name(name) {} + + /// @copydoc BlueprintNode::name + const std::string& name() const override; + + /// @copydoc BlueprintNode::toStream + void toStream(std::ostream& os) const override; + + /// This method participates in the geometry construction. + /// It checks that this node only has a single child, is correctly configured, + /// and forwards the call. + /// @param options The global blueprint options + /// @param gctx The geometry context (nominal usually) + /// @param logger The logger to use + /// @return The child volume + Volume& build(const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()) override; + + /// This method participates in the geometry construction. + /// It receives the populated portal shell from its only child and attaches + /// material proxies by consulting the configuration stored in the node. + /// @note Currently, this node will unconditionally attach + /// @ref Acts::ProtoGridSurfaceMaterial + /// + /// @param options The global blueprint options + /// @param gctx The geometry context (nominal usually) + /// @param logger The logger to use + /// @return The portal shell with material proxies attached + PortalShellBase& connect( + const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()) override; + + /// This method participates in the geometry construction. + /// Passes through the call to its only child. + /// @param options The global blueprint options + /// @param gctx The geometry context (nominal usually) + /// @param parent The parent volume + /// @param logger The logger to use during construction + void finalize(const BlueprintOptions& options, const GeometryContext& gctx, + TrackingVolume& parent, const Logger& logger) override; + + /// Retrieve the binning configuration + /// @return The binning configuration + const std::optional& binning() const; + + /// Set the binning configuration + /// @param binning The binning configuration + MaterialDesignatorBlueprintNode& setBinning(BinningConfig binning); + + private: + /// @copydoc BlueprintNode::addToGraphviz + void addToGraphviz(std::ostream& os) const override; + + void handleCylinderBinning( + CylinderPortalShell& cylShell, + const std::vector< + std::tuple>& binning, + const Logger& logger); + + std::string m_name{}; + + std::optional m_binning{}; +}; + +} // namespace Acts diff --git a/Core/include/Acts/Geometry/Portal.hpp b/Core/include/Acts/Geometry/Portal.hpp index 722b433d036..3d1a065026f 100644 --- a/Core/include/Acts/Geometry/Portal.hpp +++ b/Core/include/Acts/Geometry/Portal.hpp @@ -110,6 +110,7 @@ class Portal { /// precision). The resulting portal will have one portal along the shared /// surface's normal vector, and one opposite that vector. /// + /// ``` /// portal1 portal2 /// +---+ +---+ /// | | | | @@ -118,6 +119,7 @@ class Portal { /// | | | | /// | | | | /// +---+ +---+ + /// ``` /// /// @note The input portals need to have compatible link loadaout, e.g. one /// portal needs to have the *along normal* slot filled, and the @@ -140,6 +142,7 @@ class Portal { /// relative to one another (e.g. one along one opposite), the function will /// throw an exception. /// + /// ``` /// ^ ^ /// | | /// portal1| portal2| @@ -149,6 +152,7 @@ class Portal { /// | | /// | | /// v v + /// ``` /// /// @note This is a destructive operation on both portals, their /// links will be moved to produce merged links, which can fail diff --git a/Core/include/Acts/Geometry/StaticBlueprintNode.hpp b/Core/include/Acts/Geometry/StaticBlueprintNode.hpp new file mode 100644 index 00000000000..f1b2b9cbd0a --- /dev/null +++ b/Core/include/Acts/Geometry/StaticBlueprintNode.hpp @@ -0,0 +1,67 @@ +// 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/BlueprintNode.hpp" +#include "Acts/Geometry/GeometryContext.hpp" +#include "Acts/Geometry/PortalShell.hpp" +#include "Acts/Geometry/TrackingVolume.hpp" + +namespace Acts { + +/// The static blueprint node wraps a single already-constructred @c TrackingVolume. +/// The node will present this volume to its hierarchy. The volume is given as +/// mutable, and will be potentially enlarged in order to connect to neighboring +/// volumes. +/// - In case the volume already has child volumes, they will be retained. +/// - In case the volume already has a registered navigation policy, it will be +/// overwritten with the one configured on this node, regardless of content. +class StaticBlueprintNode : public BlueprintNode { + public: + /// Construct the static node from an existing volume + /// @param volume The volume to wrap + explicit StaticBlueprintNode(std::unique_ptr volume); + + /// Get the name of this node. It is automatically taken from the wrapped + /// volume + /// @return The name of the volume + const std::string& name() const override; + + /// @copydoc BlueprintNode::build + /// Build-phase of the blueprint construction. Returns the wrapped volume for + /// sizing. + Volume& build(const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()) override; + + /// @copydoc BlueprintNode::connect + PortalShellBase& connect( + const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger = Acts::getDummyLogger()) override; + + /// @copydoc BlueprintNode::finalize + void finalize(const BlueprintOptions& options, const GeometryContext& gctx, + TrackingVolume& parent, + const Logger& logger = Acts::getDummyLogger()) override; + + virtual StaticBlueprintNode& setNavigationPolicyFactory( + std::shared_ptr navigationPolicyFactory); + + const NavigationPolicyFactory* navigationPolicyFactory() const; + + protected: + void addToGraphviz(std::ostream& os) const override; + + std::unique_ptr m_volume; + + std::unique_ptr m_shell; + + std::shared_ptr m_navigationPolicyFactory; +}; + +} // namespace Acts diff --git a/Core/include/Acts/Material/ProtoVolumeMaterial.hpp b/Core/include/Acts/Material/ProtoVolumeMaterial.hpp index 38206e8f6dc..dd302e31ccb 100644 --- a/Core/include/Acts/Material/ProtoVolumeMaterial.hpp +++ b/Core/include/Acts/Material/ProtoVolumeMaterial.hpp @@ -72,12 +72,11 @@ class ProtoVolumeMaterial : public IVolumeMaterial { Material m_material; }; -/// Return the material inline const Acts::Material Acts::ProtoVolumeMaterial::material( const Acts::Vector3& /*position*/) const { return m_material; } -/// Return the bin Utility + inline const Acts::BinUtility& Acts::ProtoVolumeMaterial::binUtility() const { return m_binUtility; } diff --git a/Core/include/Acts/Seeding/PathSeeder.hpp b/Core/include/Acts/Seeding/PathSeeder.hpp index 87ceda365c8..f619f1a0ab4 100644 --- a/Core/include/Acts/Seeding/PathSeeder.hpp +++ b/Core/include/Acts/Seeding/PathSeeder.hpp @@ -108,8 +108,6 @@ class PathSeeder { /// @param gctx The geometry context /// @param sourceLinkGridLookup The lookup table for the source links /// @param seedCollection The collection of seeds to fill - /// - /// @return The vector of seeds template void findSeeds(const GeometryContext& gctx, const std::unordered_map& diff --git a/Core/include/Acts/Utilities/Delegate.hpp b/Core/include/Acts/Utilities/Delegate.hpp index a0ef2cf7252..8cac473abb9 100644 --- a/Core/include/Acts/Utilities/Delegate.hpp +++ b/Core/include/Acts/Utilities/Delegate.hpp @@ -50,7 +50,6 @@ class Delegate { /// Alias of the return type using return_type = R; using holder_type = H; - /// Alias to the function pointer type this class will store using function_type = return_type (*)(const holder_type *, Args...); using function_ptr_type = return_type (*)(Args...); using signature_type = R(Args...); @@ -81,12 +80,14 @@ class Delegate { Delegate(const Delegate &) noexcept = default; Delegate &operator=(const Delegate &) noexcept = default; + /// @cond /// Constructor with an explicit runtime callable /// @param callable The runtime value of the callable /// @note The function signature requires the first argument of the callable is `const void*`. /// i.e. if the signature of the delegate is `void(int)`, the /// callable's signature has to be `void(const void*, int)`. explicit Delegate(function_type callable) { connect(callable); } + /// @endcond /// Constructor with a possibly stateful function object. /// @tparam Callable Type of the callable @@ -129,6 +130,7 @@ class Delegate { requires(isNoFunPtr::value) = delete; + /// @cond /// Assignment operator with an explicit runtime callable /// @param callable The runtime value of the callable /// @note The function signature requires the first argument of the callable is `const void*`. @@ -147,6 +149,7 @@ class Delegate { { connect(callable); } + /// @endcond /// Assignment operator from rvalue reference is deleted, should catch /// assignment from temporary objects and thus invalid pointers @@ -155,6 +158,7 @@ class Delegate { requires(isNoFunPtr::value) = delete; + /// @cond /// Connect a free function pointer. /// @note The function pointer must be ``constexpr`` for @c Delegate to accept it /// @tparam Callable The compile-time free function pointer @@ -175,6 +179,7 @@ class Delegate { return std::invoke(Callable, std::forward(args)...); }; } + /// @endcond /// Assignment operator with possibly stateful function object. /// @tparam Callable Type of the callable @@ -195,6 +200,7 @@ class Delegate { requires(isNoFunPtr::value) = delete; + /// @cond /// Connect anything that is assignable to the function pointer /// @param callable The runtime value of the callable /// @note The function signature requires the first argument of the callable is `const void*`. @@ -206,6 +212,7 @@ class Delegate { } m_function = callable; } + /// @endcond template void connect(function_type callable, const Type *instance) diff --git a/Core/src/Geometry/Blueprint.cpp b/Core/src/Geometry/Blueprint.cpp new file mode 100644 index 00000000000..f4531acaa2f --- /dev/null +++ b/Core/src/Geometry/Blueprint.cpp @@ -0,0 +1,157 @@ +// 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 "Acts/Geometry/Blueprint.hpp" + +#include "Acts/Geometry/CuboidVolumeBounds.hpp" +#include "Acts/Geometry/CylinderVolumeBounds.hpp" +#include "Acts/Geometry/Extent.hpp" +#include "Acts/Geometry/PortalShell.hpp" +#include "Acts/Geometry/VolumeBounds.hpp" +#include "Acts/Navigation/INavigationPolicy.hpp" +#include "Acts/Utilities/GraphViz.hpp" + +namespace { +const std::string s_rootName = "Root"; +} + +namespace Acts { + +Blueprint::Blueprint(const Config &config) : m_cfg(config) {} + +const std::string &Blueprint::name() const { + return s_rootName; +} + +Volume &Blueprint::build(const BlueprintOptions & /*options*/, + const GeometryContext & /*gctx*/, + const Logger & /*logger*/) { + throw std::logic_error("Root node cannot be built"); +} + +PortalShellBase &Blueprint::connect(const BlueprintOptions & /*options*/, + const GeometryContext & /*gctx*/, + const Logger & /*logger*/) { + throw std::logic_error("Root node cannot be connected"); +} + +void Blueprint::finalize(const BlueprintOptions & /*options*/, + const GeometryContext & /*gctx*/, + TrackingVolume & /*parent*/, + const Logger & /*logger*/) { + throw std::logic_error("Root node cannot be finalized"); +} + +void Blueprint::addToGraphviz(std::ostream &os) const { + GraphViz::Node node{ + .id = name(), .label = "World", .shape = GraphViz::Shape::House}; + + os << node; + BlueprintNode::addToGraphviz(os); +} + +std::unique_ptr Blueprint::construct( + const BlueprintOptions &options, const GeometryContext &gctx, + const Logger &logger) { + using enum BinningValue; + + ACTS_INFO(prefix() << "Building tracking geometry from blueprint tree"); + + options.validate(); + + if (m_cfg.envelope == ExtentEnvelope::Zero()) { + ACTS_WARNING(prefix() << "Root node is configured with zero envelope. This " + "might lead to navigation issues"); + } + + if (children().size() != 1) { + ACTS_ERROR(prefix() << "Root node must have exactly one child"); + throw std::logic_error("Root node must have exactly one child"); + } + + auto &child = children().at(0); + + ACTS_DEBUG(prefix() << "Executing building on tree"); + Volume &topVolume = child.build(options, gctx, logger); + const auto &bounds = topVolume.volumeBounds(); + + std::stringstream ss; + bounds.toStream(ss); + ACTS_DEBUG(prefix() << "have top volume: " << ss.str() << "\n" + << topVolume.transform().matrix()); + + std::shared_ptr worldBounds; + + if (const auto *cyl = dynamic_cast(&bounds); + cyl != nullptr) { + using enum CylinderVolumeBounds::BoundValues; + + // Make a copy that we'll modify + auto newBounds = std::make_shared(*cyl); + + const auto &zEnv = m_cfg.envelope[binZ]; + if (zEnv[0] != zEnv[1]) { + ACTS_ERROR( + prefix() << "Root node cylinder envelope for z must be symmetric"); + throw std::logic_error( + "Root node cylinder envelope for z must be " + "symmetric"); + } + + const auto &rEnv = m_cfg.envelope[binR]; + + newBounds->set({ + {eHalfLengthZ, newBounds->get(eHalfLengthZ) + zEnv[0]}, + {eMinR, std::max(0.0, newBounds->get(eMinR) - rEnv[0])}, + {eMaxR, newBounds->get(eMaxR) + rEnv[1]}, + }); + + worldBounds = std::move(newBounds); + + } else if (const auto *box = + dynamic_cast(&bounds); + box != nullptr) { + throw std::logic_error{"Not implemented"}; + } else { + throw std::logic_error{"Unsupported volume bounds type"}; + } + + ACTS_DEBUG(prefix() << "New root volume bounds are: " << *worldBounds); + + auto world = std::make_unique( + topVolume.transform(), std::move(worldBounds), "World"); + + // @TODO: This needs to become configurable + world->setNavigationPolicy( + options.defaultNavigationPolicyFactory->build(gctx, *world, logger)); + + // Need one-sided portal shell that connects outwards to nullptr + SingleCylinderPortalShell worldShell{*world}; + worldShell.applyToVolume(); + + auto &shell = child.connect(options, gctx, logger); + + shell.fill(*world); + + child.finalize(options, gctx, *world, logger); + + std::set> names; + + world->visitVolumes([&names, &logger, this](const auto *volume) { + if (names.contains(volume->volumeName())) { + ACTS_ERROR(prefix() << "Duplicate volume name: " << volume->volumeName()); + throw std::logic_error("Duplicate volume name"); + } + names.insert(volume->volumeName()); + }); + + return std::make_unique( + std::move(world), nullptr, m_cfg.geometryIdentifierHook, logger); +} + +} // namespace Acts diff --git a/Core/src/Geometry/BlueprintNode.cpp b/Core/src/Geometry/BlueprintNode.cpp new file mode 100644 index 00000000000..13c03495494 --- /dev/null +++ b/Core/src/Geometry/BlueprintNode.cpp @@ -0,0 +1,173 @@ +// 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 "Acts/Geometry/BlueprintNode.hpp" + +#include "Acts/Geometry/Blueprint.hpp" +#include "Acts/Geometry/CylinderContainerBlueprintNode.hpp" +#include "Acts/Geometry/LayerBlueprintNode.hpp" +#include "Acts/Geometry/MaterialDesignatorBlueprintNode.hpp" +#include "Acts/Geometry/StaticBlueprintNode.hpp" +#include "Acts/Navigation/INavigationPolicy.hpp" +#include "Acts/Navigation/TryAllNavigationPolicy.hpp" + +#include +#include + +namespace Acts { + +namespace { +bool hasDescendent(const BlueprintNode& descendent, + const BlueprintNode& ancestor) { + if (&descendent == &ancestor) { + return true; + } + + return std::ranges::any_of(ancestor.children(), + [&](const auto& child) -> bool { + return hasDescendent(descendent, child); + }); +} +} // namespace + +void BlueprintNode::toStream(std::ostream& os) const { + os << "BlueprintNode(" << name() << ")"; +} + +BlueprintNode& BlueprintNode::addChild(std::shared_ptr child) { + if (!child) { + throw std::invalid_argument("Child is nullptr"); + } + + if (dynamic_cast(child.get()) != nullptr) { + throw std::invalid_argument("Cannot add a Blueprint as a child"); + } + + if (child->depth() != 0) { + throw std::invalid_argument("Child has already been added to another node"); + } + + if (hasDescendent(*this, *child)) { + throw std::invalid_argument("Adding child would create a cycle"); + } + + child->setDepth(m_depth + 1); + m_children.push_back(std::move(child)); + return *this; +} + +BlueprintNode::MutableChildRange BlueprintNode::children() { + return MutableChildRange{m_children}; +} + +BlueprintNode::ChildRange BlueprintNode::children() const { + return ChildRange{m_children}; +} + +std::size_t BlueprintNode::depth() const { + return m_depth; +} + +void BlueprintNode::setDepth(std::size_t depth) { + m_depth = depth; + for (auto& child : children()) { + child.setDepth(depth + 1); + } +} + +std::string BlueprintNode::indent() const { + return std::string(m_depth * 2, ' '); +} + +std::string BlueprintNode::prefix() const { + return indent() + "[" + name() + "]: "; +} + +StaticBlueprintNode& BlueprintNode::addStaticVolume( + std::unique_ptr volume, + const std::function& callback) { + if (!volume) { + throw std::invalid_argument("Volume is nullptr"); + } + + auto child = std::make_shared(std::move(volume)); + addChild(child); + + if (callback) { + callback(*child); + } + return *child; +} + +StaticBlueprintNode& BlueprintNode::addStaticVolume( + const Transform3& transform, std::shared_ptr volumeBounds, + const std::string& volumeName, + const std::function& callback) { + return addStaticVolume(std::make_unique( + transform, std::move(volumeBounds), volumeName), + callback); +} + +CylinderContainerBlueprintNode& BlueprintNode::addCylinderContainer( + const std::string& name, BinningValue direction, + const std::function& + callback) { + auto cylinder = + std::make_shared(name, direction); + addChild(cylinder); + if (callback) { + callback(*cylinder); + } + return *cylinder; +} + +MaterialDesignatorBlueprintNode& BlueprintNode::addMaterial( + const std::string& name, + const std::function& + callback) { + auto material = std::make_shared(name); + addChild(material); + if (callback) { + callback(*material); + } + return *material; +} + +LayerBlueprintNode& BlueprintNode::addLayer( + const std::string& name, + const std::function& callback) { + auto layer = std::make_shared(name); + addChild(layer); + if (callback) { + callback(*layer); + } + return *layer; +} + +void BlueprintNode::clearChildren() { + for (auto& child : children()) { + child.setDepth(0); + } + m_children.clear(); +} + +void BlueprintNode::graphviz(std::ostream& os) const { + os << "digraph BlueprintNode {" << std::endl; + addToGraphviz(os); + os << "}" << std::endl; +} + +void BlueprintNode::addToGraphviz(std::ostream& os) const { + for (const auto& child : children()) { + os << indent() << "\"" << name() << "\" -> \"" << child.name() << "\";" + << std::endl; + child.addToGraphviz(os); + } +} + +} // namespace Acts diff --git a/Core/src/Geometry/BlueprintOptions.cpp b/Core/src/Geometry/BlueprintOptions.cpp new file mode 100644 index 00000000000..07913665861 --- /dev/null +++ b/Core/src/Geometry/BlueprintOptions.cpp @@ -0,0 +1,29 @@ +// 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 "Acts/Geometry/BlueprintOptions.hpp" + +#include "Acts/Geometry/NavigationPolicyFactory.hpp" +#include "Acts/Navigation/TryAllNavigationPolicy.hpp" + +namespace Acts { + +void BlueprintOptions::validate() const { + if (!defaultNavigationPolicyFactory) { + throw std::invalid_argument("Navigation policy factory is nullptr"); + } +} + +std::unique_ptr +BlueprintOptions::makeDefaultNavigationPolicyFactory() { + return NavigationPolicyFactory::make() + .add() + .asUniquePtr(); +} + +} // namespace Acts diff --git a/Core/src/Geometry/CMakeLists.txt b/Core/src/Geometry/CMakeLists.txt index 74f6e0e126d..189c4396112 100644 --- a/Core/src/Geometry/CMakeLists.txt +++ b/Core/src/Geometry/CMakeLists.txt @@ -43,4 +43,11 @@ target_sources( PortalLinkBase.cpp PortalError.cpp PortalShell.cpp + BlueprintNode.cpp + Blueprint.cpp + BlueprintOptions.cpp + CylinderContainerBlueprintNode.cpp + StaticBlueprintNode.cpp + LayerBlueprintNode.cpp + MaterialDesignatorBlueprintNode.cpp ) diff --git a/Core/src/Geometry/CylinderContainerBlueprintNode.cpp b/Core/src/Geometry/CylinderContainerBlueprintNode.cpp new file mode 100644 index 00000000000..284b30949ac --- /dev/null +++ b/Core/src/Geometry/CylinderContainerBlueprintNode.cpp @@ -0,0 +1,257 @@ +// 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 "Acts/Geometry/CylinderContainerBlueprintNode.hpp" + +#include "Acts/Geometry/BlueprintNode.hpp" +#include "Acts/Geometry/CylinderVolumeStack.hpp" +#include "Acts/Geometry/GeometryContext.hpp" +#include "Acts/Geometry/PortalShell.hpp" +#include "Acts/Geometry/TrackingVolume.hpp" +#include "Acts/Navigation/INavigationPolicy.hpp" +#include "Acts/Utilities/GraphViz.hpp" +#include "Acts/Visualization/GeometryView3D.hpp" + +#include +#include + +namespace Acts { + +CylinderContainerBlueprintNode::CylinderContainerBlueprintNode( + const std::string& name, BinningValue direction, + CylinderVolumeStack::AttachmentStrategy attachmentStrategy, + CylinderVolumeStack::ResizeStrategy resizeStrategy) + : m_name(name), + m_direction(direction), + m_attachmentStrategy(attachmentStrategy), + m_resizeStrategy(resizeStrategy) {} + +const std::string& CylinderContainerBlueprintNode::name() const { + return m_name; +} + +Volume& CylinderContainerBlueprintNode::build(const BlueprintOptions& options, + const GeometryContext& gctx, + const Logger& logger) { + ACTS_DEBUG(prefix() << "cylinder container build (dir=" << m_direction + << ")"); + + if (m_stack != nullptr) { + ACTS_ERROR(prefix() << "Volume is already built"); + throw std::runtime_error("Volume is already built"); + } + + for (auto& child : children()) { + Volume& volume = child.build(options, gctx, logger); + m_childVolumes.push_back(&volume); + // We need to remember which volume we got from which child, so we can + // assemble a correct portal shell later + m_volumeToNode[&volume] = &child; + } + ACTS_VERBOSE(prefix() << "-> Collected " << m_childVolumes.size() + << " child volumes"); + + ACTS_VERBOSE(prefix() << "-> Building the stack"); + m_stack = std::make_unique(m_childVolumes, m_direction, + m_attachmentStrategy, + m_resizeStrategy, logger); + ACTS_DEBUG(prefix() << "-> Stack bounds are: " << m_stack->volumeBounds()); + + ACTS_DEBUG(prefix() << " *** build complete ***"); + + return *m_stack; +} + +std::vector +CylinderContainerBlueprintNode::collectChildShells( + const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger) { + std::vector shells; + ACTS_DEBUG(prefix() << "Have " << m_childVolumes.size() << " child volumes"); + for (Volume* volume : m_childVolumes) { + if (isGapVolume(*volume)) { + // We need to create a TrackingVolume from the gap and put it in the shell + auto gap = std::make_unique(*volume); + gap->setVolumeName(name() + "::Gap" + std::to_string(m_gaps.size() + 1)); + ACTS_DEBUG(prefix() << " ~> Gap volume (" << gap->volumeName() + << "): " << gap->volumeBounds()); + auto shell = std::make_unique(*gap); + assert(shell->isValid()); + shells.push_back(shell.get()); + + m_gaps.emplace_back(std::move(shell), std::move(gap)); + + } else { + // Figure out which child we got this volume from + auto it = m_volumeToNode.find(volume); + if (it == m_volumeToNode.end()) { + throw std::runtime_error("Volume not found in child volumes"); + } + BlueprintNode& child = *it->second; + + ACTS_DEBUG(prefix() << " ~> Child (" << child.name() + << ") volume: " << volume->volumeBounds()); + + auto* shell = dynamic_cast( + &child.connect(options, gctx, logger)); + if (shell == nullptr) { + ACTS_ERROR(prefix() + << "Child volume of cylinder stack is not a cylinder"); + throw std::runtime_error( + "Child volume of cylinder stack is not a cylinder"); + } + assert(shell->isValid()); + + shells.push_back(shell); + } + } + return shells; +} + +CylinderStackPortalShell& CylinderContainerBlueprintNode::connect( + const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger) { + ACTS_DEBUG(prefix() << "Cylinder container connect"); + if (m_stack == nullptr) { + ACTS_ERROR(prefix() << "Volume is not built"); + throw std::runtime_error("Volume is not built"); + } + + ACTS_DEBUG(prefix() << "Collecting child shells from " << children().size() + << " children"); + + // We have child volumes and gaps as bare Volumes in `m_childVolumes` after + // `build()` has completed. For the stack shell, we need TrackingVolumes in + // the right order. + + std::vector shells = + collectChildShells(options, gctx, logger); + + // Sanity checks + throw_assert(shells.size() == m_childVolumes.size(), + "Number of shells does not match number of child volumes"); + + throw_assert(std::ranges::none_of( + shells, [](const auto* shell) { return shell == nullptr; }), + "Invalid shell pointer"); + + throw_assert(std::ranges::all_of( + shells, [](const auto* shell) { return shell->isValid(); }), + "Invalid shell"); + + ACTS_DEBUG(prefix() << "Producing merged cylinder stack shell in " + << m_direction << " direction from " << shells.size() + << " shells"); + m_shell = std::make_unique(gctx, std::move(shells), + m_direction, logger); + + assert(m_shell != nullptr && "No shell was built at the end of connect"); + assert(m_shell->isValid() && "Shell is not valid at the end of connect"); + return *m_shell; +} + +void CylinderContainerBlueprintNode::finalize(const BlueprintOptions& options, + const GeometryContext& gctx, + TrackingVolume& parent, + const Logger& logger) { + ACTS_DEBUG(prefix() << "Finalizing cylinder container"); + + if (m_stack == nullptr) { + ACTS_ERROR(prefix() << "Volume is not built"); + throw std::runtime_error("Volume is not built"); + } + + if (m_shell == nullptr) { + ACTS_ERROR(prefix() << "Volume is not connected"); + throw std::runtime_error("Volume is not connected"); + } + + const auto* policyFactory = options.defaultNavigationPolicyFactory.get(); + + ACTS_DEBUG(prefix() << "Registering " << m_gaps.size() + << " gap volumes with parent"); + for (auto& [shell, gap] : m_gaps) { + auto* gapPtr = gap.get(); + parent.addVolume(std::move(gap)); + shell->applyToVolume(); + auto policy = policyFactory->build(gctx, *gapPtr, logger); + gapPtr->setNavigationPolicy(std::move(policy)); + } + + ACTS_DEBUG(prefix() << "Finalizing " << children().size() << " children"); + + for (auto& child : children()) { + child.finalize(options, gctx, parent, logger); + } +} + +bool CylinderContainerBlueprintNode::isGapVolume(const Volume& volume) const { + assert(m_stack != nullptr); + return std::ranges::any_of( + m_stack->gaps(), [&](const auto& gap) { return gap.get() == &volume; }); +} + +CylinderContainerBlueprintNode& CylinderContainerBlueprintNode::setDirection( + BinningValue direction) { + if (m_stack != nullptr) { + throw std::runtime_error("Cannot change direction after build"); + } + m_direction = direction; + return *this; +} + +CylinderContainerBlueprintNode& +CylinderContainerBlueprintNode::setAttachmentStrategy( + CylinderVolumeStack::AttachmentStrategy attachmentStrategy) { + if (m_stack != nullptr) { + throw std::runtime_error("Cannot change direction after build"); + } + m_attachmentStrategy = attachmentStrategy; + return *this; +} + +CylinderContainerBlueprintNode& +CylinderContainerBlueprintNode::setResizeStrategy( + CylinderVolumeStack::ResizeStrategy resizeStrategy) { + if (m_stack != nullptr) { + throw std::runtime_error("Cannot change direction after build"); + } + m_resizeStrategy = resizeStrategy; + return *this; +} + +void CylinderContainerBlueprintNode::addToGraphviz(std::ostream& os) const { + std::stringstream ss; + ss << "" + name() + ""; + ss << "
CylinderContainer"; + ss << "
dir: " << m_direction; + GraphViz::Node node{ + .id = name(), .label = ss.str(), .shape = GraphViz::Shape::DoubleOctagon}; + os << node << std::endl; + for (const auto& child : children()) { + os << indent() << GraphViz::Edge{{.id = name()}, {.id = child.name()}} + << std::endl; + child.addToGraphviz(os); + } +} + +BinningValue CylinderContainerBlueprintNode::direction() const { + return m_direction; +} + +CylinderVolumeStack::AttachmentStrategy +CylinderContainerBlueprintNode::attachmentStrategy() const { + return m_attachmentStrategy; +} + +CylinderVolumeStack::ResizeStrategy +CylinderContainerBlueprintNode::resizeStrategy() const { + return m_resizeStrategy; +} + +} // namespace Acts diff --git a/Core/src/Geometry/LayerBlueprintNode.cpp b/Core/src/Geometry/LayerBlueprintNode.cpp new file mode 100644 index 00000000000..de2e2cdb8cd --- /dev/null +++ b/Core/src/Geometry/LayerBlueprintNode.cpp @@ -0,0 +1,149 @@ +// 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 "Acts/Geometry/LayerBlueprintNode.hpp" + +#include "Acts/Definitions/Algebra.hpp" +#include "Acts/Geometry/CuboidVolumeBounds.hpp" +#include "Acts/Geometry/CylinderVolumeBounds.hpp" +#include "Acts/Geometry/ProtoLayer.hpp" +#include "Acts/Geometry/VolumeBounds.hpp" +#include "Acts/Utilities/GraphViz.hpp" + +namespace Acts { + +Volume& LayerBlueprintNode::build(const BlueprintOptions& options, + const GeometryContext& gctx, + const Logger& logger) { + if (m_surfaces.empty()) { + ACTS_ERROR("LayerBlueprintNode: no surfaces provided"); + throw std::invalid_argument("LayerBlueprintNode: no surfaces provided"); + } + + ACTS_DEBUG(prefix() << "Building Layer " << name() << " from " + << m_surfaces.size() << " surfaces"); + ACTS_VERBOSE(prefix() << " -> layer type: " << m_layerType); + ACTS_VERBOSE(prefix() << " -> transform:\n" << m_transform.matrix()); + + Extent extent; + + ProtoLayer protoLayer{gctx, m_surfaces, m_transform.inverse()}; + ACTS_VERBOSE(prefix() << "Built proto layer: " << protoLayer); + + extent.addConstrain(protoLayer.extent, m_envelope); + + ACTS_VERBOSE(prefix() << " -> layer extent: " << extent); + + buildVolume(extent, logger); + assert(m_volume != nullptr && "Volume not built from proto layer"); + + for (auto& surface : m_surfaces) { + m_volume->addSurface(surface); + } + + return StaticBlueprintNode::build(options, gctx, logger); +} + +void LayerBlueprintNode::buildVolume(const Extent& extent, + const Logger& logger) { + ACTS_VERBOSE(prefix() << "Building volume for layer " << name()); + using enum BinningValue; + using enum LayerType; + + std::shared_ptr bounds; + switch (m_layerType) { + case Cylinder: + case Disc: { + double minR = extent.min(binR); + double maxR = extent.max(binR); + double hlZ = extent.interval(binZ) / 2.0; + bounds = std::make_shared(minR, maxR, hlZ); + break; + } + case Plane: { + double hlX = extent.interval(binX) / 2.0; + double hlY = extent.interval(binY) / 2.0; + double hlZ = extent.interval(binZ) / 2.0; + bounds = std::make_shared(hlX, hlY, hlZ); + break; + } + } + + assert(bounds != nullptr); + + ACTS_VERBOSE(prefix() << " -> bounds: " << *bounds); + + Transform3 transform = m_transform; + transform.translation() = + Vector3{extent.medium(binX), extent.medium(binY), extent.medium(binZ)}; + + ACTS_VERBOSE(prefix() << " -> adjusted transform:\n" << transform.matrix()); + + m_volume = + std::make_unique(transform, std::move(bounds), m_name); +} + +const std::string& LayerBlueprintNode::name() const { + return m_name; +} + +LayerBlueprintNode& LayerBlueprintNode::setSurfaces( + std::vector> surfaces) { + m_surfaces = std::move(surfaces); + return *this; +} + +const std::vector>& LayerBlueprintNode::surfaces() + const { + return m_surfaces; +} + +LayerBlueprintNode& LayerBlueprintNode::setTransform( + const Transform3& transform) { + m_transform = transform; + return *this; +} + +const Transform3& LayerBlueprintNode::transform() const { + return m_transform; +} + +LayerBlueprintNode& LayerBlueprintNode::setEnvelope( + const ExtentEnvelope& envelope) { + m_envelope = envelope; + return *this; +} + +const ExtentEnvelope& LayerBlueprintNode::envelope() const { + return m_envelope; +} + +LayerBlueprintNode& LayerBlueprintNode::setLayerType(LayerType layerType) { + m_layerType = layerType; + return *this; +} + +const LayerBlueprintNode::LayerType& LayerBlueprintNode::layerType() const { + return m_layerType; +} + +void LayerBlueprintNode::addToGraphviz(std::ostream& os) const { + std::stringstream ss; + ss << "" << name() << ""; + ss << "
"; + ss << m_layerType; + + GraphViz::Node node{ + .id = name(), .label = ss.str(), .shape = GraphViz::Shape::Diamond}; + + os << node; + + BlueprintNode::addToGraphviz(os); +} + +} // namespace Acts diff --git a/Core/src/Geometry/MaterialDesignatorBlueprintNode.cpp b/Core/src/Geometry/MaterialDesignatorBlueprintNode.cpp new file mode 100644 index 00000000000..0e89b65d6e3 --- /dev/null +++ b/Core/src/Geometry/MaterialDesignatorBlueprintNode.cpp @@ -0,0 +1,191 @@ +// 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 "Acts/Geometry/MaterialDesignatorBlueprintNode.hpp" + +#include "Acts/Detector/ProtoBinning.hpp" +#include "Acts/Geometry/CylinderVolumeBounds.hpp" +#include "Acts/Geometry/Portal.hpp" +#include "Acts/Material/ProtoSurfaceMaterial.hpp" +#include "Acts/Surfaces/Surface.hpp" +#include "Acts/Utilities/GraphViz.hpp" +#include "Acts/Utilities/Helpers.hpp" + +namespace Acts { + +const std::string& MaterialDesignatorBlueprintNode::name() const { + return m_name; +} + +void MaterialDesignatorBlueprintNode::toStream(std::ostream& os) const { + os << "MaterialDesignatorBlueprintNode(" << name() << ")"; +} + +Volume& MaterialDesignatorBlueprintNode::build(const BlueprintOptions& options, + const GeometryContext& gctx, + const Logger& logger) { + if (children().size() != 1) { + ACTS_ERROR(prefix() << "MaterialDesignatorBlueprintNode must have exactly " + "one child, but has " + << children().size()); + throw std::runtime_error( + "MaterialDesignatorBlueprintNode must have exactly one child"); + } + + if (!m_binning) { + ACTS_ERROR(prefix() << "Binning is not set"); + throw std::runtime_error("Binning is not set"); + } + + return children().at(0).build(options, gctx, logger); +} + +void MaterialDesignatorBlueprintNode::handleCylinderBinning( + CylinderPortalShell& cylShell, + const std::vector< + std::tuple>& binning, + const Logger& logger) { + ACTS_DEBUG(prefix() << "Binning is set to compatible type"); + using enum CylinderVolumeBounds::Face; + + for (auto& [face, loc0, loc1] : binning) { + if (face == OuterCylinder || face == InnerCylinder) { + if (loc0.binValue != BinningValue::binRPhi) { + ACTS_ERROR(prefix() << "Binning is not in RPhi"); + throw std::runtime_error("Binning is not in RPhi"); + } + + if (loc1.binValue != BinningValue::binZ) { + ACTS_ERROR(prefix() << "Binning is not in Z"); + throw std::runtime_error("Binning is not in Z"); + } + } + + if (face == PositiveDisc || face == NegativeDisc) { + if (loc0.binValue != BinningValue::binR) { + ACTS_ERROR(prefix() << "Binning is not in R"); + throw std::runtime_error("Binning is not in R"); + } + if (loc1.binValue != BinningValue::binPhi) { + ACTS_ERROR(prefix() << "Binning is not in Phi"); + throw std::runtime_error("Binning is not in Phi"); + } + } + + Experimental::BinningDescription desc{.binning = {loc0, loc1}}; + ACTS_DEBUG(prefix() << "~> Assigning proto binning " << desc.toString() + << " to face " << face); + + auto material = std::make_shared(std::move(desc)); + + auto portal = cylShell.portal(face); + if (portal == nullptr) { + ACTS_ERROR(prefix() << "Portal is nullptr"); + throw std::runtime_error("Portal is nullptr"); + } + portal->surface().assignSurfaceMaterial(std::move(material)); + } +} + +PortalShellBase& MaterialDesignatorBlueprintNode::connect( + const BlueprintOptions& options, const GeometryContext& gctx, + const Logger& logger) { + ACTS_DEBUG(prefix() << "MaterialDesignatorBlueprintNode::connect"); + if (children().size() != 1) { + ACTS_ERROR(prefix() << "MaterialDesignatorBlueprintNode must have exactly " + "one child, but has " + << children().size()); + throw std::runtime_error( + "MaterialDesignatorBlueprintNode must have exactly one child"); + } + if (!m_binning) { + ACTS_ERROR(prefix() << "Binning is not set"); + throw std::runtime_error("Binning is not set"); + } + + auto& shell = children().at(0).connect(options, gctx, logger); + + ACTS_DEBUG(prefix() << "Received shell from child " + << children().at(0).name()); + + if (auto* cylShell = dynamic_cast(&shell)) { + ACTS_DEBUG(prefix() << "Connecting cylinder shell"); + + if (const auto* binning = std::get_if>>(&m_binning.value()); + binning != nullptr) { + handleCylinderBinning(*cylShell, *binning, logger); + } else { + ACTS_ERROR(prefix() << "Binning is set to unknown type"); + throw std::runtime_error("Unknown binning type"); + } + + } + // @TODO: Handle cuboid volume shell here + else { + ACTS_ERROR(prefix() << "Shell is not supported"); + throw std::runtime_error("Shell is not supported"); + } + + return shell; +} + +void MaterialDesignatorBlueprintNode::finalize(const BlueprintOptions& options, + const GeometryContext& gctx, + TrackingVolume& parent, + const Logger& logger) { + if (children().size() != 1) { + throw std::runtime_error( + "MaterialDesignatorBlueprintNode must have exactly one child"); + } + return children().at(0).finalize(options, gctx, parent, logger); +} + +void MaterialDesignatorBlueprintNode::addToGraphviz(std::ostream& os) const { + if (!m_binning) { + throw std::runtime_error("Binning is not set"); + } + + std::stringstream ss; + ss << "" + name() + ""; + ss << "
CylinderContainer"; + + std::visit( + overloaded{ + [&](const std::vector< + std::tuple>& binning) { + for (const auto& [face, loc0, loc1] : binning) { + ss << "
" << face; + ss << ": " << loc0.binValue << "=" << loc0.bins(); + ss << ", " << loc1.binValue << "=" << loc1.bins(); + } + }, + [](const auto& /*binning*/) { + // No output in all other cases + }}, + m_binning.value()); + os << GraphViz::Node{ + .id = name(), .label = ss.str(), .shape = GraphViz::Shape::Hexagon}; + BlueprintNode::addToGraphviz(os); +} + +const std::optional& +MaterialDesignatorBlueprintNode::binning() const { + return m_binning; +} + +MaterialDesignatorBlueprintNode& MaterialDesignatorBlueprintNode::setBinning( + BinningConfig binning) { + m_binning = std::move(binning); + return *this; +} + +} // namespace Acts diff --git a/Core/src/Geometry/PortalLinkBase.cpp b/Core/src/Geometry/PortalLinkBase.cpp index bdc65f8a552..f558fe19238 100644 --- a/Core/src/Geometry/PortalLinkBase.cpp +++ b/Core/src/Geometry/PortalLinkBase.cpp @@ -109,7 +109,7 @@ std::unique_ptr PortalLinkBase::merge( } else if (const auto* bTrivial = dynamic_cast(b.get()); bTrivial != nullptr) { - ACTS_VERBOSE( + ACTS_WARNING( "Merging a grid portal with a trivial portal (via composite)"); return std::make_unique(std::move(a), std::move(b), direction); @@ -128,7 +128,7 @@ std::unique_ptr PortalLinkBase::merge( aTrivial != nullptr) { if (const auto* bGrid = dynamic_cast(b.get()); bGrid) { - ACTS_VERBOSE( + ACTS_WARNING( "Merging a trivial portal with a grid portal (via composite)"); return std::make_unique(std::move(a), std::move(b), direction); @@ -141,7 +141,7 @@ std::unique_ptr PortalLinkBase::merge( direction); } else if (dynamic_cast(b.get()) != nullptr) { - ACTS_WARNING("Merging a trivial portal with a composite portal"); + ACTS_VERBOSE("Merging a trivial portal with a composite portal"); return std::make_unique(std::move(a), std::move(b), direction); @@ -156,12 +156,12 @@ std::unique_ptr PortalLinkBase::merge( direction); } else if (dynamic_cast(b.get()) != nullptr) { - ACTS_WARNING("Merging a composite portal with a trivial portal"); + ACTS_VERBOSE("Merging a composite portal with a trivial portal"); return std::make_unique(std::move(a), std::move(b), direction); } else if (dynamic_cast(b.get()) != nullptr) { - ACTS_WARNING("Merging two composite portals"); + ACTS_VERBOSE("Merging two composite portals"); return std::make_unique(std::move(a), std::move(b), direction); diff --git a/Core/src/Geometry/StaticBlueprintNode.cpp b/Core/src/Geometry/StaticBlueprintNode.cpp new file mode 100644 index 00000000000..12defdcc2eb --- /dev/null +++ b/Core/src/Geometry/StaticBlueprintNode.cpp @@ -0,0 +1,170 @@ +// 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 "Acts/Geometry/StaticBlueprintNode.hpp" + +#include "Acts/Geometry/GeometryContext.hpp" +#include "Acts/Geometry/PortalShell.hpp" +#include "Acts/Geometry/VolumeBounds.hpp" +#include "Acts/Navigation/INavigationPolicy.hpp" +#include "Acts/Utilities/GraphViz.hpp" +#include "Acts/Visualization/GeometryView3D.hpp" + +namespace Acts { + +StaticBlueprintNode::StaticBlueprintNode(std::unique_ptr volume) + : m_volume(std::move(volume)) {} + +Volume& StaticBlueprintNode::build(const BlueprintOptions& options, + const GeometryContext& gctx, + const Logger& logger) { + ACTS_DEBUG(prefix() << "static build"); + if (!m_volume) { + throw std::runtime_error("Volume is not built"); + } + + ACTS_DEBUG(prefix() << "Building volume (" << name() << ") with " + << children().size() << " children"); + for (auto& child : children()) { + child.build(options, gctx, logger); + } + + ACTS_DEBUG(prefix() << "-> returning volume " << *m_volume); + return *m_volume; +} + +PortalShellBase& StaticBlueprintNode::connect(const BlueprintOptions& options, + const GeometryContext& gctx, + const Logger& logger) { + ACTS_DEBUG(prefix() << "Static connect"); + if (m_volume == nullptr) { + throw std::runtime_error("Volume is not present"); + } + + ACTS_DEBUG(prefix() << "Connecting parent volume (" << name() << ") with " + << children().size() << " children"); + + for (auto& child : children()) { + auto& shell = child.connect(options, gctx, logger); + // Register ourselves on the outside of the shell + shell.fill(*m_volume); + } + + VolumeBounds::BoundsType type = m_volume->volumeBounds().type(); + if (type == VolumeBounds::eCylinder) { + m_shell = std::make_unique(*m_volume); + + } else if (type == VolumeBounds::eCuboid) { + throw std::logic_error("Cuboid is not implemented yet"); + + } else { + throw std::logic_error("Volume type is not supported"); + } + + assert(m_shell != nullptr && + "No shell was built at the end of StaticBlueprintNode::connect"); + assert(m_shell->isValid() && + "Shell is not valid at the end of StaticBlueprintNode::connect"); + return *m_shell; +} + +void StaticBlueprintNode::finalize(const BlueprintOptions& options, + const GeometryContext& gctx, + TrackingVolume& parent, + const Logger& logger) { + ACTS_DEBUG(prefix() << "Finalizing static volume"); + + if (!m_volume) { + ACTS_ERROR(prefix() << "Volume is not built"); + throw std::runtime_error("Volume is not built"); + } + + if (!m_shell) { + ACTS_ERROR(prefix() << "Shell is not built"); + throw std::runtime_error("Shell is not built"); + } + + for (auto& child : children()) { + child.finalize(options, gctx, *m_volume, logger); + } + + ACTS_DEBUG(prefix() << "Registering " << m_shell->size() + << " portals into volume " << m_volume->volumeName()); + m_shell->applyToVolume(); + + ACTS_DEBUG(prefix() << " Adding volume (" << m_volume->volumeName() + << ") to parent volume (" << parent.volumeName() << ")"); + + const auto* policyFactory = options.defaultNavigationPolicyFactory.get(); + + if (m_navigationPolicyFactory) { + policyFactory = m_navigationPolicyFactory.get(); + } + m_volume->setNavigationPolicy(policyFactory->build(gctx, *m_volume, logger)); + + parent.addVolume(std::move(m_volume)); +} + +const std::string& StaticBlueprintNode::name() const { + static const std::string uninitialized = "uninitialized"; + if (m_volume == nullptr) { + return uninitialized; + } + return m_volume->volumeName(); +} + +StaticBlueprintNode& StaticBlueprintNode::setNavigationPolicyFactory( + std::shared_ptr navigationPolicyFactory) { + m_navigationPolicyFactory = std::move(navigationPolicyFactory); + return *this; +} + +const NavigationPolicyFactory* StaticBlueprintNode::navigationPolicyFactory() + const { + return m_navigationPolicyFactory.get(); +} + +void StaticBlueprintNode::addToGraphviz(std::ostream& os) const { + std::stringstream ss; + ss << "" << name() << ""; + ss << "
"; + if (m_volume == nullptr) { + throw std::runtime_error("Volume is not built"); + } + switch (m_volume->volumeBounds().type()) { + case VolumeBounds::eCylinder: + ss << "Cylinder"; + break; + case VolumeBounds::eCuboid: + ss << "Cuboid"; + break; + case VolumeBounds::eCone: + ss << "Cone"; + break; + case VolumeBounds::eCutoutCylinder: + ss << "CutoutCylinder"; + break; + case VolumeBounds::eGenericCuboid: + ss << "GenericCuboid"; + break; + case VolumeBounds::eTrapezoid: + ss << "Trapezoid"; + break; + default: + ss << "Other"; + } + + GraphViz::Node node{ + .id = name(), .label = ss.str(), .shape = GraphViz::Shape::Rectangle}; + + os << node; + + BlueprintNode::addToGraphviz(os); +} + +} // namespace Acts diff --git a/Examples/Python/CMakeLists.txt b/Examples/Python/CMakeLists.txt index b7e7a5b4112..acca27e91f4 100644 --- a/Examples/Python/CMakeLists.txt +++ b/Examples/Python/CMakeLists.txt @@ -16,6 +16,7 @@ pybind11_add_module(ActsPythonBindings src/Material.cpp src/Geometry.cpp src/GeometryBuildingGen1.cpp + src/Blueprint.cpp src/ExampleAlgorithms.cpp src/MagneticField.cpp src/Output.cpp diff --git a/Examples/Python/src/Blueprint.cpp b/Examples/Python/src/Blueprint.cpp new file mode 100644 index 00000000000..f5ce4ddc3d0 --- /dev/null +++ b/Examples/Python/src/Blueprint.cpp @@ -0,0 +1,432 @@ +// 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 "Acts/Geometry/Blueprint.hpp" + +#include "Acts/Definitions/Units.hpp" +#include "Acts/Geometry/BlueprintNode.hpp" +#include "Acts/Geometry/CylinderContainerBlueprintNode.hpp" +#include "Acts/Geometry/CylinderVolumeStack.hpp" +#include "Acts/Geometry/LayerBlueprintNode.hpp" +#include "Acts/Geometry/MaterialDesignatorBlueprintNode.hpp" +#include "Acts/Geometry/StaticBlueprintNode.hpp" +#include "Acts/Navigation/NavigationStream.hpp" +#include "Acts/Plugins/Python/Utilities.hpp" +#include "Acts/Utilities/BinningType.hpp" +#include "Acts/Utilities/Logger.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +namespace py = pybind11; +using namespace pybind11::literals; + +namespace Acts::Python { +namespace { +using std::uniform_real_distribution; + +// This is temporary! +void pseudoNavigation(const TrackingGeometry& trackingGeometry, + const GeometryContext& gctx, std::filesystem::path& path, + std::size_t runs, std::size_t substepsPerCm, + std::pair etaRange, + Logging::Level logLevel) { + using namespace Acts::UnitLiterals; + + ACTS_LOCAL_LOGGER(getDefaultLogger("pseudoNavigation", logLevel)); + + std::ofstream csv{path}; + csv << "x,y,z,volume,boundary,sensitive,material" << std::endl; + + std::mt19937 rnd{42}; + + std::uniform_real_distribution<> dist{-1, 1}; + std::uniform_real_distribution<> subStepDist{0.01, 0.99}; + + double thetaMin = 2 * std::atan(std::exp(-etaRange.first)); + double thetaMax = 2 * std::atan(std::exp(-etaRange.second)); + std::uniform_real_distribution<> thetaDist{thetaMin, thetaMax}; + + using namespace Acts::UnitLiterals; + + for (std::size_t run = 0; run < runs; run++) { + Vector3 position = Vector3::Zero(); + + double theta = thetaDist(rnd); + double phi = 2 * std::numbers::pi * dist(rnd); + + Vector3 direction; + direction[0] = std::sin(theta) * std::cos(phi); + direction[1] = std::sin(theta) * std::sin(phi); + direction[2] = std::cos(theta); + + ACTS_VERBOSE("start navigation " << run); + ACTS_VERBOSE("pos: " << position.transpose()); + ACTS_VERBOSE("dir: " << direction.transpose()); + ACTS_VERBOSE(direction.norm()); + + std::mt19937 rng{static_cast(run)}; + + const auto* volume = trackingGeometry.lowestTrackingVolume(gctx, position); + assert(volume != nullptr); + ACTS_VERBOSE(volume->volumeName()); + + NavigationStream main; + const TrackingVolume* currentVolume = volume; + + csv << run << "," << position[0] << "," << position[1] << "," + << position[2]; + csv << "," << volume->geometryId().volume(); + csv << "," << volume->geometryId().boundary(); + csv << "," << volume->geometryId().sensitive(); + csv << "," << 0; + csv << std::endl; + + ACTS_VERBOSE("start pseudo navigation"); + + auto writeIntersection = [&](const Vector3& pos, const Surface& surface) { + csv << run << "," << pos[0] << "," << pos[1] << "," << pos[2]; + csv << "," << surface.geometryId().volume(); + csv << "," << surface.geometryId().boundary(); + csv << "," << surface.geometryId().sensitive(); + csv << "," << (surface.surfaceMaterial() != nullptr ? 1 : 0); + csv << std::endl; + }; + + for (std::size_t i = 0; i < 100; i++) { + assert(currentVolume != nullptr); + main = NavigationStream{}; + + AppendOnlyNavigationStream navStream{main}; + currentVolume->initializeNavigationCandidates( + {.position = position, .direction = direction}, navStream, logger()); + + ACTS_VERBOSE(main.candidates().size() << " candidates"); + + for (const auto& candidate : main.candidates()) { + ACTS_VERBOSE(" -> " << candidate.surface().geometryId()); + ACTS_VERBOSE(" " << candidate.surface().toStream(gctx)); + } + + ACTS_VERBOSE("initializing candidates"); + main.initialize(gctx, {position, direction}, BoundaryTolerance::None()); + + ACTS_VERBOSE(main.candidates().size() << " candidates remaining"); + + for (const auto& candidate : main.candidates()) { + ACTS_VERBOSE(" -> " << candidate.surface().geometryId()); + ACTS_VERBOSE(" " << candidate.surface().toStream(gctx)); + } + + if (main.currentCandidate().surface().isOnSurface(gctx, position, + direction)) { + ACTS_VERBOSE( + "Already on surface at initialization, skipping candidate"); + + writeIntersection(position, main.currentCandidate().surface()); + + if (!main.switchToNextCandidate()) { + ACTS_WARNING("candidates exhausted unexpectedly"); + break; + } + } + + bool terminated = false; + while (main.remainingCandidates() > 0) { + const auto& candidate = main.currentCandidate(); + + ACTS_VERBOSE(candidate.portal); + ACTS_VERBOSE(candidate.intersection.position().transpose()); + + ACTS_VERBOSE("moving to position: " << position.transpose() << " (r=" + << VectorHelpers::perp(position) + << ")"); + + Vector3 delta = candidate.intersection.position() - position; + + std::size_t substeps = + std::max(1l, std::lround(delta.norm() / 10_cm * substepsPerCm)); + + for (std::size_t j = 0; j < substeps; j++) { + Vector3 subpos = position + subStepDist(rng) * delta; + csv << run << "," << subpos[0] << "," << subpos[1] << "," + << subpos[2]; + csv << "," << currentVolume->geometryId().volume(); + csv << ",0,0,0"; // zero boundary and sensitive ids + csv << std::endl; + } + + position = candidate.intersection.position(); + ACTS_VERBOSE(" -> " + << position.transpose() + << " (r=" << VectorHelpers::perp(position) << ")"); + + writeIntersection(position, candidate.surface()); + + if (candidate.portal != nullptr) { + ACTS_VERBOSE( + "On portal: " << candidate.portal->surface().toStream(gctx)); + currentVolume = + candidate.portal->resolveVolume(gctx, position, direction) + .value(); + + if (currentVolume == nullptr) { + ACTS_VERBOSE("switched to nullptr -> we're done"); + terminated = true; + } + break; + + } else { + ACTS_VERBOSE("Not on portal"); + } + + main.switchToNextCandidate(); + } + + if (terminated) { + ACTS_VERBOSE("Terminate pseudo navigation"); + break; + } + + ACTS_VERBOSE("switched to " << currentVolume->volumeName()); + + ACTS_VERBOSE("-----"); + } + } +} + +} // namespace + +void addBlueprint(Context& ctx) { + auto m = ctx.get("main"); + + auto blueprintNode = + py::class_>( + m, "BlueprintNode"); + + auto rootNode = + py::class_>( + m, "Blueprint"); + + rootNode + .def(py::init()) + // Return value needs to be shared pointer because python otherwise + // can't manage the lifetime + .def( + "construct", + [](Blueprint& self, const BlueprintOptions& options, + const GeometryContext& gctx, + Logging::Level level) -> std::shared_ptr { + return self.construct(options, gctx, + *getDefaultLogger("Blueprint", level)); + }, + py::arg("options"), py::arg("gctx"), + py::arg("level") = Logging::INFO); + + { + auto c = py::class_(rootNode, "Config").def(py::init()); + ACTS_PYTHON_STRUCT_BEGIN(c, Blueprint::Config); + ACTS_PYTHON_MEMBER(envelope); + ACTS_PYTHON_MEMBER(geometryIdentifierHook); + ACTS_PYTHON_STRUCT_END(); + } + + auto addContextManagerProtocol = [](class_& cls) { + using type = typename class_::type; + cls.def("__enter__", [](type& self) -> type& { return self; }) + .def("__exit__", [](type& /*self*/, const py::object& /*exc_type*/, + const py::object& /*exc_value*/, + const py::object& /*traceback*/) { + // No action needed on exit + }); + }; + + auto addNodeMethods = [&blueprintNode](const std::string& name, + auto&& callable, auto&&... args) { + blueprintNode.def(name.c_str(), callable, args...) + .def(("add" + name).c_str(), callable, args...); + }; + + blueprintNode + .def("__str__", + [](const BlueprintNode& self) { + std::stringstream ss; + ss << self; + return ss.str(); + }) + .def("addChild", &BlueprintNode::addChild) + .def_property_readonly("children", + py::overload_cast<>(&BlueprintNode::children)) + .def("clearChildren", &BlueprintNode::clearChildren) + .def_property_readonly("name", &BlueprintNode::name) + .def_property_readonly("depth", &BlueprintNode::depth) + .def("graphviz", [](BlueprintNode& self, const py::object& fh) { + std::stringstream ss; + self.graphviz(ss); + fh.attr("write")(ss.str()); + }); + + py::class_(m, "BlueprintOptions") + .def(py::init<>()) + .def_readwrite("defaultNavigationPolicyFactory", + &BlueprintOptions::defaultNavigationPolicyFactory); + + py::class_(blueprintNode, + "MutableChildRange") + .def( + "__iter__", + [](BlueprintNode::MutableChildRange& self) { + return py::make_iterator(self.begin(), self.end()); + }, + py::keep_alive<0, 1>()) + .def( + "__getitem__", + [](BlueprintNode::MutableChildRange& self, + int i) -> Acts::BlueprintNode& { + if (i < 0) { + i += self.size(); + } + return self.at(i); + }, + py::return_value_policy::reference_internal) + .def("__len__", [](const BlueprintNode::MutableChildRange& self) { + return self.size(); + }); + + auto staticNode = + py::class_>( + m, "StaticBlueprintNode") + .def(py::init([](const Transform3& transform, + const std::shared_ptr& bounds, + const std::string& name) { + return std::make_shared( + std::make_unique(transform, bounds, + name)); + }), + py::arg("transform"), py::arg("bounds"), + py::arg("name") = "undefined") + .def_property("navigationPolicyFactory", + &Acts::StaticBlueprintNode::navigationPolicyFactory, + &Acts::StaticBlueprintNode::setNavigationPolicyFactory); + + addContextManagerProtocol(staticNode); + + addNodeMethods( + "StaticVolume", + [](BlueprintNode& self, const Transform3& transform, + const std::shared_ptr& bounds, const std::string& name) { + auto node = std::make_shared( + std::make_unique(transform, bounds, name)); + self.addChild(node); + return node; + }, + py::arg("transform"), py::arg("bounds"), py::arg("name") = "undefined"); + + auto cylNode = + py::class_>( + m, "CylinderContainerBlueprintNode") + .def(py::init(), + py::arg("name"), py::arg("direction"), + py::arg("attachmentStrategy") = + CylinderVolumeStack::AttachmentStrategy::Gap, + py::arg("resizeStrategy") = + CylinderVolumeStack::ResizeStrategy::Gap) + .def_property( + "attachmentStrategy", + &Acts::CylinderContainerBlueprintNode::attachmentStrategy, + &Acts::CylinderContainerBlueprintNode::setAttachmentStrategy) + .def_property( + "resizeStrategy", + &Acts::CylinderContainerBlueprintNode::resizeStrategy, + &Acts::CylinderContainerBlueprintNode::setResizeStrategy) + .def_property("direction", + &Acts::CylinderContainerBlueprintNode::direction, + &Acts::CylinderContainerBlueprintNode::setDirection); + + addContextManagerProtocol(cylNode); + + addNodeMethods( + "CylinderContainer", + [](BlueprintNode& self, const std::string& name, BinningValue direction) { + auto cylinder = + std::make_shared(name, direction); + self.addChild(cylinder); + return cylinder; + }, + py::arg("name"), py::arg("direction")); + + auto matNode = + py::class_>( + m, "MaterialDesignatorBlueprintNode") + .def(py::init(), "name"_a) + .def_property("binning", &MaterialDesignatorBlueprintNode::binning, + &MaterialDesignatorBlueprintNode::setBinning); + + addContextManagerProtocol(matNode); + + addNodeMethods( + "Material", + [](BlueprintNode& self, const std::string& name) { + auto child = std::make_shared(name); + self.addChild(child); + return child; + }, + "name"_a); + + auto layerNode = + py::class_>( + m, "LayerBlueprintNode") + .def(py::init(), py::arg("name")) + .def_property_readonly("name", &Acts::LayerBlueprintNode::name) + .def_property("surfaces", &Acts::LayerBlueprintNode::surfaces, + &Acts::LayerBlueprintNode::setSurfaces) + .def_property("transform", &Acts::LayerBlueprintNode::transform, + &Acts::LayerBlueprintNode::setTransform) + .def_property("envelope", &Acts::LayerBlueprintNode::envelope, + &Acts::LayerBlueprintNode::setEnvelope) + .def_property("layerType", &Acts::LayerBlueprintNode::layerType, + &Acts::LayerBlueprintNode::setLayerType) + .def_property("navigationPolicyFactory", + &Acts::LayerBlueprintNode::navigationPolicyFactory, + &Acts::LayerBlueprintNode::setNavigationPolicyFactory); + + py::enum_(layerNode, "LayerType") + .value("Cylinder", Acts::LayerBlueprintNode::LayerType::Cylinder) + .value("Disc", Acts::LayerBlueprintNode::LayerType::Disc) + .value("Plane", Acts::LayerBlueprintNode::LayerType::Plane); + + addContextManagerProtocol(layerNode); + + addNodeMethods( + "Layer", + [](BlueprintNode& self, const std::string& name) { + auto child = std::make_shared(name); + self.addChild(child); + return child; + }, + py::arg("name")); + + // TEMPORARY + m.def("pseudoNavigation", &pseudoNavigation, "trackingGeometry"_a, "gctx"_a, + "path"_a, "runs"_a, "substepsPerCm"_a = 2, + "etaRange"_a = std::pair{-4.5, 4.5}, "logLevel"_a = Logging::INFO); +} + +} // namespace Acts::Python diff --git a/Examples/Python/src/Geometry.cpp b/Examples/Python/src/Geometry.cpp index 4c15e0dcf94..e9bbbfa350e 100644 --- a/Examples/Python/src/Geometry.cpp +++ b/Examples/Python/src/Geometry.cpp @@ -30,6 +30,7 @@ #include "Acts/Geometry/GeometryContext.hpp" #include "Acts/Geometry/GeometryHierarchyMap.hpp" #include "Acts/Geometry/GeometryIdentifier.hpp" +#include "Acts/Geometry/ProtoLayer.hpp" #include "Acts/Geometry/TrackingGeometry.hpp" #include "Acts/Geometry/Volume.hpp" #include "Acts/Geometry/VolumeBounds.hpp" @@ -91,6 +92,9 @@ struct IdentifierSurfacesCollector { } // namespace namespace Acts::Python { + +void addBlueprint(Context& ctx); + void addGeometry(Context& ctx) { auto m = ctx.get("main"); @@ -301,6 +305,8 @@ void addGeometry(Context& ctx) { .value("Gap", CylinderVolumeStack::ResizeStrategy::Gap) .value("Expand", CylinderVolumeStack::ResizeStrategy::Expand); } + + addBlueprint(ctx); } void addExperimentalGeometry(Context& ctx) { @@ -702,6 +708,15 @@ void addExperimentalGeometry(Context& ctx) { ACTS_PYTHON_DECLARE_ALGORITHM(ActsExamples::VolumeAssociationTest, mex, "VolumeAssociationTest", name, ntests, randomNumbers, randomRange, detector); + + py::class_(m, "ProtoLayer") + .def(py::init>&, + const Transform3&>(), + "gctx"_a, "surfaces"_a, "transform"_a = Transform3::Identity()) + .def("min", &ProtoLayer::min, "bval"_a, "addenv"_a = true) + .def("max", &ProtoLayer::max, "bval"_a, "addenv"_a = true) + .def_property_readonly("surfaces", &ProtoLayer::surfaces); } } // namespace Acts::Python diff --git a/Examples/Python/tests/test_blueprint.py b/Examples/Python/tests/test_blueprint.py new file mode 100644 index 00000000000..8471bf52105 --- /dev/null +++ b/Examples/Python/tests/test_blueprint.py @@ -0,0 +1,80 @@ +import pytest + +import acts + +mm = acts.UnitConstants.mm +m = acts.UnitConstants.m +degree = acts.UnitConstants.degree + +bv = acts.BinningValue + +gctx = acts.GeometryContext() +logLevel = acts.logging.VERBOSE + + +def test_zdirection_container_blueprint(tmp_path): + + def write(root: acts.BlueprintNode, stage: int): + gz = tmp_path / f"blueprint_{stage}.dot" + print(gz) + with gz.open("w") as fh: + root.graphviz(fh) + + base = acts.Transform3.Identity() + + root = acts.Blueprint(envelope=acts.ExtentEnvelope(r=[10 * mm, 10 * mm])) + assert root.depth == 0 + + barrel = root.addCylinderContainer("Barrel", direction=bv.binR) + + assert barrel.depth == 1 + + r = 25 * mm + for i in range(1, 3): + r += 50 * mm + bounds = acts.CylinderVolumeBounds(r, r + 20 * mm, 200 * mm) + vol = barrel.addStaticVolume(base, bounds, name=f"Barrel_{i}") + assert vol.depth == 2 + + write(barrel, 1) + + root.clearChildren() + + assert barrel.depth == 0 + + det = root.addCylinderContainer("Detector", direction=bv.binZ) + + assert det.depth == 1 + + with det.CylinderContainer("nEC", direction=bv.binZ) as ec: + assert ec.depth == 2 + z = -200 + for i in range(1, 3): + z -= 200 * mm + bounds = acts.CylinderVolumeBounds(100 * mm, 150 * mm, 50 * mm) + + trf = base * acts.Translation3(acts.Vector3(0, 0, z)) + + vol = ec.addStaticVolume(trf, bounds, name=f"nEC_{i}") + assert vol.depth == 3 + + write(ec, 2) + + det.addChild(barrel) + assert barrel.depth == 2 + + write(det, 3) + + with det.CylinderContainer("pEC", direction=bv.binZ) as ec: + assert ec.depth == 2 + z = 200 + for i in range(1, 3): + z += 200 * mm + bounds = acts.CylinderVolumeBounds(100 * mm, 150 * mm, 50 * mm) + + trf = base * acts.Translation3(acts.Vector3(0, 0, z)) + + vol = ec.addStaticVolume(trf, bounds, name=f"pEC_{i}") + assert vol.depth == 3 + + write(root, 4) diff --git a/Examples/Scripts/Python/blueprint.py b/Examples/Scripts/Python/blueprint.py new file mode 100755 index 00000000000..0dca3a6629f --- /dev/null +++ b/Examples/Scripts/Python/blueprint.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +from pathlib import Path + +import acts + +mm = acts.UnitConstants.mm +degree = acts.UnitConstants.degree + +root = acts.Blueprint(envelope=acts.ExtentEnvelope(r=[10 * mm, 10 * mm])) + + +pixel = root.addCylinderContainer(direction=acts.BinningValue.binZ, name="Pixel") +print(repr(pixel)) + +trf = acts.Transform3.Identity() * acts.Translation3(acts.Vector3(0, 0, 0 * mm)) + + +if True: + barrel = acts.CylinderContainerBlueprintNode( + "PixelBarrel", + acts.BinningValue.binR, + attachmentStrategy=acts.CylinderVolumeStack.AttachmentStrategy.Gap, + resizeStrategy=acts.CylinderVolumeStack.ResizeStrategy.Gap, + ) + pixel.addChild(barrel) + + print("Barrel") + r = 25 * mm + for i in range(0, 4): + r += 50 * mm + bounds = acts.CylinderVolumeBounds(r, r + 20 * mm, 200 * mm) + print(bounds) + brlLayer = barrel.addStaticVolume(trf, bounds, name=f"PixelBarrelLayer{i}") + assert brlLayer.name == f"PixelBarrelLayer{i}" + + +if True: + + with pixel.CylinderContainer("PixelPosEndcap", acts.BinningValue.binZ) as ec: + print("Positive Endcap") + + ec.attachmentStrategy = acts.CylinderVolumeStack.AttachmentStrategy.Gap + ec.resizeStrategy = acts.CylinderVolumeStack.ResizeStrategy.Gap + + z = 200 + for i in range(0, 4): + z += 200 * mm + bounds = acts.CylinderVolumeBounds(100 * mm, 150 * mm, 50 * mm) + print(bounds) + + trf = acts.Transform3.Identity() * acts.Translation3(acts.Vector3(0, 0, z)) + + with ec.StaticVolume(trf, bounds, name=f"PixelPosEndcapDisk{i}") as disc: + print("Add disk", i) + + assert disc.name == f"PixelPosEndcapDisk{i}" + + +if True: + with pixel.Material() as mat: + with mat.CylinderContainer( + direction=acts.BinningValue.binZ, name="PixelNegEndcap" + ) as ec: + ec.attachmentStrategy = acts.CylinderVolumeStack.AttachmentStrategy.Gap + + print("Negative Endcap") + + z = -200 + for i in range(0, 4): + z -= 200 * mm + bounds = acts.CylinderVolumeBounds(200 * mm, 300 * mm, 50 * mm) + print(bounds) + + trf = acts.Transform3.Identity() * acts.Translation3( + acts.Vector3(0, 0, z) + ) + + with ec.StaticVolume( + trf, bounds, name=f"PixelNegEndcapDisk{i}" + ) as disk: + print("Add disk", i) + assert disk.name == f"PixelNegEndcapDisk{i}" + + +with open("blueprint.dot", "w") as fh: + root.graphviz(fh) + + +gctx = acts.GeometryContext() +trackingGeometry = root.construct( + options=acts.BlueprintNode.Options(), gctx=gctx, level=acts.logging.VERBOSE +) + +vis = acts.ObjVisualization3D() +trackingGeometry.visualize(vis, gctx) +with Path("blueprint.obj").open("w") as fh: + vis.write(fh) +# print("DONE") diff --git a/Tests/UnitTests/Core/Detector/CMakeLists.txt b/Tests/UnitTests/Core/Detector/CMakeLists.txt index eff298adb84..0c51456d7e4 100644 --- a/Tests/UnitTests/Core/Detector/CMakeLists.txt +++ b/Tests/UnitTests/Core/Detector/CMakeLists.txt @@ -1,4 +1,4 @@ -add_unittest(Blueprint BlueprintTests.cpp) +add_unittest(Gen2Blueprint BlueprintTests.cpp) add_unittest(BlueprintHelper BlueprintHelperTests.cpp) add_unittest(CylindricalContainerBuilder CylindricalContainerBuilderTests.cpp) add_unittest(CylindricalDetectorFromBlueprint CylindricalDetectorFromBlueprintTests.cpp) diff --git a/Tests/UnitTests/Core/Geometry/BlueprintApiTests.cpp b/Tests/UnitTests/Core/Geometry/BlueprintApiTests.cpp new file mode 100644 index 00000000000..737f410e24b --- /dev/null +++ b/Tests/UnitTests/Core/Geometry/BlueprintApiTests.cpp @@ -0,0 +1,409 @@ +// 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 +#include +#include + +#include "Acts/Definitions/Algebra.hpp" +#include "Acts/Definitions/Units.hpp" +#include "Acts/Detector/ProtoBinning.hpp" +#include "Acts/Geometry/Blueprint.hpp" +#include "Acts/Geometry/CylinderContainerBlueprintNode.hpp" +#include "Acts/Geometry/CylinderVolumeBounds.hpp" +#include "Acts/Geometry/CylinderVolumeStack.hpp" +#include "Acts/Geometry/GeometryContext.hpp" +#include "Acts/Geometry/LayerBlueprintNode.hpp" +#include "Acts/Geometry/MaterialDesignatorBlueprintNode.hpp" +#include "Acts/Geometry/TrackingGeometry.hpp" +#include "Acts/Geometry/TrackingVolume.hpp" +#include "Acts/Navigation/INavigationPolicy.hpp" +#include "Acts/Navigation/NavigationStream.hpp" +#include "Acts/Surfaces/RectangleBounds.hpp" +#include "Acts/Tests/CommonHelpers/DetectorElementStub.hpp" +#include "Acts/Utilities/Logger.hpp" +#include "Acts/Visualization/GeometryView3D.hpp" +#include "Acts/Visualization/ObjVisualization3D.hpp" + +#include +#include +#include + +using namespace Acts::UnitLiterals; + +namespace Acts::Test { + +auto logger = Acts::getDefaultLogger("UnitTests", Acts::Logging::DEBUG); + +GeometryContext gctx; + +inline std::vector> makeFanLayer( + const Transform3& base, + std::vector>& elements, + double r = 300_mm, std::size_t nSensors = 8, double thickness = 0) { + auto recBounds = std::make_shared(40_mm, 60_mm); + + double deltaPhi = 2 * std::numbers::pi / nSensors; + std::vector> surfaces; + for (std::size_t i = 0; i < nSensors; i++) { + // Create a fan of sensors + + Transform3 trf = base * AngleAxis3{deltaPhi * i, Vector3::UnitZ()} * + Translation3(Vector3::UnitX() * r); + + if (i % 2 == 0) { + trf = trf * Translation3{Vector3::UnitZ() * 5_mm}; + } + + auto& element = elements.emplace_back( + std::make_unique(trf, recBounds, thickness)); + + element->surface().assignDetectorElement(*element); + + surfaces.push_back(element->surface().getSharedPtr()); + } + return surfaces; +} + +inline std::vector> makeBarrelLayer( + const Transform3& base, + std::vector>& elements, + double r = 300_mm, std::size_t nStaves = 10, int nSensorsPerStave = 8, + double thickness = 0, double hlPhi = 40_mm, double hlZ = 60_mm) { + auto recBounds = std::make_shared(hlPhi, hlZ); + + double deltaPhi = 2 * std::numbers::pi / nStaves; + std::vector> surfaces; + + for (std::size_t istave = 0; istave < nStaves; istave++) { + for (int isensor = -nSensorsPerStave; isensor <= nSensorsPerStave; + isensor++) { + double z = isensor * (2 * hlZ + 5_mm); + + Transform3 trf = base * Translation3(Vector3::UnitZ() * z) * + AngleAxis3{deltaPhi * istave, Vector3::UnitZ()} * + Translation3(Vector3::UnitX() * r) * + AngleAxis3{10_degree, Vector3::UnitZ()} * + AngleAxis3{90_degree, Vector3::UnitY()} * + AngleAxis3{90_degree, Vector3::UnitZ()}; + auto& element = elements.emplace_back( + std::make_unique(trf, recBounds, thickness)); + element->surface().assignDetectorElement(*element); + surfaces.push_back(element->surface().getSharedPtr()); + } + } + + return surfaces; +} + +BOOST_AUTO_TEST_SUITE(Geometry); + +BOOST_AUTO_TEST_SUITE(BlueprintApiTest); + +void pseudoNavigation(const TrackingGeometry& trackingGeometry, + Vector3 position, const Vector3& direction, + std::ostream& csv, std::size_t run, + std::size_t substepsPerCm, const Logger& logger) { + ACTS_VERBOSE("start navigation " << run); + ACTS_VERBOSE("dir: " << direction.transpose()); + ACTS_VERBOSE(direction.norm()); + + std::mt19937 rng{static_cast(run)}; + std::uniform_real_distribution<> dist{0.01, 0.99}; + + const auto* volume = trackingGeometry.lowestTrackingVolume(gctx, position); + BOOST_REQUIRE_NE(volume, nullptr); + ACTS_VERBOSE(volume->volumeName()); + + NavigationStream main; + const TrackingVolume* currentVolume = volume; + + csv << run << "," << position[0] << "," << position[1] << "," << position[2]; + csv << "," << volume->geometryId().volume(); + csv << "," << volume->geometryId().boundary(); + csv << "," << volume->geometryId().sensitive(); + csv << std::endl; + + ACTS_VERBOSE("start pseudo navigation"); + + for (std::size_t i = 0; i < 100; i++) { + main = NavigationStream{}; + AppendOnlyNavigationStream stream{main}; + + currentVolume->initializeNavigationCandidates( + {.position = position, .direction = direction}, stream, logger); + + ACTS_VERBOSE(main.candidates().size() << " candidates"); + + for (const auto& candidate : main.candidates()) { + ACTS_VERBOSE(" -> " << candidate.surface().geometryId()); + ACTS_VERBOSE(" " << candidate.surface().toStream(gctx)); + } + + ACTS_VERBOSE("initializing candidates"); + main.initialize(gctx, {position, direction}, BoundaryTolerance::None()); + + ACTS_VERBOSE(main.candidates().size() << " candidates remaining"); + + for (const auto& candidate : main.candidates()) { + ACTS_VERBOSE(" -> " << candidate.surface().geometryId()); + ACTS_VERBOSE(" " << candidate.surface().toStream(gctx)); + } + + if (main.currentCandidate().surface().isOnSurface(gctx, position, + direction)) { + ACTS_VERBOSE("Already on surface at initialization, skipping candidate"); + + auto id = main.currentCandidate().surface().geometryId(); + csv << run << "," << position[0] << "," << position[1] << "," + << position[2]; + csv << "," << id.volume(); + csv << "," << id.boundary(); + csv << "," << id.sensitive(); + csv << std::endl; + if (!main.switchToNextCandidate()) { + ACTS_WARNING("candidates exhausted unexpectedly"); + break; + } + } + + auto writeIntersection = [&](const Vector3& pos, const Surface& surface) { + csv << run << "," << pos[0] << "," << pos[1] << "," << pos[2]; + csv << "," << surface.geometryId().volume(); + csv << "," << surface.geometryId().boundary(); + csv << "," << surface.geometryId().sensitive(); + csv << std::endl; + }; + + bool terminated = false; + while (main.remainingCandidates() > 0) { + const auto& candidate = main.currentCandidate(); + + ACTS_VERBOSE(candidate.portal); + ACTS_VERBOSE(candidate.intersection.position().transpose()); + + ACTS_VERBOSE("moving to position: " << position.transpose() << " (r=" + << VectorHelpers::perp(position) + << ")"); + + Vector3 delta = candidate.intersection.position() - position; + + std::size_t substeps = + std::max(1l, std::lround(delta.norm() / 10_cm * substepsPerCm)); + + for (std::size_t j = 0; j < substeps; j++) { + // position += delta / (substeps + 1); + Vector3 subpos = position + dist(rng) * delta; + csv << run << "," << subpos[0] << "," << subpos[1] << "," << subpos[2]; + csv << "," << currentVolume->geometryId().volume(); + csv << ",0,0"; // zero boundary and sensitive ids + csv << std::endl; + } + + position = candidate.intersection.position(); + ACTS_VERBOSE(" -> " + << position.transpose() + << " (r=" << VectorHelpers::perp(position) << ")"); + + writeIntersection(position, candidate.surface()); + + if (candidate.portal != nullptr) { + ACTS_VERBOSE( + "On portal: " << candidate.portal->surface().toStream(gctx)); + currentVolume = + candidate.portal->resolveVolume(gctx, position, direction).value(); + + if (currentVolume == nullptr) { + ACTS_VERBOSE("switched to nullptr -> we're done"); + terminated = true; + } + break; + + } else { + ACTS_VERBOSE("Not on portal"); + } + + main.switchToNextCandidate(); + } + + if (terminated) { + ACTS_VERBOSE("Terminate pseudo navigation"); + break; + } + + ACTS_VERBOSE("switched to " << currentVolume->volumeName()); + + ACTS_VERBOSE("-----"); + } +} + +BOOST_AUTO_TEST_CASE(NodeApiTestContainers) { + // Transform3 base{AngleAxis3{30_degree, Vector3{1, 0, 0}}}; + Transform3 base{Transform3::Identity()}; + + std::vector> detectorElements; + auto makeFan = [&](const Transform3& layerBase, auto&&..., double r, + std::size_t nSensors, double thickness) { + return makeFanLayer(layerBase, detectorElements, r, nSensors, thickness); + }; + + Blueprint::Config cfg; + cfg.envelope[BinningValue::binZ] = {20_mm, 20_mm}; + cfg.envelope[BinningValue::binR] = {0_mm, 20_mm}; + auto root = std::make_unique(cfg); + + root->addMaterial("GlobalMaterial", [&](MaterialDesignatorBlueprintNode& + mat) { + Experimental::ProtoBinning zBinning{BinningValue::binZ, + AxisBoundaryType::Bound, 20}; + + Experimental::ProtoBinning rPhiBinning{BinningValue::binRPhi, + AxisBoundaryType::Bound, 20}; + + mat.setBinning(std::vector{std::tuple{ + CylinderVolumeBounds::Face::OuterCylinder, rPhiBinning, zBinning}}); + + mat.addCylinderContainer("Detector", BinningValue::binR, [&](auto& det) { + det.addCylinderContainer("Pixel", BinningValue::binZ, [&](auto& cyl) { + cyl.setAttachmentStrategy(CylinderVolumeStack::AttachmentStrategy::Gap) + .setResizeStrategy(CylinderVolumeStack::ResizeStrategy::Gap); + + cyl.addCylinderContainer( + "PixelNegativeEndcap", BinningValue::binZ, [&](auto& ec) { + ec.setAttachmentStrategy( + CylinderVolumeStack::AttachmentStrategy::Gap); + + auto makeLayer = [&](const Transform3& trf, auto& layer) { + std::vector> surfaces; + auto layerSurfaces = makeFan(trf, 300_mm, 10, 2_mm); + std::copy(layerSurfaces.begin(), layerSurfaces.end(), + std::back_inserter(surfaces)); + layerSurfaces = makeFan(trf, 500_mm, 16, 2_mm); + std::copy(layerSurfaces.begin(), layerSurfaces.end(), + std::back_inserter(surfaces)); + + layer.setSurfaces(surfaces) + .setLayerType(LayerBlueprintNode::LayerType::Disc) + .setEnvelope(ExtentEnvelope{{ + .z = {5_mm, 5_mm}, + .r = {10_mm, 20_mm}, + }}) + .setTransform(base); + }; + + ec.addLayer("PixelNeg1", [&](auto& layer) { + makeLayer(base * Translation3{Vector3{0, 0, -700_mm}}, layer); + }); + + ec.addLayer("PixelNeg2", [&](auto& layer) { + makeLayer(base * Translation3{Vector3{0, 0, -500_mm}}, layer); + }); + }); + + cyl.addCylinderContainer( + "PixelBarrel", BinningValue::binR, [&](auto& brl) { + brl.setAttachmentStrategy( + CylinderVolumeStack::AttachmentStrategy::Gap) + .setResizeStrategy(CylinderVolumeStack::ResizeStrategy::Gap); + + auto makeLayer = [&](const std::string& name, double r, + std::size_t nStaves, int nSensorsPerStave) { + brl.addLayer(name, [&](auto& layer) { + std::vector> surfaces = + makeBarrelLayer(base, detectorElements, r, nStaves, + nSensorsPerStave, 2.5_mm, 10_mm, 20_mm); + + layer.setSurfaces(surfaces) + .setLayerType(LayerBlueprintNode::LayerType::Cylinder) + .setEnvelope(ExtentEnvelope{{ + .z = {5_mm, 5_mm}, + .r = {1_mm, 1_mm}, + }}) + .setTransform(base); + }); + }; + + makeLayer("PixelLayer0", 30_mm, 18, 5); + makeLayer("PixelLayer1", 90_mm, 30, 6); + + brl.addStaticVolume(base, + std::make_shared( + 100_mm, 110_mm, 250_mm), + "PixelSupport"); + + makeLayer("PixelLayer2", 150_mm, 40, 7); + makeLayer("PixelLayer3", 250_mm, 70, 8); + }); + + auto& ec = + cyl.addCylinderContainer("PixelPosWrapper", BinningValue::binR); + ec.setResizeStrategy(CylinderVolumeStack::ResizeStrategy::Gap); + ec.addStaticVolume(std::make_unique( + base * Translation3{Vector3{0, 0, 600_mm}}, + std::make_shared(150_mm, 390_mm, 200_mm), + "PixelPositiveEndcap")); + }); + + det.addStaticVolume( + base, std::make_shared(0_mm, 23_mm, 1000_mm), + "BeamPipe"); + }); + }); + + std::ofstream dot{"api_test_container.dot"}; + root->graphviz(dot); + + auto trackingGeometry = root->construct({}, gctx, *logger); + + trackingGeometry->visitVolumes([&](const TrackingVolume* volume) { + std::cout << volume->volumeName() << std::endl; + std::cout << " -> id: " << volume->geometryId() << std::endl; + std::cout << " -> " << volume->portals().size() << " portals" << std::endl; + }); + + ObjVisualization3D vis; + + trackingGeometry->visualize(vis, gctx, {}, {}); + + vis.write("api_test_container.obj"); + + Vector3 position = Vector3::Zero(); + std::ofstream csv{"api_test_container.csv"}; + csv << "x,y,z,volume,boundary,sensitive" << std::endl; + + std::mt19937 rnd{42}; + + std::uniform_real_distribution<> dist{-1, 1}; + + double etaWidth = 3; + double thetaMin = 2 * std::atan(std::exp(-etaWidth)); + double thetaMax = 2 * std::atan(std::exp(etaWidth)); + std::uniform_real_distribution<> thetaDist{thetaMin, thetaMax}; + + using namespace Acts::UnitLiterals; + + for (std::size_t i = 0; i < 5000; i++) { + double theta = thetaDist(rnd); + double phi = 2 * std::numbers::pi * dist(rnd); + + Vector3 direction; + direction[0] = std::sin(theta) * std::cos(phi); + direction[1] = std::sin(theta) * std::sin(phi); + direction[2] = std::cos(theta); + + pseudoNavigation(*trackingGeometry, position, direction, csv, i, 2, + *logger->clone(std::nullopt, Logging::DEBUG)); + } +} + +BOOST_AUTO_TEST_SUITE_END(); + +BOOST_AUTO_TEST_SUITE_END(); + +} // namespace Acts::Test diff --git a/Tests/UnitTests/Core/Geometry/BlueprintTests.cpp b/Tests/UnitTests/Core/Geometry/BlueprintTests.cpp new file mode 100644 index 00000000000..c3a0ecc028f --- /dev/null +++ b/Tests/UnitTests/Core/Geometry/BlueprintTests.cpp @@ -0,0 +1,531 @@ +// 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 +#include +#include + +#include "Acts/Definitions/Units.hpp" +#include "Acts/Geometry/Blueprint.hpp" +#include "Acts/Geometry/BlueprintNode.hpp" +#include "Acts/Geometry/CylinderContainerBlueprintNode.hpp" +#include "Acts/Geometry/CylinderVolumeBounds.hpp" +#include "Acts/Geometry/CylinderVolumeStack.hpp" +#include "Acts/Geometry/GeometryContext.hpp" +#include "Acts/Geometry/LayerBlueprintNode.hpp" +#include "Acts/Geometry/MaterialDesignatorBlueprintNode.hpp" +#include "Acts/Geometry/StaticBlueprintNode.hpp" +#include "Acts/Geometry/TrackingVolume.hpp" +#include "Acts/Material/BinnedSurfaceMaterial.hpp" +#include "Acts/Material/ProtoSurfaceMaterial.hpp" +#include "Acts/Surfaces/RectangleBounds.hpp" +#include "Acts/Tests/CommonHelpers/DetectorElementStub.hpp" +#include "Acts/Utilities/BinningType.hpp" +#include "Acts/Utilities/Logger.hpp" + +#include +#include +#include + +using namespace Acts::UnitLiterals; + +namespace Acts::Test { + +auto logger = Acts::getDefaultLogger("UnitTests", Acts::Logging::INFO); + +GeometryContext gctx; + +namespace { + +auto nameLookup(const TrackingGeometry& geo) { + return [&](const std::string& name) -> const TrackingVolume& { + const TrackingVolume* volume = nullptr; + + geo.visitVolumes([&](const TrackingVolume* v) { + if (v->volumeName() == name) { + volume = v; + } + }); + + if (volume == nullptr) { + throw std::runtime_error("Volume not found: " + name); + } + return *volume; + }; +} + +std::size_t countVolumes(const TrackingGeometry& geo) { + std::size_t nVolumes = 0; + geo.visitVolumes([&](const TrackingVolume* /*volume*/) { nVolumes++; }); + return nVolumes; +} + +} // namespace + +BOOST_AUTO_TEST_SUITE(Geometry); + +BOOST_AUTO_TEST_SUITE(BlueprintNodeTest); + +BOOST_AUTO_TEST_CASE(InvalidRoot) { + Logging::ScopedFailureThreshold threshold{Logging::Level::FATAL}; + + Blueprint::Config cfg; + Blueprint root{cfg}; + BOOST_CHECK_THROW(root.construct({}, gctx, *logger), std::logic_error); + + // More than one child is also invalid + auto cylBounds = std::make_shared(10_mm, 20_mm, 100_mm); + root.addChild( + std::make_unique(std::make_unique( + Transform3::Identity(), cylBounds, "child1"))); + root.addChild( + std::make_unique(std::make_unique( + Transform3::Identity(), cylBounds, "child2"))); + + BOOST_CHECK_THROW(root.construct({}, gctx, *logger), std::logic_error); +} + +class DummyNode : public BlueprintNode { + public: + explicit DummyNode(const std::string& name) : m_name(name) {} + + const std::string& name() const override { return m_name; } + + Volume& build(const BlueprintOptions& /*options*/, + const GeometryContext& /*gctx*/, + const Acts::Logger& /*logger*/) override { + throw std::logic_error("Not implemented"); + } + + PortalShellBase& connect(const BlueprintOptions& /*options*/, + const GeometryContext& /*gctx*/, + const Logger& /*logger */) override { + throw std::logic_error("Not implemented"); + } + + void finalize(const BlueprintOptions& /*options*/, + const GeometryContext& /*gctx*/, TrackingVolume& /*parent*/, + const Logger& /*logger*/) override { + throw std::logic_error("Not implemented"); + } + + private: + std::string m_name; +}; + +BOOST_AUTO_TEST_CASE(RootCannotBeChild) { + auto node = std::make_shared("node"); + Blueprint::Config cfg; + auto root = std::make_shared(cfg); + + BOOST_CHECK_THROW(node->addChild(root), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(AddChildInvalid) { + auto node = std::make_shared("node"); + + // Add self + BOOST_CHECK_THROW(node->addChild(node), std::invalid_argument); + + // Add nullptr + BOOST_CHECK_THROW(node->addChild(nullptr), std::invalid_argument); + + auto nodeB = std::make_shared("nodeB"); + auto nodeC = std::make_shared("nodeC"); + + node->addChild(nodeB); + nodeB->addChild(nodeC); + BOOST_CHECK_THROW(nodeC->addChild(node), std::invalid_argument); + + // already has parent, can't be added as a child anywhere else + BOOST_CHECK_THROW(node->addChild(nodeC), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(Depth) { + auto node1 = std::make_shared("node1"); + auto node2 = std::make_shared("node2"); + auto node3 = std::make_shared("node3"); + + BOOST_CHECK_EQUAL(node1->depth(), 0); + BOOST_CHECK_EQUAL(node2->depth(), 0); + BOOST_CHECK_EQUAL(node3->depth(), 0); + + node2->addChild(node3); + BOOST_CHECK_EQUAL(node2->depth(), 0); + BOOST_CHECK_EQUAL(node3->depth(), 1); + + node1->addChild(node2); + BOOST_CHECK_EQUAL(node1->depth(), 0); + BOOST_CHECK_EQUAL(node2->depth(), 1); + BOOST_CHECK_EQUAL(node3->depth(), 2); +} + +BOOST_AUTO_TEST_CASE(Static) { + Blueprint::Config cfg; + cfg.envelope[BinningValue::binZ] = {20_mm, 20_mm}; + cfg.envelope[BinningValue::binR] = {1_mm, 2_mm}; + Blueprint root{cfg}; + + double hlZ = 30_mm; + auto cylBounds = std::make_shared(10_mm, 20_mm, hlZ); + auto cyl = std::make_unique(Transform3::Identity(), cylBounds, + "child"); + + root.addStaticVolume(std::move(cyl)); + + BOOST_CHECK_EQUAL(root.children().size(), 1); + + auto tGeometry = root.construct({}, gctx, *logger); + + BOOST_REQUIRE(tGeometry); + + BOOST_CHECK_EQUAL(tGeometry->highestTrackingVolume()->volumes().size(), 1); + + BOOST_CHECK_EQUAL(countVolumes(*tGeometry), 2); + + auto lookup = nameLookup(*tGeometry); + auto actCyl = + dynamic_cast(lookup("child").volumeBounds()); + // Size as given + BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMinR), 10_mm); + BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMaxR), 20_mm); + BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eHalfLengthZ), hlZ); + + auto worldCyl = + dynamic_cast(lookup("World").volumeBounds()); + BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMinR), 9_mm); + BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMaxR), 22_mm); + BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eHalfLengthZ), + hlZ + 20_mm); + + BOOST_CHECK_EQUAL(lookup("World").portals().size(), 8); +} + +BOOST_AUTO_TEST_CASE(CylinderContainer) { + Blueprint::Config cfg; + cfg.envelope[BinningValue::binZ] = {20_mm, 20_mm}; + cfg.envelope[BinningValue::binR] = {2_mm, 20_mm}; + auto root = std::make_unique(cfg); + + auto& cyl = root->addCylinderContainer("Container", BinningValue::binZ); + cyl.setAttachmentStrategy(CylinderVolumeStack::AttachmentStrategy::Gap); + + double z0 = -200_mm; + double hlZ = 30_mm; + auto cylBounds = std::make_shared(10_mm, 20_mm, hlZ); + for (std::size_t i = 0; i < 3; i++) { + auto childCyl = std::make_unique( + Transform3::Identity() * + Translation3{Vector3{0, 0, z0 + i * 2 * hlZ * 1.2}}, + cylBounds, "child" + std::to_string(i)); + cyl.addStaticVolume(std::move(childCyl)); + } + + auto tGeometry = root->construct({}, gctx, *logger); + + // 4 real volumes + 2 gaps + BOOST_CHECK_EQUAL(countVolumes(*tGeometry), 6); + + auto lookup = nameLookup(*tGeometry); + auto worldCyl = + dynamic_cast(lookup("World").volumeBounds()); + BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMinR), 8_mm); + BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMaxR), 40_mm); + BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eHalfLengthZ), 122_mm); + + BOOST_CHECK_EQUAL(lookup("World").portals().size(), 8); + + for (std::size_t i = 0; i < 3; i++) { + auto actCyl = dynamic_cast( + lookup("child" + std::to_string(i)).volumeBounds()); + BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMinR), 10_mm); + BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMaxR), 20_mm); + BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eHalfLengthZ), hlZ); + } + + for (std::size_t i = 0; i < 2; i++) { + auto gapCyl = dynamic_cast( + lookup("Container::Gap" + std::to_string(i + 1)).volumeBounds()); + BOOST_CHECK_EQUAL(gapCyl.get(CylinderVolumeBounds::eMinR), 10_mm); + BOOST_CHECK_EQUAL(gapCyl.get(CylinderVolumeBounds::eMaxR), 20_mm); + BOOST_CHECK_EQUAL(gapCyl.get(CylinderVolumeBounds::eHalfLengthZ), 6_mm); + } +} + +BOOST_AUTO_TEST_CASE(Confined) { + Transform3 base{Transform3::Identity()}; + + Blueprint::Config cfg; + cfg.envelope[BinningValue::binZ] = {20_mm, 20_mm}; + cfg.envelope[BinningValue::binR] = {2_mm, 20_mm}; + auto root = std::make_unique(cfg); + + root->addStaticVolume( + base, std::make_shared(50_mm, 400_mm, 1000_mm), + "PixelWrapper", [&](auto& wrap) { + double rMin = 100_mm; + double rMax = 350_mm; + double hlZ = 100_mm; + + wrap.addStaticVolume( + base * Translation3{Vector3{0, 0, -600_mm}}, + std::make_shared(rMin, rMax, hlZ), + "PixelNeg1"); + + wrap.addStaticVolume( + base * Translation3{Vector3{0, 0, -200_mm}}, + std::make_shared(rMin, rMax, hlZ), + "PixelNeg2"); + + wrap.addStaticVolume( + base * Translation3{Vector3{0, 0, 200_mm}}, + std::make_shared(rMin, rMax, hlZ), + "PixelPos1"); + + wrap.addStaticVolume( + base * Translation3{Vector3{0, 0, 600_mm}}, + std::make_shared(rMin, rMax, hlZ), + "PixelPos2"); + }); + + auto trackingGeometry = root->construct({}, gctx, *logger); + + // overall dimensions are the wrapper volume + envelope + auto lookup = nameLookup(*trackingGeometry); + auto worldCyl = + dynamic_cast(lookup("World").volumeBounds()); + BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMinR), 48_mm); + BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eMaxR), 420_mm); + BOOST_CHECK_EQUAL(worldCyl.get(CylinderVolumeBounds::eHalfLengthZ), 1020_mm); + + // 4 outer portals and 4 inner + BOOST_CHECK_EQUAL(lookup("World").portals().size(), 8); + BOOST_CHECK_EQUAL(lookup("World").volumes().size(), 1); + + auto wrapperCyl = dynamic_cast( + lookup("PixelWrapper").volumeBounds()); + BOOST_CHECK_EQUAL(wrapperCyl.get(CylinderVolumeBounds::eMinR), 50_mm); + BOOST_CHECK_EQUAL(wrapperCyl.get(CylinderVolumeBounds::eMaxR), 400_mm); + BOOST_CHECK_EQUAL(wrapperCyl.get(CylinderVolumeBounds::eHalfLengthZ), + 1000_mm); + BOOST_CHECK_EQUAL(lookup("PixelWrapper").portals().size(), 4 + 4 * 4); + BOOST_CHECK_EQUAL(lookup("PixelWrapper").volumes().size(), 4); + + for (const auto& name : + {"PixelNeg1", "PixelNeg2", "PixelPos1", "PixelPos2"}) { + auto actCyl = + dynamic_cast(lookup(name).volumeBounds()); + BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMinR), 100_mm); + BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eMaxR), 350_mm); + BOOST_CHECK_EQUAL(actCyl.get(CylinderVolumeBounds::eHalfLengthZ), 100_mm); + BOOST_CHECK_EQUAL(lookup(name).portals().size(), 4); + } +} + +BOOST_AUTO_TEST_CASE(DiscLayer) { + double yrot = 45_degree; + Transform3 base = Transform3::Identity() * AngleAxis3{yrot, Vector3::UnitY()}; + + std::vector> surfaces; + std::vector> elements; + double r = 300_mm; + std::size_t nSensors = 8; + double thickness = 2.5_mm; + auto recBounds = std::make_shared(40_mm, 60_mm); + + double deltaPhi = 2 * std::numbers::pi / nSensors; + for (std::size_t i = 0; i < nSensors; i++) { + // Create a fan of sensors + + Transform3 trf = base * AngleAxis3{deltaPhi * i, Vector3::UnitZ()} * + Translation3(Vector3::UnitX() * r); + + if (i % 2 == 0) { + trf = trf * Translation3{Vector3::UnitZ() * 5_mm}; + } + + auto& element = elements.emplace_back( + std::make_unique(trf, recBounds, thickness)); + + element->surface().assignDetectorElement(*element); + + surfaces.push_back(element->surface().getSharedPtr()); + } + + Blueprint root{{.envelope = ExtentEnvelope{{ + .z = {2_mm, 2_mm}, + .r = {3_mm, 5_mm}, + }}}}; + + root.addLayer("Layer0", [&](auto& layer) { + layer.setSurfaces(surfaces) + .setLayerType(LayerBlueprintNode::LayerType::Disc) + .setEnvelope(ExtentEnvelope{{ + .z = {0.1_mm, 0.1_mm}, + .r = {1_mm, 1_mm}, + }}) + .setTransform(base); + }); + + auto trackingGeometry = root.construct({}, gctx, *logger); + + std::size_t nSurfaces = 0; + + trackingGeometry->visitSurfaces([&](const Surface* surface) { + if (surface->associatedDetectorElement() != nullptr) { + nSurfaces++; + } + }); + + BOOST_CHECK_EQUAL(nSurfaces, surfaces.size()); + BOOST_CHECK_EQUAL(countVolumes(*trackingGeometry), 2); + auto lookup = nameLookup(*trackingGeometry); + auto layerCyl = dynamic_cast( + lookup("Layer0").volumeBounds()); + BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMinR), 258.9999999_mm, + 1e-6); + BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMaxR), 346.25353003_mm, + 1e-6); + BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eHalfLengthZ), 3.85_mm, + 1e-6); +} + +BOOST_AUTO_TEST_CASE(CylinderLayer) { + double yrot = 0_degree; + Transform3 base = Transform3::Identity() * AngleAxis3{yrot, Vector3::UnitY()}; + + std::vector> surfaces; + std::vector> elements; + + double r = 300_mm; + std::size_t nStaves = 10; + int nSensorsPerStave = 8; + double thickness = 0; + double hlPhi = 40_mm; + double hlZ = 60_mm; + auto recBounds = std::make_shared(hlPhi, hlZ); + + double deltaPhi = 2 * std::numbers::pi / nStaves; + + for (std::size_t istave = 0; istave < nStaves; istave++) { + for (int isensor = -nSensorsPerStave; isensor <= nSensorsPerStave; + isensor++) { + double z = isensor * (2 * hlZ + 5_mm); + + Transform3 trf = base * Translation3(Vector3::UnitZ() * z) * + AngleAxis3{deltaPhi * istave, Vector3::UnitZ()} * + Translation3(Vector3::UnitX() * r) * + AngleAxis3{10_degree, Vector3::UnitZ()} * + AngleAxis3{90_degree, Vector3::UnitY()} * + AngleAxis3{90_degree, Vector3::UnitZ()}; + auto& element = elements.emplace_back( + std::make_unique(trf, recBounds, thickness)); + element->surface().assignDetectorElement(*element); + surfaces.push_back(element->surface().getSharedPtr()); + } + } + + Blueprint root{{.envelope = ExtentEnvelope{{ + .z = {2_mm, 2_mm}, + .r = {3_mm, 5_mm}, + }}}}; + + root.addLayer("Layer0", [&](auto& layer) { + layer.setSurfaces(surfaces) + .setLayerType(LayerBlueprintNode::LayerType::Cylinder) + .setEnvelope(ExtentEnvelope{{ + .z = {10_mm, 10_mm}, + .r = {20_mm, 10_mm}, + }}) + .setTransform(base); + }); + + auto trackingGeometry = root.construct({}, gctx, *logger); + + std::size_t nSurfaces = 0; + + trackingGeometry->visitSurfaces([&](const Surface* surface) { + if (surface->associatedDetectorElement() != nullptr) { + nSurfaces++; + } + }); + + BOOST_CHECK_EQUAL(nSurfaces, surfaces.size()); + BOOST_CHECK_EQUAL(countVolumes(*trackingGeometry), 2); + auto lookup = nameLookup(*trackingGeometry); + auto layerCyl = dynamic_cast( + lookup("Layer0").volumeBounds()); + BOOST_CHECK_EQUAL(lookup("Layer0").portals().size(), 4); + BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMinR), 275.6897761_mm, + 1e-6); + BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eMaxR), 319.4633358_mm, + 1e-6); + BOOST_CHECK_CLOSE(layerCyl.get(CylinderVolumeBounds::eHalfLengthZ), 1070_mm, + 1e-6); +} + +BOOST_AUTO_TEST_CASE(Material) { + Blueprint::Config cfg; + cfg.envelope[BinningValue::binZ] = {20_mm, 20_mm}; + cfg.envelope[BinningValue::binR] = {1_mm, 2_mm}; + Blueprint root{cfg}; + + double hlZ = 30_mm; + auto cylBounds = std::make_shared(10_mm, 20_mm, hlZ); + auto cyl = std::make_unique(Transform3::Identity(), cylBounds, + "child"); + + using enum BinningValue; + using enum CylinderVolumeBounds::Face; + using enum AxisBoundaryType; + + root.addMaterial("Material", [&](auto& mat) { + // @TODO: This API is not great + mat.setBinning(std::vector{ + std::tuple{NegativeDisc, Experimental::ProtoBinning{binR, Bound, 5}, + Experimental::ProtoBinning{binPhi, Bound, 10}}, + std::tuple{PositiveDisc, Experimental::ProtoBinning{binR, Bound, 15}, + Experimental::ProtoBinning{binPhi, Bound, 20}}, + }); + + mat.addStaticVolume(std::move(cyl)); + }); + + auto trackingGeometry = root.construct({}, gctx, *logger); + + BOOST_CHECK_EQUAL(countVolumes(*trackingGeometry), 2); + auto lookup = nameLookup(*trackingGeometry); + auto& child = lookup("child"); + + const auto* negDisc = child.portals().at(0).surface().surfaceMaterial(); + const auto* posDisc = child.portals().at(1).surface().surfaceMaterial(); + BOOST_CHECK_NE(negDisc, nullptr); + BOOST_CHECK_NE(posDisc, nullptr); + + const auto& negDiscMat = + dynamic_cast(*negDisc); + const auto& posDiscMat = + dynamic_cast(*posDisc); + + BOOST_CHECK_EQUAL(negDiscMat.binning().binning.at(0).bins(), 5); + BOOST_CHECK_EQUAL(negDiscMat.binning().binning.at(1).bins(), 10); + BOOST_CHECK_EQUAL(posDiscMat.binning().binning.at(0).bins(), 15); + BOOST_CHECK_EQUAL(posDiscMat.binning().binning.at(1).bins(), 20); + + for (std::size_t i = 2; i < child.portals().size(); i++) { + BOOST_CHECK_EQUAL(child.portals().at(i).surface().surfaceMaterial(), + nullptr); + } +} + +BOOST_AUTO_TEST_SUITE_END(); + +BOOST_AUTO_TEST_SUITE_END(); + +} // namespace Acts::Test diff --git a/Tests/UnitTests/Core/Geometry/CMakeLists.txt b/Tests/UnitTests/Core/Geometry/CMakeLists.txt index 0226e46d7d0..41c218129fe 100644 --- a/Tests/UnitTests/Core/Geometry/CMakeLists.txt +++ b/Tests/UnitTests/Core/Geometry/CMakeLists.txt @@ -35,3 +35,5 @@ add_unittest(CylinderVolumeStack CylinderVolumeStackTests.cpp) add_unittest(PortalLink PortalLinkTests.cpp) add_unittest(Portal PortalTests.cpp) add_unittest(PortalShell PortalShellTests.cpp) +add_unittest(Blueprint BlueprintTests.cpp) +add_unittest(BlueprintApi BlueprintApiTests.cpp) diff --git a/docs/core/geometry/concepts.md b/docs/core/geometry/concepts.md new file mode 100644 index 00000000000..abd2d31951a --- /dev/null +++ b/docs/core/geometry/concepts.md @@ -0,0 +1,75 @@ +# Concepts + +:::{todo} +Not complete yet +::: + +## Tracking geometry + +## Volume + +### Volume bounds + +## Tracking volume + +## Portals + +:::{doxygenclass} Acts::Portal +::: + +### Portal links + +:::{doxygenclass} Acts::PortalLinkBase +::: + +#### Trivial portal link + +:::{doxygenclass} Acts::TrivialPortalLink +::: + +#### Grid portal link + +:::{doxygenclass} Acts::GridPortalLink +::: + +:::{doxygenclass} Acts::GridPortalLinkT +::: + +#### Composite portal link + +:::{doxygenclass} Acts::CompositePortalLink +::: + +### Portal shells + +:::{doxygenclass} Acts::PortalShellBase +::: + +:::{doxygenclass} Acts::CylinderPortalShell +::: + +:::{doxygenclass} Acts::SingleCylinderPortalShell +::: + +:::{doxygenclass} Acts::CylinderStackPortalShell +::: + +### Navigation policy + +:::{doxygenclass} Acts::INavigationPolicy +::: + +:::{doxygenclass} Acts::MultiNavigationPolicyBase +::: + +:::{doxygenclass} Acts::MultiNavigationPolicy +::: + +:::{doxygenclass} Acts::SurfaceArrayNavigationPolicy +::: + +:::{doxygenclass} Acts::TryAllNavigationPolicy +::: + +:::{doxygenstruct} Acts::ProtoLayer +::: diff --git a/docs/core/geometry/construction.md b/docs/core/geometry/construction.md new file mode 100644 index 00000000000..5bcebdb24be --- /dev/null +++ b/docs/core/geometry/construction.md @@ -0,0 +1,43 @@ +# Construction + +:::{todo} +Not complete yet +::: + +## Blueprint tracking geometry construction + +:::{doxygenclass} Acts::BlueprintNode +::: + +:::{doxygenclass} Acts::Blueprint +::: + +### Container nodes + +:::{doxygenclass} Acts::CylinderContainerBlueprintNode +::: + +### Material nodes + +:::{doxygenclass} Acts::MaterialDesignatorBlueprintNode +::: + +### Geometry identification specification + +### *Layers* and other nodes + +:::{doxygenclass} Acts::StaticBlueprintNode +::: + +:::{doxygenclass} Acts::LayerBlueprintNode +::: + +## API + +### C++ API Example + +### Python API Examples + +### Plugin usage + +### Extension capabilities diff --git a/docs/core/geometry/index.md b/docs/core/geometry/index.md index b0009a36ef1..2bcb5cdec3f 100644 --- a/docs/core/geometry/index.md +++ b/docs/core/geometry/index.md @@ -1,4 +1,5 @@ (geometry_impl)= + # Geometry module The ACTS geometry model is strongly based on the ATLAS Tracking geometry. Its @@ -15,9 +16,11 @@ logical layers will be modelled as volumes, see [](layerless_geometry). :::{toctree} :maxdepth: 1 +concepts geometry_id material surfaces legacy/legacy +construction layerless/layerless ::: diff --git a/docs/core/geometry/layerless/layerless.md b/docs/core/geometry/layerless/layerless.md index b9784ac1f74..98e1624592a 100644 --- a/docs/core/geometry/layerless/layerless.md +++ b/docs/core/geometry/layerless/layerless.md @@ -1,13 +1,13 @@ (layerless_geometry)= + # Layerless geometry ## Geometry module rosetta stone -:::{todo} -Describe replacements of `TrackingGeometry`, `TrackingVolume` etc. and how the classes map to one another. +:::{note} +The combination of the original (Gen 1) geometry classes and the new *layerless* modelling (Gen 2, this page) will result in a combined Gen 3 geometry model. ::: - :::{toctree} building ::: diff --git a/docs/known-warnings.txt b/docs/known-warnings.txt index 2138dc11bb9..2c23b8a9319 100644 --- a/docs/known-warnings.txt +++ b/docs/known-warnings.txt @@ -5,5 +5,6 @@ .*Duplicate explicit target name: .* .*undefined label: .*class_acts_1_1_convex_polygon_bounds_3_01_polygon_dynamic_01_4.* .*undefined label: .*class_acts_1_1_grid_surface_material_t.* +.*undefined label: .*namespace_acts_.* # I think these are because we use specialization .*undefined label: .*bdt.*