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