Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-08-02 08:26:41

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