From 45692302448dc93af3cb53385256a9af5f92a69c Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 29 Aug 2024 13:09:18 +0200 Subject: [PATCH] feat: Gen3 geometry Portals (#3501) Part of #3502 This PR implements *Portals*. A portal connects two or more neighboring volumes. Each volume has a set of portals that describes which volumes lie behind the portal in that direction. Portals use associated portal links to perform lookups of target volumes. Each portal has two links, and a corresponding surface. One link is associated with the direction along the surface's normal vector, and one with the opposite direction. Portals can be **fused** and **merged**. **Fusing** is the combination of two portal linkson the same logical surfaces. The actual surface instances can be different, as long as they are geometrically equivalent (within numerical precistion). The resulting portal will have one portal along the shared surface's normal vector, and one opposite that vector. ``` portal1 portal2 +---+ +---+ | | | | | | | | <----+ | + | +----> | | | | | | | | +---+ +---+ ``` The input portals need to have compatible link loadout, e.g. one portal needs to have the *along normal* slot filled, and the other one needs to have the *opposite normal* slot filled. If portals share a filled slot, the function throws an exception. **Merging** is the complementary operation to the fusing of portals. To be able to merge portals, the surfaces of their associated links need to be *mergeable*, and the portal links need to be compatible. This means that both portals need to have a link along the portal surface normal, opposite the normal, or both. If the equipped links are opposite relative to one another (e.g. one along one opposite), the function will throw an exception. ``` ^ ^ | | portal1| portal2| +-------+-------+ +-------+-------+ | | + | | +-------+-------+ +-------+-------+ | | | | v v ``` This is a destructive operation on both portals, their links will be moved to produce merged links, which can fail if the portal links are not compatible --- Core/include/Acts/Definitions/Algebra.hpp | 2 + Core/include/Acts/Geometry/Portal.hpp | 234 +++++++ Core/src/Geometry/CMakeLists.txt | 1 + Core/src/Geometry/Portal.cpp | 332 ++++++++++ Tests/UnitTests/Core/Detector/CMakeLists.txt | 2 +- Tests/UnitTests/Core/Geometry/CMakeLists.txt | 1 + Tests/UnitTests/Core/Geometry/PortalTests.cpp | 602 ++++++++++++++++++ 7 files changed, 1173 insertions(+), 1 deletion(-) create mode 100644 Core/include/Acts/Geometry/Portal.hpp create mode 100644 Core/src/Geometry/Portal.cpp create mode 100644 Tests/UnitTests/Core/Geometry/PortalTests.cpp diff --git a/Core/include/Acts/Definitions/Algebra.hpp b/Core/include/Acts/Definitions/Algebra.hpp index fa5edcc2252..c489f5ecd94 100644 --- a/Core/include/Acts/Definitions/Algebra.hpp +++ b/Core/include/Acts/Definitions/Algebra.hpp @@ -100,4 +100,6 @@ using AngleAxis3 = Eigen::AngleAxis; using Transform2 = Eigen::Transform; using Transform3 = Eigen::Transform; +constexpr ActsScalar s_transformEquivalentTolerance = 1e-9; + } // namespace Acts diff --git a/Core/include/Acts/Geometry/Portal.hpp b/Core/include/Acts/Geometry/Portal.hpp new file mode 100644 index 00000000000..c6ec526a42d --- /dev/null +++ b/Core/include/Acts/Geometry/Portal.hpp @@ -0,0 +1,234 @@ +// This file is part of the Acts project. +// +// Copyright (C) 2024 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 http://mozilla.org/MPL/2.0/. + +#pragma once + +#include "Acts/Definitions/Algebra.hpp" +#include "Acts/Definitions/Direction.hpp" +#include "Acts/Utilities/BinningType.hpp" +#include "Acts/Utilities/Logger.hpp" +#include "Acts/Utilities/Result.hpp" + +#include + +namespace Acts { + +class RegularSurface; +class GeometryContext; +class TrackingVolume; +class CylinderSurface; +class PlaneSurface; +class DiscSurface; +class Surface; + +class PortalLinkBase; + +/// Exception thrown when portals cannot be merged +class PortalMergingException : public std::exception { + const char* what() const noexcept override; +}; + +/// Exception thrown when portals cannot be fused +class PortalFusingException : public std::exception { + const char* what() const noexcept override; +}; + +/// A portal connects two or more neighboring volumes. Each volume has a set of +/// portals that describes which volumes lie behind the portal in that +/// direction. Portals use associated portal links to perform lookups of target +/// volumes. +/// Each portal has two links, and a corresponding surface. One link is +/// associated with the direction along the surface's normal vector, and one +/// with the opposite direction. +class Portal { + public: + /// Constructor for a portal from a single link + /// @param direction The direction of the link + /// @param link The portal link + Portal(Direction direction, std::unique_ptr link); + + /// Constructor for a portal from a surface and volume, where a trivial portal + /// link is automatically constructed. + /// @param direction The direction of the link + /// @param surface The surface from which to create the portal link + /// @param volume The volume this portal connects to in the @p direction + /// relative to the normal of @p surface. + Portal(Direction direction, std::shared_ptr surface, + TrackingVolume& volume); + + /// Constructor for a portal from two links. One of the links can be + /// `nullptr`, but at least one of them needs to be set. If both are set, they + /// need to be valid compatible links that can be fused. + /// @param gctx The geometry context + /// @param alongNormal The link along the normal of the surface + /// @param oppositeNormal The link opposite to the normal of the + Portal(const GeometryContext& gctx, + std::unique_ptr alongNormal, + std::unique_ptr oppositeNormal); + + /// Helper struct for the arguments to the portal constructor below using + /// designated initializers. + struct Arguments { + /// Aggregate over a surface and a volume with optional semantics + struct Link { + Link() = default; + /// Constructor from a surface and a volume + Link(std::shared_ptr surfaceIn, TrackingVolume& volumeIn) + : surface(std::move(surfaceIn)), volume(&volumeIn) {} + + /// The associated surface + std::shared_ptr surface = nullptr; + /// The associated volume + TrackingVolume* volume = nullptr; + }; + + /// Entry for the link along normal + /// Entry for the link opposite normal + Link alongNormal{}; + Link oppositeNormal{}; + }; + + /// Constructor that takes a geometry context and an rvalue reference to a + /// helper struct from above. This pattern allows you to use designated + /// initializers to construct this object like: + /// ```cpp + /// Portal{gctx, {.oppositeNormal = {cyl1, *vol1}}}; + /// Portal{gctx, {.alongNormal = {cyl2, *vol2}}}; + /// ``` + /// @param gctx The geometry context + /// @param args The struct containing the arguments + Portal(const GeometryContext& gctx, Arguments&& args); + + /// Fuse two portals together. Fusing is the combination of two portal links + /// on the same logical surfaces. The actual surface instances can be + /// different, as long as they are geometrically equivalent (within numerical + /// precision). The resulting portal will have one portal along the shared + /// surface's normal vector, and one opposite that vector. + /// + /// portal1 portal2 + /// +---+ +---+ + /// | | | | + /// | | | | + /// <----+ | + | +----> + /// | | | | + /// | | | | + /// +---+ +---+ + /// + /// @note The input portals need to have compatible link loadaout, e.g. one + /// portal needs to have the *along normal* slot filled, and the + /// otherone one needs to have the *opposite normal* slot filled. If + /// portals share a filled slot, the function throws an exception. + /// @note This is a destructive operation on the portals involved + /// @param gctx The geometry context + /// @param aPortal The first portal + /// @param bPortal The second portal + /// @param logger The logger to push output to + static Portal fuse(const GeometryContext& gctx, Portal& aPortal, + Portal& bPortal, const Logger& logger = getDummyLogger()); + + /// Merge two adjacent portals with each other to produce a new portal that + /// encompasses both inputs. It is the complementary operation to the fusing + /// of portals. To be able to merge portals, the surfaces of their associated + /// links need to be *mergeable*, and the portal links need to be compatible. + /// This means that both portals need to have a link along the portal surface + /// normal, opposite the normal, or both. If the equipped links are opposite + /// relative to one another (e.g. one along one opposite), the function will + /// throw an exception. + /// + /// ^ ^ + /// | | + /// portal1| portal2| + /// +-------+-------+ +-------+-------+ + /// | | + | | + /// +-------+-------+ +-------+-------+ + /// | | + /// | | + /// v v + /// + /// @note This is a destructive operation on both portals, their + /// links will be moved to produce merged links, which can fail + /// if the portal links are not compatible + /// @param gctx The geometry context + /// @param aPortal The first portal + /// @param bPortal The second portal + /// @param direction The direction of the merge (e.g. along z) + /// @param logger The logger to push output to + static Portal merge(const GeometryContext& gctx, Portal& aPortal, + Portal& bPortal, BinningValue direction, + const Logger& logger = getDummyLogger()); + + /// Resolve the volume for a 3D position and a direction + /// The @p direction is used to select the right portal link, if it is set. + /// In case no link is found in the specified direction, a `nullptr` is + /// returned. + /// @param gctx The geometry context + /// @param position The 3D position + /// @param direction The direction + /// @return The target volume (can be `nullptr`) + Result resolveVolume(const GeometryContext& gctx, + const Vector3& position, + const Vector3& direction) const; + + /// Set a link on the portal into the slot specified by the direction. + /// @note The surface associated with @p link must be logically equivalent + /// to the one of the link that's already set on the portal. + /// @param gctx The geometry context + /// @param direction The direction + /// @param link The link to set + void setLink(const GeometryContext& gctx, Direction direction, + std::unique_ptr link); + + /// Helper function create a trivial portal link based on a surface. + /// @param gctx The geometry context + /// @param direction The direction of the link to create + /// @param surface The surface + /// @note The @p surface must be logically equivalent + /// to the one of the link that's already set on the portal. + /// @param volume The target volume + void setLink(const GeometryContext& gctx, Direction direction, + std::shared_ptr surface, TrackingVolume& volume); + + /// Get the link associated with the @p direction. Can be null if the associated link is unset. + /// @param direction The direction + /// @return The link (can be null) + const PortalLinkBase* getLink(Direction direction) const; + + /// Returns true if the portal is valid, that means it has at least one + /// non-null link associated.Portals can be in an invalid state after they get + /// merged or fused with other portals. + /// @return True if the portal is valid + bool isValid() const; + + /// Create and attach a trivial portal link to the empty slot of this portal + /// @param volume The target volume to connect to + void fill(TrackingVolume& volume); + + /// Access the portal surface that is shared between the two links + /// @return The portal surface + const RegularSurface& surface() const; + + private: + /// Helper to check surface equivalence without checking material status. This + /// is needed because we allow fusing portals with surfaces that are + /// equivalent but one of them has material while the other does not. The + /// normal surface comparison would determine these surfaces as not + /// equivalent. + /// @param gctx The geometry context + /// @param a The first surface + /// @param b The second surface + /// @return True if the surfaces are equivalent + static bool isSameSurface(const GeometryContext& gctx, const Surface& a, + const Surface& b); + + std::shared_ptr m_surface; + + std::unique_ptr m_alongNormal; + std::unique_ptr m_oppositeNormal; +}; + +} // namespace Acts diff --git a/Core/src/Geometry/CMakeLists.txt b/Core/src/Geometry/CMakeLists.txt index 00d9618f062..fa5d70c1c02 100644 --- a/Core/src/Geometry/CMakeLists.txt +++ b/Core/src/Geometry/CMakeLists.txt @@ -35,6 +35,7 @@ target_sources( Volume.cpp VolumeBounds.cpp CylinderVolumeStack.cpp + Portal.cpp GridPortalLink.cpp GridPortalLinkMerging.cpp TrivialPortalLink.cpp diff --git a/Core/src/Geometry/Portal.cpp b/Core/src/Geometry/Portal.cpp new file mode 100644 index 00000000000..4b1e65356af --- /dev/null +++ b/Core/src/Geometry/Portal.cpp @@ -0,0 +1,332 @@ +// This file is part of the Acts project. +// +// Copyright (C) 2024 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 http://mozilla.org/MPL/2.0/. + +#include "Acts/Geometry/Portal.hpp" + +#include "Acts/Definitions/Algebra.hpp" +#include "Acts/Geometry/GeometryContext.hpp" +#include "Acts/Geometry/PortalLinkBase.hpp" +#include "Acts/Geometry/TrivialPortalLink.hpp" +#include "Acts/Surfaces/RegularSurface.hpp" +#include "Acts/Utilities/BinningType.hpp" + +#include +#include +#include + +namespace Acts { + +const char* PortalMergingException::what() const noexcept { + return "Failure to merge portals"; +} + +const char* PortalFusingException::what() const noexcept { + return "Failure to fuse portals"; +} + +Portal::Portal(Direction direction, std::unique_ptr link) { + if (link == nullptr) { + throw std::invalid_argument("Link must not be null"); + } + + m_surface = link->surfacePtr(); + + if (direction == Direction::AlongNormal) { + m_alongNormal = std::move(link); + } else { + m_oppositeNormal = std::move(link); + } +} + +Portal::Portal(Direction direction, std::shared_ptr surface, + TrackingVolume& volume) + : Portal(direction, + std::make_unique(std::move(surface), volume)) {} + +Portal::Portal(const GeometryContext& gctx, + std::unique_ptr alongNormal, + std::unique_ptr oppositeNormal) { + if (alongNormal == nullptr && oppositeNormal == nullptr) { + throw std::invalid_argument("At least one link must be provided"); + } + + if (alongNormal != nullptr) { + setLink(gctx, Direction::AlongNormal, std::move(alongNormal)); + } + if (oppositeNormal != nullptr) { + setLink(gctx, Direction::OppositeNormal, std::move(oppositeNormal)); + } +} + +Portal::Portal(const GeometryContext& gctx, Arguments&& args) { + if (!args.alongNormal.surface && !args.oppositeNormal.surface) { + throw std::invalid_argument("At least one link must be provided"); + } + + if (args.alongNormal.surface) { + setLink(gctx, Direction::AlongNormal, + std::make_unique( + std::move(args.alongNormal.surface), *args.alongNormal.volume)); + } + if (args.oppositeNormal.surface) { + setLink(gctx, Direction::OppositeNormal, + std::make_unique( + std::move(args.oppositeNormal.surface), + *args.oppositeNormal.volume)); + } +} + +void Portal::setLink(const GeometryContext& gctx, Direction direction, + std::unique_ptr link) { + if (link == nullptr) { + throw std::invalid_argument("Link must not be null"); + } + + auto& target = + direction == Direction::AlongNormal ? m_alongNormal : m_oppositeNormal; + const auto& other = + direction == Direction::AlongNormal ? m_oppositeNormal : m_alongNormal; + + // check if surfaces are identical + if (m_surface != nullptr && + !isSameSurface(gctx, link->surface(), *m_surface)) { + throw PortalFusingException(); + } + + // check if they both have material but are not the same surface + if (m_surface != nullptr && (m_surface.get() != &link->surface()) && + link->surface().surfaceMaterial() != nullptr && + m_surface->surfaceMaterial() != nullptr) { + throw PortalFusingException(); + } + + target = std::move(link); + + if (other == nullptr) { + // We don't have an existing surface, take the one we just got + m_surface = target->surfacePtr(); + return; + } + + if (target->surface().surfaceMaterial() != nullptr) { + // new link has material: assign that to existing link + m_surface = target->surfacePtr(); + other->setSurface(m_surface); + } else { + // none have material, or the existing surface had material: assign the + // existing surface by convention + target->setSurface(m_surface); + } +} + +void Portal::setLink(const GeometryContext& gctx, Direction direction, + std::shared_ptr surface, + TrackingVolume& volume) { + setLink(gctx, direction, + std::make_unique(std::move(surface), volume)); +} + +const PortalLinkBase* Portal::getLink(Direction direction) const { + if (direction == Direction::AlongNormal) { + return m_alongNormal.get(); + } else { + return m_oppositeNormal.get(); + } +} + +Result Portal::resolveVolume( + const GeometryContext& gctx, const Vector3& position, + const Vector3& direction) const { + assert(m_surface != nullptr); + const Vector3 normal = m_surface->normal(gctx, position); + Direction side = Direction::fromScalarZeroAsPositive(normal.dot(direction)); + + const PortalLinkBase* link = side == Direction::AlongNormal + ? m_alongNormal.get() + : m_oppositeNormal.get(); + + if (link == nullptr) { + // no link is attached in this direction => this is the end of the world as + // we know it. (i feel fine) + return nullptr; + } else { + auto res = link->resolveVolume(gctx, position); + if (!res.ok()) { + return res.error(); + } + return *res; + } +} + +bool Portal::isValid() const { + return m_alongNormal != nullptr || m_oppositeNormal != nullptr; +} + +const RegularSurface& Portal::surface() const { + assert(m_surface != nullptr); + return *m_surface; +} + +Portal Portal::merge(const GeometryContext& gctx, Portal& aPortal, + Portal& bPortal, BinningValue direction, + const Logger& logger) { + ACTS_DEBUG("Merging to portals along " << direction); + + if (&aPortal == &bPortal) { + ACTS_ERROR("Cannot merge a portal with itself"); + throw PortalMergingException{}; + } + + if (aPortal.m_surface->surfaceMaterial() != nullptr || + bPortal.m_surface->surfaceMaterial() != nullptr) { + ACTS_ERROR("Cannot merge portals with material"); + throw PortalMergingException{}; + } + + std::unique_ptr mergedAlongNormal = nullptr; + std::unique_ptr mergedOppositeNormal = nullptr; + + bool aHasAlongNormal = aPortal.m_alongNormal != nullptr; + bool aHasOppositeNormal = aPortal.m_oppositeNormal != nullptr; + bool bHasAlongNormal = bPortal.m_alongNormal != nullptr; + bool bHasOppositeNormal = bPortal.m_oppositeNormal != nullptr; + + if (aHasAlongNormal != bHasAlongNormal || + aHasOppositeNormal != bHasOppositeNormal) { + ACTS_ERROR("Portals do not have the same links attached"); + throw PortalMergingException(); + } + + if (aPortal.m_alongNormal != nullptr) { + if (bPortal.m_alongNormal == nullptr) { + ACTS_ERROR( + "Portal A has link along normal, while b does not. This is not " + "supported"); + throw PortalMergingException(); + } + + ACTS_VERBOSE("Portals have links along normal, merging"); + mergedAlongNormal = PortalLinkBase::merge(std::move(aPortal.m_alongNormal), + std::move(bPortal.m_alongNormal), + direction, logger); + } + + if (aPortal.m_oppositeNormal != nullptr) { + if (bPortal.m_oppositeNormal == nullptr) { + ACTS_ERROR( + "Portal A has link opposite normal, while b does not. This is not " + "supported"); + throw PortalMergingException(); + } + + ACTS_VERBOSE("Portals have links opposite normal, merging"); + mergedOppositeNormal = PortalLinkBase::merge( + std::move(aPortal.m_oppositeNormal), + std::move(bPortal.m_oppositeNormal), direction, logger); + } + + aPortal.m_surface.reset(); + bPortal.m_surface.reset(); + return Portal{gctx, std::move(mergedAlongNormal), + std::move(mergedOppositeNormal)}; +} + +Portal Portal::fuse(const GeometryContext& gctx, Portal& aPortal, + Portal& bPortal, const Logger& logger) { + ACTS_DEBUG("Fusing two portals"); + if (&aPortal == &bPortal) { + ACTS_ERROR("Cannot merge a portal with itself"); + throw PortalMergingException{}; + } + + bool aHasAlongNormal = aPortal.m_alongNormal != nullptr; + bool aHasOppositeNormal = aPortal.m_oppositeNormal != nullptr; + bool bHasAlongNormal = bPortal.m_alongNormal != nullptr; + bool bHasOppositeNormal = bPortal.m_oppositeNormal != nullptr; + + if (aPortal.m_surface == nullptr || bPortal.m_surface == nullptr) { + ACTS_ERROR("Portals have no surface"); + throw PortalFusingException(); + } + + if (aPortal.m_surface->associatedDetectorElement() != nullptr || + bPortal.m_surface->associatedDetectorElement() != nullptr) { + ACTS_ERROR("Cannot fuse portals with detector elements"); + throw PortalFusingException(); + } + + if (!isSameSurface(gctx, *aPortal.m_surface, *bPortal.m_surface)) { + ACTS_ERROR("Portals have different surfaces"); + throw PortalFusingException(); + } + + if (aPortal.m_surface->surfaceMaterial() != nullptr && + bPortal.m_surface->surfaceMaterial() != nullptr) { + ACTS_ERROR("Cannot fuse portals if both have material"); + throw PortalFusingException(); + } + + if (aHasAlongNormal == bHasAlongNormal || + aHasOppositeNormal == bHasOppositeNormal) { + ACTS_ERROR("Portals have the same links attached"); + throw PortalFusingException(); + } + + aPortal.m_surface.reset(); + bPortal.m_surface.reset(); + if (aHasAlongNormal) { + ACTS_VERBOSE("Taking along normal from lhs, opposite normal from rhs"); + return Portal{gctx, std::move(aPortal.m_alongNormal), + std::move(bPortal.m_oppositeNormal)}; + } else { + ACTS_VERBOSE("Taking along normal from rhs, opposite normal from lhs"); + return Portal{gctx, std::move(bPortal.m_alongNormal), + std::move(aPortal.m_oppositeNormal)}; + } +} + +bool Portal::isSameSurface(const GeometryContext& gctx, const Surface& a, + const Surface& b) { + if (&a == &b) { + return true; + } + + if (a.type() != b.type()) { + return false; + } + + if (a.bounds() != b.bounds()) { + return false; + } + + if (!a.transform(gctx).isApprox(b.transform(gctx), + s_transformEquivalentTolerance)) { + return false; + } + + return true; +}; + +void Portal::fill(TrackingVolume& volume) { + if (m_alongNormal != nullptr && m_oppositeNormal != nullptr) { + throw std::logic_error{"Portal is already filled"}; + } + + if (m_surface == nullptr) { + throw std::logic_error{"Portal has no existing link set, can't fill"}; + } + + if (m_alongNormal == nullptr) { + m_alongNormal = std::make_unique(m_surface, volume); + } else { + assert(m_oppositeNormal == nullptr); + m_oppositeNormal = std::make_unique(m_surface, volume); + } +} + +} // namespace Acts diff --git a/Tests/UnitTests/Core/Detector/CMakeLists.txt b/Tests/UnitTests/Core/Detector/CMakeLists.txt index 5ad3c568db0..eff298adb84 100644 --- a/Tests/UnitTests/Core/Detector/CMakeLists.txt +++ b/Tests/UnitTests/Core/Detector/CMakeLists.txt @@ -21,7 +21,7 @@ add_unittest(ReferenceGenerators ReferenceGeneratorsTests.cpp) add_unittest(SupportSurfacesHelper SupportSurfacesHelperTests.cpp) add_unittest(ProtoDetector ProtoDetectorTests.cpp) add_unittest(ProtoBinning ProtoBinningTests.cpp) -add_unittest(Portal PortalTests.cpp) +add_unittest(DetectorPortal PortalTests.cpp) add_unittest(PortalGenerators PortalGeneratorsTests.cpp) add_unittest(VolumeStructureBuilder VolumeStructureBuilderTests.cpp) add_unittest(MultiWireStructureBuilder MultiWireStructureBuilderTests.cpp) diff --git a/Tests/UnitTests/Core/Geometry/CMakeLists.txt b/Tests/UnitTests/Core/Geometry/CMakeLists.txt index 3b058a882b4..1fa422bb0a6 100644 --- a/Tests/UnitTests/Core/Geometry/CMakeLists.txt +++ b/Tests/UnitTests/Core/Geometry/CMakeLists.txt @@ -33,3 +33,4 @@ add_unittest(VolumeBounds VolumeBoundsTests.cpp) add_unittest(Volume VolumeTests.cpp) add_unittest(CylinderVolumeStack CylinderVolumeStackTests.cpp) add_unittest(PortalLink PortalLinkTests.cpp) +add_unittest(Portal PortalTests.cpp) diff --git a/Tests/UnitTests/Core/Geometry/PortalTests.cpp b/Tests/UnitTests/Core/Geometry/PortalTests.cpp new file mode 100644 index 00000000000..b36086dc8c9 --- /dev/null +++ b/Tests/UnitTests/Core/Geometry/PortalTests.cpp @@ -0,0 +1,602 @@ +// This file is part of the Acts project. +// +// Copyright (C) 2024 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 http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include + +#include "Acts/Definitions/Units.hpp" +#include "Acts/Geometry/CylinderVolumeBounds.hpp" +#include "Acts/Geometry/GeometryContext.hpp" +#include "Acts/Geometry/GridPortalLink.hpp" +#include "Acts/Geometry/Portal.hpp" +#include "Acts/Geometry/TrackingVolume.hpp" +#include "Acts/Geometry/TrivialPortalLink.hpp" +#include "Acts/Material/HomogeneousSurfaceMaterial.hpp" +#include "Acts/Surfaces/CylinderSurface.hpp" +#include "Acts/Surfaces/DiscSurface.hpp" +#include "Acts/Surfaces/RadialBounds.hpp" +#include "Acts/Surfaces/Surface.hpp" +#include "Acts/Surfaces/SurfaceMergingException.hpp" +#include "Acts/Utilities/BinningType.hpp" +#include "Acts/Utilities/ThrowAssert.hpp" + +#include + +using namespace Acts::UnitLiterals; + +namespace Acts::Test { + +auto logger = Acts::getDefaultLogger("UnitTests", Acts::Logging::VERBOSE); + +struct Fixture { + Logging::Level m_level; + Fixture() { + m_level = Acts::Logging::getFailureThreshold(); + Acts::Logging::setFailureThreshold(Acts::Logging::FATAL); + } + + ~Fixture() { Acts::Logging::setFailureThreshold(m_level); } +}; + +std::shared_ptr makeDummyVolume() { + return std::make_shared( + Transform3::Identity(), + std::make_shared(30_mm, 40_mm, 100_mm)); +} + +GeometryContext gctx; + +BOOST_FIXTURE_TEST_SUITE(Geometry, Fixture) + +BOOST_AUTO_TEST_SUITE(Portals) +BOOST_AUTO_TEST_SUITE(Merging) + +BOOST_AUTO_TEST_CASE(Cylinder) { + auto vol1 = makeDummyVolume(); + vol1->setVolumeName("vol1"); + auto vol2 = makeDummyVolume(); + vol2->setVolumeName("vol2"); + + auto cyl1 = Surface::makeShared( + Transform3{Translation3{Vector3::UnitZ() * -100_mm}}, 50_mm, 100_mm); + + auto cyl2 = Surface::makeShared( + Transform3{Translation3{Vector3::UnitZ() * 100_mm}}, 50_mm, 100_mm); + + Portal portal1{Direction::AlongNormal, + std::make_unique(cyl1, *vol1)}; + BOOST_CHECK(portal1.isValid()); + + BOOST_CHECK_EQUAL( + portal1 + .resolveVolume(gctx, Vector3{50_mm, 0_mm, -100_mm}, Vector3::UnitX()) + .value(), + vol1.get()); + + BOOST_CHECK_EQUAL( + portal1 + .resolveVolume(gctx, Vector3{50_mm, 0_mm, -100_mm}, -Vector3::UnitX()) + .value(), + nullptr); + + Portal portal2{Direction::AlongNormal, cyl2, *vol2}; + BOOST_CHECK(portal2.isValid()); + + BOOST_CHECK_EQUAL( + portal2 + .resolveVolume(gctx, Vector3{50_mm, 0_mm, 100_mm}, -Vector3::UnitX()) + .value(), + nullptr); + + BOOST_CHECK_EQUAL( + portal2 + .resolveVolume(gctx, Vector3{50_mm, 0_mm, 100_mm}, Vector3::UnitX()) + .value(), + vol2.get()); + + Portal portal3{gctx, std::make_unique(cyl2, *vol2), + nullptr}; + BOOST_CHECK(portal3.isValid()); + + BOOST_CHECK_NE(portal3.getLink(Direction::AlongNormal), nullptr); + BOOST_CHECK_EQUAL(portal3.getLink(Direction::OppositeNormal), nullptr); + + Portal portal4{gctx, nullptr, + std::make_unique(cyl2, *vol2)}; + BOOST_CHECK(portal4.isValid()); + + BOOST_CHECK_EQUAL(portal4.getLink(Direction::AlongNormal), nullptr); + BOOST_CHECK_NE(portal4.getLink(Direction::OppositeNormal), nullptr); + + // Not mergeable because 1 has portal along but 4 has portal oppsite + // ^ + // | + // portal1| portal2 + // +-------+-------+ + +---------------+ + // | | | | + // +---------------+ +-------+-------+ + // | + // | + // v + BOOST_CHECK_THROW( + Portal::merge(gctx, portal1, portal4, BinningValue::binZ, *logger), + PortalMergingException); + + // This call leaves both valid because the exception is thrown before the + // pointers are moved + BOOST_CHECK(portal1.isValid()); + BOOST_CHECK(portal2.isValid()); + + BOOST_CHECK_EQUAL( + portal2.resolveVolume(gctx, Vector3{50_mm, 0_mm, 50_mm}, Vector3::UnitX()) + .value(), + vol2.get()); + + BOOST_CHECK_EQUAL( + portal2 + .resolveVolume(gctx, Vector3{50_mm, 0_mm, 50_mm}, -Vector3::UnitX()) + .value(), + nullptr); + + // Cannot merge in binRPhi + BOOST_CHECK_THROW( + Portal::merge(gctx, portal1, portal2, BinningValue::binRPhi, *logger), + SurfaceMergingException); + + // The call above leaves both portals invalid because the exception is thrown + // after the pointers are moved (during durface merging) + BOOST_CHECK(!portal1.isValid()); + BOOST_CHECK(!portal2.isValid()); + + // ^ ^ + // | | + // portal1| portal2| + // +-------+-------+ + +-------+-------+ + // | | | | + // +---------------+ +---------------+ + + // Reset portals to valid to continue + portal1 = Portal{gctx, {.alongNormal = {cyl1, *vol1}}}; + portal2 = Portal{gctx, {.alongNormal = {cyl2, *vol2}}}; + + Portal merged12 = + Portal::merge(gctx, portal1, portal2, BinningValue::binZ, *logger); + BOOST_CHECK_NE(merged12.getLink(Direction::AlongNormal), nullptr); + BOOST_CHECK_EQUAL(merged12.getLink(Direction::OppositeNormal), nullptr); + + auto grid12 = dynamic_cast( + merged12.getLink(Direction::AlongNormal)); + BOOST_REQUIRE_NE(grid12, nullptr); + grid12->printContents(std::cout); + + BOOST_CHECK_EQUAL( + merged12 + .resolveVolume(gctx, Vector3{50_mm, 0_mm, -50_mm}, Vector3::UnitX()) + .value(), + vol1.get()); + + BOOST_CHECK_EQUAL( + merged12 + .resolveVolume(gctx, Vector3{50_mm, 0_mm, 50_mm}, Vector3::UnitX()) + .value(), + vol2.get()); + + BOOST_CHECK_EQUAL( + merged12 + .resolveVolume(gctx, Vector3{50_mm, 0_mm, -50_mm}, -Vector3::UnitX()) + .value(), + nullptr); + + BOOST_CHECK_EQUAL( + merged12 + .resolveVolume(gctx, Vector3{50_mm, 0_mm, 50_mm}, -Vector3::UnitX()) + .value(), + nullptr); + + portal1 = Portal{gctx, {.alongNormal = {cyl1, *vol1}}}; + + // Can't merge with self + BOOST_CHECK_THROW( + Portal::merge(gctx, portal1, portal1, BinningValue::binZ, *logger), + PortalMergingException); + + // Can't merge because the surfaces are the same + portal1 = Portal{gctx, {.alongNormal = {cyl1, *vol1}}}; + portal2 = Portal{gctx, {.alongNormal = {cyl1, *vol2}}}; + BOOST_CHECK_THROW( + Portal::merge(gctx, portal1, portal2, BinningValue::binZ, *logger), + AssertionFailureException); + + // Can't merge because surface has material + auto material = + std::make_shared(MaterialSlab{}); // vacuum + cyl2->assignSurfaceMaterial(material); + portal1 = Portal{gctx, {.alongNormal = {cyl1, *vol1}}}; + portal2 = Portal{gctx, {.alongNormal = {cyl2, *vol2}}}; + BOOST_CHECK_THROW( + Portal::merge(gctx, portal1, portal2, BinningValue::binZ, *logger), + PortalMergingException); +} + +BOOST_AUTO_TEST_CASE(Disc) { + auto vol1 = makeDummyVolume(); + vol1->setVolumeName("vol1"); + auto vol2 = makeDummyVolume(); + vol2->setVolumeName("vol2"); + auto vol3 = makeDummyVolume(); + vol3->setVolumeName("vol3"); + auto vol4 = makeDummyVolume(); + vol4->setVolumeName("vol4"); + + auto disc1 = Surface::makeShared( + Transform3::Identity(), std::make_shared(50_mm, 100_mm)); + + auto disc2 = Surface::makeShared( + Transform3::Identity(), std::make_shared(100_mm, 150_mm)); + + Portal portal1{ + gctx, {.alongNormal = {disc1, *vol1}, .oppositeNormal = {disc1, *vol2}}}; + + Portal portal2{ + gctx, {.alongNormal = {disc2, *vol3}, .oppositeNormal = {disc2, *vol4}}}; + + BOOST_CHECK(portal1.isValid()); + BOOST_CHECK(portal2.isValid()); + + BOOST_CHECK_EQUAL( + portal1.resolveVolume(gctx, Vector3{55_mm, 0_mm, 0_mm}, Vector3::UnitZ()) + .value(), + vol1.get()); + BOOST_CHECK_EQUAL( + portal1.resolveVolume(gctx, Vector3{55_mm, 0_mm, 0_mm}, -Vector3::UnitZ()) + .value(), + vol2.get()); + + BOOST_CHECK_EQUAL( + portal2.resolveVolume(gctx, Vector3{105_mm, 0_mm, 0_mm}, Vector3::UnitZ()) + .value(), + vol3.get()); + BOOST_CHECK_EQUAL( + portal2 + .resolveVolume(gctx, Vector3{105_mm, 0_mm, 0_mm}, -Vector3::UnitZ()) + .value(), + vol4.get()); + + BOOST_CHECK_THROW( + Portal::merge(gctx, portal1, portal2, BinningValue::binZ, *logger), + AssertionFailureException); + + BOOST_CHECK(portal1.isValid()); + BOOST_CHECK(portal2.isValid()); + + BOOST_CHECK_THROW( + Portal::merge(gctx, portal1, portal2, BinningValue::binPhi, *logger), + SurfaceMergingException); + + // Portals not valid anymore because they were moved before the exception was + // thrown + BOOST_CHECK(!portal1.isValid()); + BOOST_CHECK(!portal2.isValid()); + + // recreate them + portal1 = Portal{ + gctx, {.alongNormal = {disc1, *vol1}, .oppositeNormal = {disc1, *vol2}}}; + + portal2 = Portal{ + gctx, {.alongNormal = {disc2, *vol3}, .oppositeNormal = {disc2, *vol4}}}; + + // ^ ^ + // | | + // portal1| portal2| + // +-------+-------+ +-------+-------+ + // | | + | | + // +-------+-------+ +-------+-------+ + // | | + // | | + // v v + Portal merged12 = + Portal::merge(gctx, portal1, portal2, BinningValue::binR, *logger); + + BOOST_CHECK_EQUAL( + merged12.resolveVolume(gctx, Vector3{55_mm, 0_mm, 0_mm}, Vector3::UnitZ()) + .value(), + vol1.get()); + BOOST_CHECK_EQUAL( + merged12 + .resolveVolume(gctx, Vector3{55_mm, 0_mm, 0_mm}, -Vector3::UnitZ()) + .value(), + vol2.get()); + + BOOST_CHECK_EQUAL( + merged12 + .resolveVolume(gctx, Vector3{105_mm, 0_mm, 0_mm}, Vector3::UnitZ()) + .value(), + vol3.get()); + BOOST_CHECK_EQUAL( + merged12 + .resolveVolume(gctx, Vector3{105_mm, 0_mm, 0_mm}, -Vector3::UnitZ()) + .value(), + vol4.get()); + + // Can't merge because surface has material + auto material = + std::make_shared(MaterialSlab{}); // vacuum + disc2->assignSurfaceMaterial(material); + portal1 = Portal{ + gctx, {.alongNormal = {disc1, *vol1}, .oppositeNormal = {disc1, *vol2}}}; + portal2 = Portal{ + gctx, {.alongNormal = {disc2, *vol3}, .oppositeNormal = {disc2, *vol4}}}; + BOOST_CHECK_THROW( + Portal::merge(gctx, portal1, portal2, BinningValue::binR, *logger), + PortalMergingException); +} + +BOOST_AUTO_TEST_SUITE_END() // Merging + +BOOST_AUTO_TEST_SUITE(Fusing) + +BOOST_AUTO_TEST_CASE(Separated) { + auto vol1 = makeDummyVolume(); + vol1->setVolumeName("vol1"); + auto vol2 = makeDummyVolume(); + vol2->setVolumeName("vol2"); + + auto cyl1 = Surface::makeShared(Transform3::Identity(), + 50_mm, 100_mm); + + auto cyl2 = Surface::makeShared(Transform3::Identity(), + 60_mm, 100_mm); + + Portal portal1{gctx, {.oppositeNormal = {cyl1, *vol1}}}; + + Portal portal2{gctx, {.alongNormal = {cyl2, *vol2}}}; + + BOOST_CHECK(portal1.isValid()); + BOOST_CHECK(portal2.isValid()); + + // Surfaces have a 10mm gap in r + BOOST_CHECK_THROW(Portal::fuse(gctx, portal1, portal2, *logger), + PortalFusingException); + + BOOST_CHECK(portal1.isValid()); + BOOST_CHECK(portal2.isValid()); + + // Same way can't set cyl2 as other link + BOOST_CHECK_THROW(portal1.setLink(gctx, Direction::AlongNormal, cyl2, *vol2), + PortalFusingException); + BOOST_CHECK_EQUAL(portal1.getLink(Direction::AlongNormal), nullptr); + + Portal portal1b{gctx, {.oppositeNormal = {cyl1, *vol1}}}; + BOOST_CHECK(portal1b.isValid()); + + // portal1 portal1b + // +---+ +---+ + // | | | | + // | | | | + // <----+ | + <----+ | + // | | | | + // | | | | + // +---+ +---+ + BOOST_CHECK_THROW(Portal::fuse(gctx, portal1, portal1b, *logger), + PortalFusingException); + BOOST_CHECK(portal1.isValid()); + BOOST_CHECK(portal1b.isValid()); + + auto disc1 = Surface::makeShared( + Transform3::Identity(), std::make_shared(50_mm, 100_mm)); + + auto disc2 = Surface::makeShared( + Transform3{Translation3{Vector3{0, 0, 5_mm}}}, + std::make_shared(50_mm, 100_mm)); + + // portal2 portal2b + // +---+ +---+ + // | | | | + // | | | | + // | +----> + | +----> + // | | | | + // | | | | + // +---+ +---+ + Portal portal2b{gctx, {.alongNormal = {disc2, *vol2}}}; + + BOOST_CHECK_THROW(Portal::fuse(gctx, portal2, portal2b, *logger), + PortalFusingException); + BOOST_CHECK(portal1.isValid()); + BOOST_CHECK(portal2.isValid()); + + // portal2 portal2c + // +---+ +---+ + // | | | | + // | | | | + // <----+ | + <----+ +----> + // | | | | + // | | | | + // +---+ +---+ + Portal portal2c{ + gctx, {.alongNormal = {disc2, *vol1}, .oppositeNormal = {disc2, *vol2}}}; + BOOST_CHECK(portal2c.isValid()); + + BOOST_CHECK_THROW(Portal::fuse(gctx, portal2, portal2c, *logger), + PortalFusingException); + BOOST_CHECK(portal2.isValid()); + BOOST_CHECK(portal2c.isValid()); +} + +BOOST_AUTO_TEST_CASE(Success) { + auto vol1 = makeDummyVolume(); + vol1->setVolumeName("vol1"); + auto vol2 = makeDummyVolume(); + vol2->setVolumeName("vol2"); + + auto cyl1 = Surface::makeShared(Transform3::Identity(), + 50_mm, 100_mm); + + auto cyl2 = Surface::makeShared(Transform3::Identity(), + 50_mm, 100_mm); + + BOOST_CHECK(*cyl1 == *cyl2); + + // portal1 portal2 + // +---+ +---+ + // | | | | + // | | | | + // <----+ | + | +----> + // | | | | + // | | | | + // +---+ +---+ + Portal portal1{gctx, {.oppositeNormal = {cyl1, *vol1}}}; + BOOST_CHECK_EQUAL(&portal1.getLink(Direction::OppositeNormal)->surface(), + cyl1.get()); + + Portal portal2{gctx, {.alongNormal = {cyl2, *vol2}}}; + + BOOST_CHECK(portal1.isValid()); + BOOST_CHECK(portal2.isValid()); + + Portal portal3 = Portal::fuse(gctx, portal1, portal2, *logger); + // Input portals get invalidated by the fuse + BOOST_CHECK(!portal1.isValid()); + BOOST_CHECK(!portal2.isValid()); + BOOST_CHECK(portal3.isValid()); + + BOOST_CHECK_EQUAL(portal3.surface().surfaceMaterial(), nullptr); + + // Portal surface is set to the one from "along", because it gets set first + BOOST_CHECK_EQUAL(&portal3.surface(), cyl2.get()); + // "Opposite" gets the already-set surface set as well + BOOST_CHECK_EQUAL(&portal3.getLink(Direction::OppositeNormal)->surface(), + cyl2.get()); +} + +BOOST_AUTO_TEST_CASE(Material) { + auto vol1 = makeDummyVolume(); + auto vol2 = makeDummyVolume(); + + auto cyl1 = Surface::makeShared(Transform3::Identity(), + 50_mm, 100_mm); + + auto cyl2 = Surface::makeShared(Transform3::Identity(), + 50_mm, 100_mm); + + // portal1 portal2 + // +---+ +---+ + // | | | | + // | | | | + // <----+ | + | +----> + // | | | | + // | | | | + // +---+ +---+ + Portal portal1{gctx, {.oppositeNormal = {cyl1, *vol1}}}; + Portal portal2{gctx, {.alongNormal = {cyl2, *vol2}}}; + + auto material = + std::make_shared(MaterialSlab{}); // vacuum + + cyl1->assignSurfaceMaterial(material); + + Portal portal12 = Portal::fuse(gctx, portal1, portal2, *logger); + + // cyl1 had material, so this surface needs to be retained + BOOST_CHECK_EQUAL(&portal12.surface(), cyl1.get()); + BOOST_CHECK_EQUAL(portal12.surface().surfaceMaterial(), material.get()); + + // Reset portals + portal1 = Portal{gctx, {.oppositeNormal = {cyl1, *vol1}}}; + portal2 = Portal{gctx, {.alongNormal = {cyl2, *vol2}}}; + cyl2->assignSurfaceMaterial(material); + + // Both have material, this should fail + BOOST_CHECK_THROW(Portal::fuse(gctx, portal1, portal2, *logger), + PortalFusingException); + // Portals should stay valid + BOOST_CHECK(portal1.isValid()); + BOOST_CHECK(portal2.isValid()); + + cyl1->assignSurfaceMaterial(nullptr); + + portal12 = Portal::fuse(gctx, portal1, portal2, *logger); + + // cyl2 had material, so this surface needs to be retained + BOOST_CHECK_EQUAL(&portal12.surface(), cyl2.get()); + BOOST_CHECK_EQUAL(portal12.surface().surfaceMaterial(), material.get()); +} + +BOOST_AUTO_TEST_SUITE_END() // Fusing + +BOOST_AUTO_TEST_CASE(Construction) { + auto vol1 = makeDummyVolume(); + + // Displaced surfaces fail + auto disc1 = Surface::makeShared( + Transform3::Identity(), std::make_shared(50_mm, 100_mm)); + + auto disc2 = Surface::makeShared( + Transform3{Translation3{Vector3{0, 0, 5_mm}}}, + std::make_shared(50_mm, 100_mm)); + + BOOST_CHECK_THROW(std::make_unique( + gctx, std::make_unique(disc1, *vol1), + std::make_unique(disc2, *vol1)), + PortalFusingException); + + BOOST_CHECK_THROW((Portal{gctx, nullptr, nullptr}), std::invalid_argument); + BOOST_CHECK_THROW(Portal(gctx, {}), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(InvalidConstruction) { + BOOST_CHECK_THROW(Portal(Direction::AlongNormal, nullptr), + std::invalid_argument); + + auto vol1 = makeDummyVolume(); + + BOOST_CHECK_THROW(Portal(Direction::AlongNormal, nullptr, *vol1), + std::invalid_argument); + + auto disc1 = Surface::makeShared( + Transform3::Identity(), std::make_shared(50_mm, 100_mm)); + Portal portal(Direction::AlongNormal, disc1, *vol1); + + BOOST_CHECK_THROW(portal.setLink(gctx, Direction::AlongNormal, nullptr), + std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(PortalFill) { + auto vol1 = makeDummyVolume(); + auto vol2 = makeDummyVolume(); + + auto cyl1 = Surface::makeShared(Transform3::Identity(), + 50_mm, 100_mm); + + Portal portal1{gctx, {.oppositeNormal = {cyl1, *vol1}}}; + Portal portal2{gctx, {.alongNormal = {cyl1, *vol2}}}; + + // Fuse these to make portal 1 and 2 empty + Portal::fuse(gctx, portal1, portal2, *logger); + + BOOST_CHECK_THROW(portal1.fill(*vol2), std::logic_error); + + portal1 = Portal{gctx, {.oppositeNormal = {cyl1, *vol1}}}; + portal2 = Portal{gctx, {.alongNormal = {cyl1, *vol2}}}; + + BOOST_CHECK_EQUAL(portal1.getLink(Direction::AlongNormal), nullptr); + BOOST_CHECK_NE(portal1.getLink(Direction::OppositeNormal), nullptr); + + portal1.fill(*vol2); + BOOST_CHECK_NE(portal1.getLink(Direction::AlongNormal), nullptr); + BOOST_CHECK_NE(portal1.getLink(Direction::OppositeNormal), nullptr); + + BOOST_CHECK_THROW(portal1.fill(*vol2), std::logic_error); +} + +BOOST_AUTO_TEST_SUITE_END() // Portals + +BOOST_AUTO_TEST_SUITE_END() // Geometry + +} // namespace Acts::Test