Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-10-29 07:55:29

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