Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-10-01 08:02:22

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
0008 
0009 #include <boost/test/data/test_case.hpp>
0010 #include <boost/test/unit_test.hpp>
0011 
0012 #include "Acts/Definitions/Algebra.hpp"
0013 #include "Acts/Definitions/Tolerance.hpp"
0014 #include "Acts/Definitions/Units.hpp"
0015 #include "Acts/Geometry/Blueprint.hpp"
0016 #include "Acts/Geometry/BlueprintNode.hpp"
0017 #include "Acts/Geometry/ContainerBlueprintNode.hpp"
0018 #include "Acts/Geometry/CylinderPortalShell.hpp"
0019 #include "Acts/Geometry/CylinderVolumeBounds.hpp"
0020 #include "Acts/Geometry/GeometryContext.hpp"
0021 #include "Acts/Geometry/NavigationPolicyFactory.hpp"
0022 #include "Acts/Geometry/StaticBlueprintNode.hpp"
0023 #include "Acts/Geometry/TrackingVolume.hpp"
0024 #include "Acts/Navigation/CylinderNavigationPolicy.hpp"
0025 #include "Acts/Navigation/FrustumNavigationPolicy.hpp"
0026 #include "Acts/Navigation/INavigationPolicy.hpp"
0027 #include "Acts/Navigation/MultiNavigationPolicy.hpp"
0028 #include "Acts/Navigation/NavigationDelegate.hpp"
0029 #include "Acts/Navigation/NavigationStream.hpp"
0030 #include "Acts/Navigation/TryAllNavigationPolicy.hpp"
0031 #include "Acts/Utilities/Logger.hpp"
0032 
0033 #include <boost/algorithm/string/join.hpp>
0034 
0035 using namespace Acts;
0036 using namespace Acts::UnitLiterals;
0037 namespace bdata = boost::unit_test::data;
0038 
0039 namespace ActsTests {
0040 
0041 BOOST_AUTO_TEST_SUITE(NavigationSuite)
0042 
0043 auto gctx = GeometryContext::dangerouslyDefaultConstruct();
0044 auto logger = getDefaultLogger("NavigationPolicyTests", Logging::VERBOSE);
0045 
0046 struct APolicy : public INavigationPolicy {
0047   APolicy(const GeometryContext& /*gctx*/, const TrackingVolume& /*volume*/,
0048           const Logger& /*logger*/) {}
0049 
0050   void initializeCandidates(const GeometryContext& /*unused*/,
0051                             const NavigationArguments& /*unused*/,
0052                             NavigationPolicyState& /*unused*/,
0053                             AppendOnlyNavigationStream& /*unused*/,
0054                             const Logger& /*unused*/) const {
0055     const_cast<APolicy*>(this)->executed = true;
0056   }
0057 
0058   void connect(NavigationDelegate& delegate) const override {
0059     connectDefault<APolicy>(delegate);
0060   }
0061 
0062   bool executed = false;
0063 };
0064 
0065 struct BPolicy : public INavigationPolicy {
0066   struct Config {
0067     int value;
0068   };
0069 
0070   BPolicy(const GeometryContext& /*gctx*/, const TrackingVolume& /*volume*/,
0071           const Logger& /*logger*/, Config config)
0072       : m_config(config) {}
0073 
0074   void connect(NavigationDelegate& delegate) const override {
0075     connectDefault<BPolicy>(delegate);
0076   }
0077 
0078   void initializeCandidates(const GeometryContext& /*unused*/,
0079                             const NavigationArguments& /*unused*/,
0080                             NavigationPolicyState& /*unused*/,
0081                             AppendOnlyNavigationStream& /*unused*/,
0082                             const Logger& /*unused*/) const {
0083     const_cast<BPolicy*>(this)->executed = true;
0084     const_cast<BPolicy*>(this)->value = m_config.value;
0085   }
0086 
0087   bool executed = false;
0088   int value = 0;
0089 
0090   Config m_config;
0091 };
0092 
0093 BOOST_AUTO_TEST_CASE(DirectTest) {
0094   TrackingVolume volume{
0095       Transform3::Identity(),
0096       std::make_shared<CylinderVolumeBounds>(250_mm, 400_mm, 310_mm),
0097       "PixelLayer3"};
0098 
0099   MultiNavigationPolicy policy{
0100       std::make_unique<APolicy>(gctx, volume, *logger),
0101       std::make_unique<BPolicy>(gctx, volume, *logger,
0102                                 BPolicy::Config{.value = 4242})};
0103 
0104   NavigationDelegate delegate;
0105   policy.connect(delegate);
0106 
0107   NavigationStream main;
0108   AppendOnlyNavigationStream stream{main};
0109   NavigationArguments args{.position = Vector3::Zero(),
0110                            .direction = Vector3::Zero()};
0111   NavigationPolicyStateManager stateManager;
0112   stateManager.pushState<MultiNavigationPolicy::State>();
0113   policy.createState(gctx, args, stateManager, *logger);
0114   auto policyState = stateManager.currentState();
0115   delegate(gctx, args, policyState, stream, *logger);
0116 
0117   BOOST_REQUIRE_EQUAL(policy.policies().size(), 2);
0118   const auto& policyA = dynamic_cast<const APolicy&>(*policy.policies()[0]);
0119   const auto& policyB = dynamic_cast<const BPolicy&>(*policy.policies()[1]);
0120 
0121   BOOST_CHECK(policyA.executed);
0122   BOOST_CHECK(policyB.executed);
0123   BOOST_CHECK_EQUAL(policyB.value, 4242);
0124 }
0125 
0126 BOOST_AUTO_TEST_CASE(FactoryTest) {
0127   TrackingVolume volume{
0128       Transform3::Identity(),
0129       std::make_shared<CylinderVolumeBounds>(250_mm, 400_mm, 310_mm),
0130       "PixelLayer3"};
0131 
0132   BPolicy::Config config{.value = 42};
0133 
0134   std::function<std::unique_ptr<INavigationPolicy>(
0135       const GeometryContext&, const TrackingVolume&, const Logger&)>
0136       factory = NavigationPolicyFactory{}
0137                     .add<APolicy>()         // no arguments
0138                     .add<BPolicy>(config);  // config struct as argument
0139 
0140   auto policyBase = factory(gctx, volume, *logger);
0141   auto policyBase2 = factory(gctx, volume, *logger);
0142 
0143   auto& policy = dynamic_cast<MultiNavigationPolicy&>(*policyBase);
0144 
0145   NavigationDelegate delegate;
0146   policy.connect(delegate);
0147 
0148   NavigationStream main;
0149   AppendOnlyNavigationStream stream{main};
0150   NavigationArguments args{.position = Vector3::Zero(),
0151                            .direction = Vector3::Zero()};
0152   NavigationPolicyStateManager stateManager;
0153   stateManager.pushState<MultiNavigationPolicy::State>();
0154   policy.createState(gctx, args, stateManager, *logger);
0155   auto policyState = stateManager.currentState();
0156   delegate(gctx, args, policyState, stream, *logger);
0157 
0158   BOOST_REQUIRE_EQUAL(policy.policies().size(), 2);
0159   const auto& policyA = dynamic_cast<const APolicy&>(*policy.policies()[0]);
0160   const auto& policyB = dynamic_cast<const BPolicy&>(*policy.policies()[1]);
0161 
0162   BOOST_CHECK(policyA.executed);
0163   BOOST_CHECK(policyB.executed);
0164   BOOST_CHECK_EQUAL(policyB.value, 42);
0165 
0166   auto& policy2 = dynamic_cast<MultiNavigationPolicy&>(*policyBase2);
0167 
0168   NavigationDelegate delegate2;
0169   policyBase2->connect(delegate2);
0170 
0171   NavigationPolicyStateManager stateManager2;
0172   stateManager2.pushState<MultiNavigationPolicy::State>();
0173   policy2.createState(gctx, args, stateManager2, *logger);
0174   auto policyState2 = stateManager2.currentState();
0175   delegate2(gctx, args, policyState2, stream, *logger);
0176 
0177   BOOST_REQUIRE_EQUAL(policy2.policies().size(), 2);
0178   const auto& policy2A = dynamic_cast<const APolicy&>(*policy2.policies()[0]);
0179   const auto& policy2B = dynamic_cast<const BPolicy&>(*policy2.policies()[1]);
0180 
0181   BOOST_CHECK(policy2A.executed);
0182   BOOST_CHECK(policy2B.executed);
0183   BOOST_CHECK_EQUAL(policy2B.value, 42);
0184 }
0185 
0186 BOOST_AUTO_TEST_CASE(AsUniquePtrTest) {
0187   TrackingVolume volume{
0188       Transform3::Identity(),
0189       std::make_shared<CylinderVolumeBounds>(250_mm, 400_mm, 310_mm),
0190       "PixelLayer3"};
0191 
0192   std::unique_ptr<NavigationPolicyFactory> factory =
0193       NavigationPolicyFactory{}.add<APolicy>().asUniquePtr();
0194 
0195   auto policyBase = factory->build(gctx, volume, *logger);
0196   auto& policy = dynamic_cast<MultiNavigationPolicy&>(*policyBase);
0197 
0198   NavigationDelegate delegate;
0199   policyBase->connect(delegate);
0200 
0201   NavigationStream main;
0202   AppendOnlyNavigationStream stream{main};
0203   NavigationArguments args{.position = Vector3::Zero(),
0204                            .direction = Vector3::Zero()};
0205   NavigationPolicyStateManager stateManager;
0206   stateManager.pushState<MultiNavigationPolicy::State>();
0207   policy.createState(gctx, args, stateManager, *logger);
0208   auto policyState = stateManager.currentState();
0209   delegate(gctx, args, policyState, stream, *logger);
0210 
0211   BOOST_REQUIRE_EQUAL(policy.policies().size(), 1);
0212   BOOST_CHECK(dynamic_cast<const APolicy&>(*policy.policies()[0]).executed);
0213 }
0214 
0215 struct CPolicy : public INavigationPolicy {};
0216 
0217 template <typename T>
0218 struct CPolicySpecialized : public CPolicy {
0219   struct Config {
0220     T value;
0221   };
0222 
0223   CPolicySpecialized(const TrackingVolume& /*volume*/, Config config)
0224       : m_config(config) {}
0225 
0226   void connect(NavigationDelegate& delegate) const override {
0227     connectDefault<CPolicySpecialized<T>>(delegate);
0228   }
0229 
0230   void initializeCandidates(const GeometryContext& /*unused*/,
0231                             const NavigationArguments& /*unused*/,
0232                             NavigationPolicyState& /*unused*/,
0233                             AppendOnlyNavigationStream& /*stream*/,
0234                             const Logger& /*logger*/) const {
0235     auto* self = const_cast<CPolicySpecialized<int>*>(this);
0236     self->executed = true;
0237     self->value = m_config.value;
0238   }
0239 
0240   bool executed = false;
0241   int value = 0;
0242 
0243   Config m_config;
0244 };
0245 
0246 struct IsolatedConfig {
0247   int value;
0248 };
0249 
0250 std::unique_ptr<INavigationPolicy> makeCPolicy(const GeometryContext& /*gctx*/,
0251                                                const TrackingVolume& volume,
0252                                                const Logger& /*logger*/,
0253                                                IsolatedConfig config) {
0254   // I can do arbitrary stuff here, including selecting the concrete policy type
0255   // at runtime, since we return a unique_ptr to the base class.
0256   CPolicySpecialized<int>::Config config2{.value = config.value};
0257   return std::make_unique<CPolicySpecialized<int>>(volume, config2);
0258 }
0259 
0260 BOOST_AUTO_TEST_CASE(IsolatedFactory) {
0261   TrackingVolume volume{
0262       Transform3::Identity(),
0263       std::make_shared<CylinderVolumeBounds>(250_mm, 400_mm, 310_mm),
0264       "PixelLayer3"};
0265 
0266   IsolatedConfig config{.value = 44};
0267   auto factory =
0268       NavigationPolicyFactory{}.add<APolicy>().add(makeCPolicy, config);
0269 
0270   auto factory2 =
0271       NavigationPolicyFactory{}.add(makeCPolicy, config).add<APolicy>();
0272 
0273   auto policyBase = factory(gctx, volume, *logger);
0274   auto& policy = dynamic_cast<MultiNavigationPolicy&>(*policyBase);
0275 
0276   NavigationDelegate delegate;
0277   policyBase->connect(delegate);
0278 
0279   NavigationStream main;
0280   AppendOnlyNavigationStream stream{main};
0281   NavigationArguments args{.position = Vector3::Zero(),
0282                            .direction = Vector3::Zero()};
0283   NavigationPolicyStateManager stateManager;
0284   stateManager.pushState<MultiNavigationPolicy::State>();
0285   policy.createState(gctx, args, stateManager, *logger);
0286   auto policyState = stateManager.currentState();
0287   delegate(gctx, args, policyState, stream, *logger);
0288 
0289   BOOST_REQUIRE_EQUAL(policy.policies().size(), 2);
0290 
0291   const auto& policyA = dynamic_cast<const APolicy&>(*policy.policies()[0]);
0292   const auto& cPolicy =
0293       dynamic_cast<const CPolicySpecialized<int>&>(*policy.policies()[1]);
0294 
0295   BOOST_CHECK(policyA.executed);
0296   BOOST_CHECK(cPolicy.executed);
0297   BOOST_CHECK_EQUAL(cPolicy.value, 44);
0298 }
0299 
0300 namespace {
0301 
0302 std::vector<const Portal*> getTruth(const Vector3& position,
0303                                     const Vector3& direction,
0304                                     const Transform3& transform,
0305                                     const TrackingVolume& cylVolume,
0306                                     SingleCylinderPortalShell& shell,
0307                                     const Logger& logger, bool posOnly = true) {
0308   Vector3 gpos = transform * position;
0309   Vector3 gdir = transform.linear() * direction;
0310   TryAllNavigationPolicy tryAll(gctx, cylVolume, logger);
0311   NavigationArguments args{.position = gpos, .direction = gdir};
0312   NavigationStream main;
0313   AppendOnlyNavigationStream stream{main};
0314   auto gctx = GeometryContext::dangerouslyDefaultConstruct();
0315   NavigationPolicyStateManager stateManager;
0316   tryAll.createState(gctx, args, stateManager, logger);
0317   auto policyState = stateManager.currentState();
0318   tryAll.initializeCandidates(gctx, args, policyState, stream, logger);
0319   main.initialize(gctx, {gpos, gdir}, logger());
0320   std::vector<const Portal*> portals;
0321   for (auto& candidate : main.candidates()) {
0322     if (!candidate.intersection().isValid()) {
0323       continue;
0324     }
0325 
0326     if (main.candidates().size() > 1 && posOnly &&
0327         !detail::checkPathLength(candidate.intersection().pathLength(),
0328                                  s_onSurfaceTolerance,
0329                                  std::numeric_limits<double>::max(), logger)) {
0330       continue;
0331     }
0332 
0333     portals.push_back(&candidate.portal());
0334   }
0335 
0336   // Find portal types
0337   const Portal* outerCylinder = nullptr;
0338   const Portal* innerCylinder = nullptr;
0339   const Portal* positiveDisc = nullptr;
0340   const Portal* negativeDisc = nullptr;
0341 
0342   for (const Portal* portal : portals) {
0343     if (portal ==
0344         shell.portal(CylinderVolumeBounds::Face::OuterCylinder).get()) {
0345       outerCylinder = portal;
0346     } else if (portal ==
0347                shell.portal(CylinderVolumeBounds::Face::InnerCylinder).get()) {
0348       innerCylinder = portal;
0349     } else if (portal ==
0350                shell.portal(CylinderVolumeBounds::Face::PositiveDisc).get()) {
0351       positiveDisc = portal;
0352     } else if (portal ==
0353                shell.portal(CylinderVolumeBounds::Face::NegativeDisc).get()) {
0354       negativeDisc = portal;
0355     }
0356   }
0357 
0358   // Build new filtered list
0359   std::vector<const Portal*> filteredPortals;
0360 
0361   // Apply existing filter: remove outer cylinder if both inner and outer are
0362   // present
0363   if ((innerCylinder != nullptr) && (outerCylinder != nullptr)) {
0364     // Keep inner, discard outer
0365     filteredPortals.push_back(innerCylinder);
0366   } else {
0367     // Keep whichever one exists
0368     if (innerCylinder != nullptr) {
0369       filteredPortals.push_back(innerCylinder);
0370     }
0371     if (outerCylinder != nullptr) {
0372       filteredPortals.push_back(outerCylinder);
0373     }
0374   }
0375 
0376   // Apply CylinderNavigationPolicy optimization: if inner cylinder is present,
0377   // assume any discs are blocked by it (based on failing test pattern)
0378   if (innerCylinder == nullptr) {
0379     // No inner cylinder, so discs are not blocked
0380     if (positiveDisc != nullptr) {
0381       filteredPortals.push_back(positiveDisc);
0382     }
0383     if (negativeDisc != nullptr) {
0384       filteredPortals.push_back(negativeDisc);
0385     }
0386   }
0387   // If inner cylinder is present, discs are omitted (blocked)
0388 
0389   return filteredPortals;
0390 }
0391 
0392 std::vector<const Portal*> getSmart(const Vector3& position,
0393                                     const Vector3& direction,
0394                                     const Transform3& transform,
0395                                     CylinderNavigationPolicy& policy) {
0396   Vector3 gpos = transform * position;
0397   Vector3 gdir = transform.linear() * direction;
0398   NavigationArguments args{.position = gpos, .direction = gdir};
0399   NavigationStream main;
0400   auto gctx = GeometryContext::dangerouslyDefaultConstruct();
0401   AppendOnlyNavigationStream stream{main};
0402   NavigationPolicyStateManager stateManager;
0403   policy.createState(gctx, args, stateManager, *logger);
0404   auto policyState = stateManager.currentState();
0405   policy.initializeCandidates(gctx, args, policyState, stream, *logger);
0406 
0407   std::vector<const Portal*> portals;
0408   // We don't filter here, because we want to test the candidates as they come
0409   // out of the policy
0410   for (auto& candidate : main.candidates()) {
0411     portals.push_back(&candidate.portal());
0412   }
0413   return portals;
0414 }
0415 
0416 void checkEqual(const std::vector<const Portal*>& exp,
0417                 const std::vector<const Portal*>& act,
0418                 SingleCylinderPortalShell& shell) {
0419   auto which = [&](const Portal* p) -> std::string {
0420     if (p == shell.portal(CylinderVolumeBounds::Face::InnerCylinder).get()) {
0421       return "InnerCylinder";
0422     }
0423     if (p == shell.portal(CylinderVolumeBounds::Face::OuterCylinder).get()) {
0424       return "OuterCylinder";
0425     }
0426     if (p == shell.portal(CylinderVolumeBounds::Face::PositiveDisc).get()) {
0427       return "PositiveDisc";
0428     }
0429     if (p == shell.portal(CylinderVolumeBounds::Face::NegativeDisc).get()) {
0430       return "NegativeDisc";
0431     }
0432     BOOST_FAIL("Unknown portal");
0433     return "";  // unreachable
0434   };
0435 
0436   std::set<const Portal*> expSet;
0437   std::set<const Portal*> actSet;
0438 
0439   std::ranges::copy(exp, std::inserter(expSet, expSet.begin()));
0440   std::ranges::copy(act, std::inserter(actSet, actSet.begin()));
0441 
0442   if (expSet != actSet) {
0443     BOOST_ERROR([&]() -> std::string {
0444       std::vector<std::string> exps;
0445       for (auto& p : exp) {
0446         exps.push_back(which(p));
0447       }
0448       std::vector<std::string> acts;
0449       for (auto& p : act) {
0450         acts.push_back(which(p));
0451       }
0452       return "[" + boost::algorithm::join(exps, ", ") + "] != [" +
0453              boost::algorithm::join(acts, ", ") + "]";
0454     }());
0455   }
0456 }
0457 
0458 }  // namespace
0459 
0460 BOOST_DATA_TEST_CASE(
0461     CylinderPolicyTest,
0462     (bdata::xrange(-135, 180, 45) *
0463      bdata::make(Vector3{0_mm, 0_mm, 0_mm}, Vector3{20_mm, 0_mm, 0_mm},
0464                  Vector3{0_mm, 20_mm, 0_mm}, Vector3{20_mm, 20_mm, 0_mm},
0465                  Vector3{0_mm, 0_mm, 20_mm})),
0466     angle, offset) {
0467   using enum CylinderVolumeBounds::Face;
0468 
0469   Transform3 transform = Transform3::Identity();
0470   transform *= AngleAxis3{angle * 1_degree, Vector3::UnitX()};
0471   transform *= Translation3{offset};
0472   auto cylBounds =
0473       std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 300_mm);
0474   auto cylVolume =
0475       std::make_shared<TrackingVolume>(transform, cylBounds, "CylinderVolume");
0476   SingleCylinderPortalShell shell{gctx, *cylVolume};
0477   shell.applyToVolume();
0478 
0479   {
0480     Vector3 position = Vector3::UnitX() * 150_mm;
0481     Vector3 direction = Vector3::UnitZ();
0482 
0483     auto exp =
0484         getTruth(position, direction, transform, *cylVolume, shell, *logger);
0485 
0486     BOOST_CHECK(exp.size() == 1);
0487     BOOST_CHECK(exp.at(0) == shell.portal(PositiveDisc).get());
0488 
0489     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0490     auto act = getSmart(position, direction, transform, policy);
0491     checkEqual(exp, act, shell);
0492   }
0493 
0494   {
0495     Vector3 position = Vector3::UnitX() * 150_mm;
0496     Vector3 direction = Vector3{1, 1, 0}.normalized();
0497 
0498     auto exp =
0499         getTruth(position, direction, transform, *cylVolume, shell, *logger);
0500 
0501     BOOST_CHECK(exp.size() == 1);
0502     BOOST_CHECK(exp.at(0) == shell.portal(OuterCylinder).get());
0503 
0504     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0505     auto act = getSmart(position, direction, transform, policy);
0506     checkEqual(exp, act, shell);
0507   }
0508 
0509   {
0510     Vector3 position = Vector3::UnitX() * 150_mm;
0511     Vector3 direction = Vector3{-1, 0, 0}.normalized();
0512 
0513     auto exp =
0514         getTruth(position, direction, transform, *cylVolume, shell, *logger);
0515 
0516     BOOST_CHECK(exp.size() == 1);
0517     BOOST_CHECK(exp.at(0) == shell.portal(InnerCylinder).get());
0518 
0519     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0520     auto act = getSmart(position, direction, transform, policy);
0521     checkEqual(exp, act, shell);
0522   }
0523 
0524   {
0525     Vector3 position = Vector3::UnitX() * 150_mm;
0526     Vector3 direction = -Vector3::UnitZ();
0527 
0528     auto exp =
0529         getTruth(position, direction, transform, *cylVolume, shell, *logger);
0530 
0531     BOOST_CHECK(exp.size() == 1);
0532     BOOST_CHECK(exp.at(0) == shell.portal(NegativeDisc).get());
0533 
0534     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0535     auto act = getSmart(position, direction, transform, policy);
0536     checkEqual(exp, act, shell);
0537   }
0538 
0539   {
0540     Vector3 position{50, -200, 0};
0541     Vector3 direction = Vector3{0, 1.5, 1}.normalized();
0542 
0543     auto exp =
0544         getTruth(position, direction, transform, *cylVolume, shell, *logger);
0545 
0546     BOOST_CHECK(exp.size() == 1);
0547     BOOST_CHECK(exp.at(0) == shell.portal(InnerCylinder).get());
0548 
0549     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0550     auto act = getSmart(position, direction, transform, policy);
0551     checkEqual(exp, act, shell);
0552   }
0553 
0554   {
0555     Vector3 position{50, -200, 0};
0556     Vector3 direction = Vector3{0, 1.2, 1}.normalized();
0557 
0558     auto exp =
0559         getTruth(position, direction, transform, *cylVolume, shell, *logger);
0560 
0561     BOOST_CHECK(exp.size() == 1);
0562     BOOST_CHECK(exp.at(0) == shell.portal(InnerCylinder).get());
0563 
0564     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0565     auto act = getSmart(position, direction, transform, policy);
0566     checkEqual(exp, act, shell);
0567   }
0568 
0569   {
0570     Vector3 position{50, -200, 0};
0571     Vector3 direction = Vector3{0, 0.9, 1}.normalized();
0572 
0573     auto exp =
0574         getTruth(position, direction, transform, *cylVolume, shell, *logger);
0575 
0576     BOOST_CHECK(exp.size() == 1);
0577     BOOST_CHECK(exp.at(0) == shell.portal(InnerCylinder).get());
0578 
0579     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0580     auto act = getSmart(position, direction, transform, policy);
0581     checkEqual(exp, act, shell);
0582   }
0583 
0584   {
0585     Vector3 position{20, -200, 0};
0586     Vector3 direction = Vector3{0.45, 0.9, 1}.normalized();
0587 
0588     auto exp =
0589         getTruth(position, direction, transform, *cylVolume, shell, *logger);
0590 
0591     BOOST_CHECK(exp.size() == 1);
0592     BOOST_CHECK(exp.at(0) == shell.portal(PositiveDisc).get());
0593 
0594     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0595     auto act = getSmart(position, direction, transform, policy);
0596     checkEqual(exp, act, shell);
0597   }
0598 
0599   {
0600     Vector3 position{20, -200, 0};
0601     Vector3 direction = Vector3{0.45, 0.9, -1}.normalized();
0602 
0603     auto exp =
0604         getTruth(position, direction, transform, *cylVolume, shell, *logger);
0605 
0606     BOOST_CHECK(exp.size() == 1);
0607     BOOST_CHECK(exp.at(0) == shell.portal(NegativeDisc).get());
0608 
0609     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0610     auto act = getSmart(position, direction, transform, policy);
0611     checkEqual(exp, act, shell);
0612   }
0613 
0614   {
0615     Vector3 position{400 * std::cos(std::numbers::pi / 4),
0616                      400 * std::sin(std::numbers::pi / 4), 0};
0617     Vector3 direction = Vector3{0.45, -0.9, -0.1}.normalized();
0618 
0619     // We're sitting ON the outer cylinder here and missing the disc and inner
0620     // cylinder: need to return the outer cylinder
0621     auto exp = getTruth(position, direction, transform, *cylVolume, shell,
0622                         *logger, false);
0623 
0624     BOOST_CHECK(exp.size() == 1);
0625     BOOST_CHECK(exp.at(0) == shell.portal(OuterCylinder).get());
0626 
0627     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0628     auto act = getSmart(position, direction, transform, policy);
0629     checkEqual(exp, act, shell);
0630   }
0631 
0632   {
0633     Vector3 position{400 * std::cos(std::numbers::pi / 4),
0634                      400 * std::sin(std::numbers::pi / 4), 0};
0635     Vector3 direction = Vector3{-0.3, -0.9, -0.1}.normalized();
0636 
0637     // We're sitting ON the outer cylinder here and missing the disc and inner
0638     // cylinder: need to return the outer cylinder
0639     auto exp = getTruth(position, direction, transform, *cylVolume, shell,
0640                         *logger, false);
0641 
0642     BOOST_CHECK(exp.size() == 1);
0643     BOOST_CHECK(exp.at(0) == shell.portal(OuterCylinder).get());
0644 
0645     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0646     auto act = getSmart(position, direction, transform, policy);
0647     checkEqual(exp, act, shell);
0648   }
0649 
0650   {
0651     Vector3 position{100 * std::cos(std::numbers::pi / 4),
0652                      100 * std::sin(std::numbers::pi / 4), 0};
0653     double dangle = 0.1;
0654     Vector3 direction = Vector3{std::cos(dangle), std::sin(dangle), 0.01};
0655 
0656     // We're sitting ON the Inner cylinder here and  pointing outwards
0657     auto exp =
0658         getTruth(position, direction, transform, *cylVolume, shell, *logger);
0659 
0660     BOOST_CHECK(exp.size() == 1);
0661     BOOST_CHECK(exp.at(0) == shell.portal(OuterCylinder).get());
0662 
0663     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0664     auto act = getSmart(position, direction, transform, policy);
0665     checkEqual(exp, act, shell);
0666   }
0667 
0668   {
0669     Vector3 position{200 * std::cos(std::numbers::pi / 4),
0670                      200 * std::sin(std::numbers::pi / 4), 0};
0671     Vector3 target{150 * std::cos(std::numbers::pi * 5 / 4),
0672                    150 * std::sin(std::numbers::pi * 5 / 4), 300};
0673     Vector3 direction = (target - position).normalized();
0674 
0675     auto exp =
0676         getTruth(position, direction, transform, *cylVolume, shell, *logger);
0677 
0678     BOOST_CHECK_EQUAL(exp.size(), 1);
0679     BOOST_CHECK_EQUAL(exp.at(0), shell.portal(InnerCylinder).get());
0680 
0681     CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0682     auto act = getSmart(position, direction, transform, policy);
0683     checkEqual(exp, act, shell);
0684   }
0685 }
0686 
0687 namespace {
0688 
0689 std::mt19937 engine;
0690 
0691 unsigned long seed() {
0692   static unsigned long s = 42;
0693   return s++;
0694 }
0695 
0696 std::uniform_real_distribution<double> rDistOffBoundary{
0697     100 + 2 * s_onSurfaceTolerance, 400 - 2 * s_onSurfaceTolerance};
0698 std::uniform_real_distribution<double> zDistOffBoundary{
0699     -300_mm + 2 * s_onSurfaceTolerance, 300_mm - 2 * s_onSurfaceTolerance};
0700 std::uniform_real_distribution<double> phiDist{-std::numbers::pi,
0701                                                std::numbers::pi};
0702 std::uniform_real_distribution<double> thetaDist{0, std::numbers::pi};
0703 
0704 }  // namespace
0705 
0706 BOOST_DATA_TEST_CASE(
0707     CylinderPolicyTestOffBoundary,
0708     bdata::random((bdata::engine = engine, bdata::seed = seed(),
0709                    bdata::distribution = rDistOffBoundary)) ^
0710         bdata::random((bdata::engine = engine, bdata::seed = seed(),
0711                        bdata::distribution = zDistOffBoundary)) ^
0712         bdata::random((bdata::engine = engine, bdata::seed = seed(),
0713                        bdata::distribution = phiDist)) ^
0714         bdata::random((bdata::engine = engine, bdata::seed = seed(),
0715                        bdata::distribution = phiDist)) ^
0716         bdata::random((bdata::engine = engine, bdata::seed = seed(),
0717                        bdata::distribution = thetaDist)) ^
0718         bdata::xrange(100),
0719     r, z, phiPos, phiDir, theta, index) {
0720   static_cast<void>(index);
0721 
0722   Transform3 transform = Transform3::Identity();
0723   auto cylBounds =
0724       std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 300_mm);
0725   auto cylVolume =
0726       std::make_shared<TrackingVolume>(transform, cylBounds, "CylinderVolume");
0727   SingleCylinderPortalShell shell{gctx, *cylVolume};
0728   shell.applyToVolume();
0729 
0730   Vector3 position{r * std::cos(phiPos), r * std::sin(phiPos), z};
0731   Vector3 direction{std::sin(theta) * std::cos(phiDir),
0732                     std::sin(theta) * std::sin(phiDir), std::cos(theta)};
0733 
0734   BOOST_CHECK(cylBounds->inside(position));
0735 
0736   auto exp =
0737       getTruth(position, direction, transform, *cylVolume, shell, *logger);
0738 
0739   CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0740   auto act = getSmart(position, direction, transform, policy);
0741   checkEqual(exp, act, shell);
0742 }
0743 
0744 BOOST_DATA_TEST_CASE(
0745     CylinderPolicyTestOnRBoundary,
0746     bdata::make(100, 400) *
0747         (bdata::random((bdata::engine = engine, bdata::seed = seed(),
0748                         bdata::distribution = zDistOffBoundary)) ^
0749          bdata::random((bdata::engine = engine, bdata::seed = seed(),
0750                         bdata::distribution = phiDist)) ^
0751          bdata::random((bdata::engine = engine, bdata::seed = seed(),
0752                         bdata::distribution = phiDist)) ^
0753          bdata::random((bdata::engine = engine, bdata::seed = seed(),
0754                         bdata::distribution = zDistOffBoundary)) ^
0755          bdata::xrange(100)),
0756     r, z, phiPos, phiTarget, zTarget, index) {
0757   static_cast<void>(index);
0758 
0759   Transform3 transform = Transform3::Identity();
0760   auto cylBounds =
0761       std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 300_mm);
0762   auto cylVolume =
0763       std::make_shared<TrackingVolume>(transform, cylBounds, "CylinderVolume");
0764   SingleCylinderPortalShell shell{gctx, *cylVolume};
0765   shell.applyToVolume();
0766 
0767   Vector3 position{r * std::cos(phiPos), r * std::sin(phiPos), z};
0768   Vector3 target{r * std::cos(phiTarget), r * std::sin(phiTarget), zTarget};
0769   Vector3 direction = (target - position).normalized();
0770 
0771   auto exp =
0772       getTruth(position, direction, transform, *cylVolume, shell, *logger);
0773 
0774   CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0775   auto act = getSmart(position, direction, transform, policy);
0776   checkEqual(exp, act, shell);
0777 }
0778 
0779 BOOST_DATA_TEST_CASE(
0780     CylinderPolicyTestOnZBoundary,
0781     bdata::make(-300, 300) *
0782         (bdata::random((bdata::engine = engine, bdata::seed = seed(),
0783                         bdata::distribution = rDistOffBoundary)) ^
0784          bdata::random((bdata::engine = engine, bdata::seed = seed(),
0785                         bdata::distribution = phiDist)) ^
0786          bdata::random((bdata::engine = engine, bdata::seed = seed(),
0787                         bdata::distribution = phiDist)) ^
0788          bdata::random((bdata::engine = engine, bdata::seed = seed(),
0789                         bdata::distribution = zDistOffBoundary)) ^
0790          bdata::xrange(100)),
0791     z, r, phiPos, phiTarget, zTarget, index) {
0792   static_cast<void>(index);
0793   Transform3 transform = Transform3::Identity();
0794   auto cylBounds =
0795       std::make_shared<CylinderVolumeBounds>(100_mm, 400_mm, 300_mm);
0796   auto cylVolume =
0797       std::make_shared<TrackingVolume>(transform, cylBounds, "CylinderVolume");
0798   SingleCylinderPortalShell shell{gctx, *cylVolume};
0799   shell.applyToVolume();
0800 
0801   Vector3 position{r * std::cos(phiPos), r * std::sin(phiPos),
0802                    static_cast<double>(z)};
0803   Vector3 target{r * std::cos(phiTarget), r * std::sin(phiTarget), zTarget};
0804   Vector3 direction = (target - position).normalized();
0805 
0806   BOOST_CHECK(cylBounds->inside(position));
0807 
0808   auto exp =
0809       getTruth(position, direction, transform, *cylVolume, shell, *logger);
0810 
0811   CylinderNavigationPolicy policy(gctx, *cylVolume, *logger);
0812   auto act = getSmart(position, direction, transform, policy);
0813   checkEqual(exp, act, shell);
0814 }
0815 
0816 BOOST_AUTO_TEST_CASE(CylinderPolicyZeroInnerRadiusTest) {
0817   // Test that CylinderNavigationPolicy rejects volumes with zero inner radius
0818   Transform3 transform = Transform3::Identity();
0819 
0820   // Create cylinder volume bounds with zero inner radius (rMin = 0)
0821   auto cylBounds = std::make_shared<CylinderVolumeBounds>(0_mm, 400_mm, 300_mm);
0822   auto cylVolume = std::make_shared<TrackingVolume>(transform, cylBounds,
0823                                                     "ZeroInnerRadiusVolume");
0824 
0825   // CylinderNavigationPolicy constructor should throw std::invalid_argument
0826   // for volumes with zero inner radius
0827   {
0828     Acts::Logging::ScopedFailureThreshold log{Logging::FATAL};
0829     BOOST_CHECK_THROW(CylinderNavigationPolicy(gctx, *cylVolume, *logger),
0830                       std::invalid_argument);
0831   }
0832 }
0833 
0834 BOOST_AUTO_TEST_CASE(FrustumNavigationPolicyTest) {
0835   // Test the frustum navigation policy with a simple geometry
0836   Blueprint::Config cfg;
0837   cfg.envelope[AxisDirection::AxisZ] = {50_mm, 50_mm};
0838   cfg.envelope[AxisDirection::AxisY] = {50_mm, 50_mm};
0839   cfg.envelope[AxisDirection::AxisX] = {50_mm, 50_mm};
0840   auto root = std::make_unique<Blueprint>(cfg);
0841   auto& cub = root->addCuboidContainer("Container", AxisDirection::AxisZ);
0842   auto cubBounds = std::make_shared<CuboidVolumeBounds>(20_mm, 20_mm, 20_mm);
0843   auto childCub = std::make_unique<TrackingVolume>(Transform3::Identity(),
0844                                                    cubBounds, "child");
0845   Acts::Experimental::FrustumNavigationPolicy::Config frustumConfig{2};
0846   Acts::GeometryIdentifier id = Acts::GeometryIdentifier().withVolume(1);
0847   childCub->assignGeometryId(id);
0848   auto cubNode =
0849       std::make_shared<Acts::StaticBlueprintNode>(std::move(childCub));
0850   int chamberId = 1;
0851   auto childBounds = std::make_shared<CuboidVolumeBounds>(5_mm, 5_mm, 5_mm);
0852   std::vector<std::shared_ptr<Acts::StaticBlueprintNode>> nodes;
0853   for (int x = -3; x < 4; x += 2) {
0854     for (int y = -3; y < 4; y += 2) {
0855       for (int z = -3; z < 4; z += 2) {
0856         auto childVol = std::make_unique<TrackingVolume>(
0857             Transform3::Identity() *
0858                 Translation3{Vector3{x * 5_mm, y * 5_mm, z * 5_mm}},
0859             childBounds,
0860             "child_" + std::to_string(x) + "_" + std::to_string(y) + "_" +
0861                 std::to_string(z));
0862         Acts::GeometryIdentifier chId = id.withLayer(chamberId++);
0863         childVol->assignGeometryId(chId);
0864         auto staticNode =
0865             std::make_shared<Acts::StaticBlueprintNode>(std::move(childVol));
0866         nodes.push_back(std::move(staticNode));
0867         cubNode->addChild(nodes.back());
0868       }
0869     }
0870   }
0871   std::shared_ptr<Acts::NavigationPolicyFactory> factory =
0872       std::make_shared<Acts::NavigationPolicyFactory>(
0873           Acts::NavigationPolicyFactory{}
0874               .add<Acts::Experimental::FrustumNavigationPolicy>(frustumConfig));
0875   cubNode->setNavigationPolicyFactory(factory);
0876   cub.addChild(cubNode);
0877   auto tGeometry = root->construct({}, gctx, *logger);
0878   const TrackingVolume* vol = tGeometry->findVolume(id);
0879   NavigationStream main;
0880   AppendOnlyNavigationStream stream{main};
0881   NavigationArguments args{.position = Vector3{1, 1, 1},
0882                            .direction = Vector3{1, 1, 1}};
0883   NavigationPolicyStateManager stateManager;
0884   std::unique_ptr<Acts::Experimental::FrustumNavigationPolicy> frustumPolicy =
0885       std::make_unique<Acts::Experimental::FrustumNavigationPolicy>(
0886           gctx, *vol, *logger, frustumConfig);
0887   frustumPolicy->createState(gctx, args, stateManager, *logger);
0888   auto policyState = stateManager.currentState();
0889   frustumPolicy->initializeCandidates(gctx, args, policyState, stream, *logger);
0890   main.initialize(gctx, {Vector3{1, 1, 1}, Vector3{1, 1, 1}}, *logger);
0891   BOOST_CHECK_EQUAL(main.candidates().size(), 10);
0892 }
0893 
0894 BOOST_AUTO_TEST_SUITE_END()
0895 
0896 }  // namespace ActsTests