Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-07-06 07:51:48

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 #pragma once
0010 
0011 #include "Acts/Definitions/Algebra.hpp"
0012 #include "Acts/Geometry/Layer.hpp"
0013 #include "Acts/Geometry/TrackingGeometry.hpp"
0014 #include "Acts/Geometry/TrackingVolume.hpp"
0015 #include "Acts/Propagator/NavigationTarget.hpp"
0016 #include "Acts/Propagator/NavigatorOptions.hpp"
0017 #include "Acts/Propagator/NavigatorStatistics.hpp"
0018 #include "Acts/Propagator/detail/NavigationHelpers.hpp"
0019 #include "Acts/Surfaces/BoundaryTolerance.hpp"
0020 #include "Acts/Surfaces/Surface.hpp"
0021 #include "Acts/Utilities/Intersection.hpp"
0022 #include "Acts/Utilities/Logger.hpp"
0023 #include "Acts/Utilities/StringHelpers.hpp"
0024 
0025 #include <algorithm>
0026 #include <cstdint>
0027 #include <limits>
0028 #include <memory>
0029 #include <vector>
0030 
0031 namespace Acts {
0032 
0033 /// @brief Captures the common functionality of the try-all navigators
0034 ///
0035 /// This class is not meant to be used directly, but to be inherited by the
0036 /// actual navigator implementations.
0037 ///
0038 class TryAllNavigatorBase {
0039  public:
0040   /// @brief Configuration for this Navigator
0041   struct Config {
0042     /// Tracking Geometry for this Navigator
0043     std::shared_ptr<const TrackingGeometry> trackingGeometry;
0044 
0045     /// stop at every sensitive surface (whether it has material or not)
0046     bool resolveSensitive = true;
0047     /// stop at every material surface (whether it is passive or not)
0048     bool resolveMaterial = true;
0049     /// stop at every surface regardless what it is
0050     bool resolvePassive = false;
0051 
0052     /// Which boundary checks to perform for surface approach
0053     BoundaryTolerance boundaryToleranceSurfaceApproach =
0054         BoundaryTolerance::None();
0055   };
0056 
0057   /// @brief Options for this Navigator
0058   struct Options : public NavigatorPlainOptions {
0059     explicit Options(const GeometryContext& gctx)
0060         : NavigatorPlainOptions(gctx) {}
0061 
0062     /// The surface tolerance
0063     double surfaceTolerance = s_onSurfaceTolerance;
0064 
0065     /// The near limit to resolve surfaces
0066     double nearLimit = s_onSurfaceTolerance;
0067 
0068     /// The far limit to resolve surfaces
0069     double farLimit = std::numeric_limits<double>::max();
0070 
0071     void setPlainOptions(const NavigatorPlainOptions& options) {
0072       static_cast<NavigatorPlainOptions&>(*this) = options;
0073     }
0074   };
0075 
0076   /// @brief Nested State struct
0077   ///
0078   /// It acts as an internal state which is created for every propagation and
0079   /// meant to keep thread-local navigation information.
0080   struct State {
0081     explicit State(const Options& options_) : options(options_) {}
0082 
0083     Options options;
0084 
0085     // Starting geometry information of the navigation which should only be set
0086     // while initialization. NOTE: This information is mostly used by actors to
0087     // check if we are on the starting surface (e.g. MaterialInteraction).
0088     const Surface* startSurface = nullptr;
0089 
0090     // Target geometry information of the navigation which should only be set
0091     // while initialization. NOTE: This information is mostly used by actors to
0092     // check if we are on the target surface (e.g. MaterialInteraction).
0093     const Surface* targetSurface = nullptr;
0094 
0095     // Current geometry information of the navigation which is set during
0096     // initialization and potentially updated after each step.
0097     const Surface* currentSurface = nullptr;
0098     const TrackingVolume* currentVolume = nullptr;
0099 
0100     /// The vector of navigation candidates to work through
0101     std::vector<detail::NavigationObjectCandidate> navigationCandidates;
0102 
0103     /// If a break has been detected
0104     bool navigationBreak = false;
0105 
0106     /// Navigation statistics
0107     NavigatorStatistics statistics;
0108   };
0109 
0110   /// Constructor with configuration object
0111   ///
0112   /// @param cfg The navigator configuration
0113   /// @param logger a logger instance
0114   TryAllNavigatorBase(Config cfg, std::unique_ptr<const Logger> logger)
0115       : m_cfg(std::move(cfg)), m_logger{std::move(logger)} {}
0116 
0117   const Surface* currentSurface(const State& state) const {
0118     return state.currentSurface;
0119   }
0120 
0121   const TrackingVolume* currentVolume(const State& state) const {
0122     return state.currentVolume;
0123   }
0124 
0125   const IVolumeMaterial* currentVolumeMaterial(const State& state) const {
0126     if (state.currentVolume == nullptr) {
0127       return nullptr;
0128     }
0129     return state.currentVolume->volumeMaterial();
0130   }
0131 
0132   const Surface* startSurface(const State& state) const {
0133     return state.startSurface;
0134   }
0135 
0136   const Surface* targetSurface(const State& state) const {
0137     return state.targetSurface;
0138   }
0139 
0140   bool endOfWorldReached(State& state) const {
0141     return state.currentVolume == nullptr;
0142   }
0143 
0144   bool navigationBreak(const State& state) const {
0145     return state.navigationBreak;
0146   }
0147 
0148   /// @brief Initialize the navigator
0149   ///
0150   /// This method initializes the navigator for a new propagation. It sets the
0151   /// current volume and surface to the start volume and surface, respectively.
0152   ///
0153   /// @param state The navigation state
0154   /// @param position The starting position
0155   /// @param direction The starting direction
0156   /// @param propagationDirection The propagation direction
0157   [[nodiscard]] Result<void> initialize(State& state, const Vector3& position,
0158                                         const Vector3& direction,
0159                                         Direction propagationDirection) const {
0160     (void)propagationDirection;
0161 
0162     ACTS_VERBOSE("initialize");
0163 
0164     state.startSurface = state.options.startSurface;
0165     state.targetSurface = state.options.targetSurface;
0166 
0167     const TrackingVolume* startVolume = nullptr;
0168 
0169     if (state.startSurface != nullptr &&
0170         state.startSurface->associatedLayer() != nullptr) {
0171       ACTS_VERBOSE(
0172           "Fast start initialization through association from Surface.");
0173       const auto* startLayer = state.startSurface->associatedLayer();
0174       startVolume = startLayer->trackingVolume();
0175     } else {
0176       ACTS_VERBOSE("Slow start initialization through search.");
0177       ACTS_VERBOSE("Starting from position " << toString(position)
0178                                              << " and direction "
0179                                              << toString(direction));
0180       startVolume = m_cfg.trackingGeometry->lowestTrackingVolume(
0181           state.options.geoContext, position);
0182     }
0183 
0184     // Initialize current volume, layer and surface
0185     {
0186       state.currentVolume = startVolume;
0187       if (state.currentVolume != nullptr) {
0188         ACTS_VERBOSE(volInfo(state) << "Start volume resolved.");
0189       } else {
0190         ACTS_ERROR("Start volume not resolved.");
0191       }
0192 
0193       state.currentSurface = state.startSurface;
0194       if (state.currentSurface != nullptr) {
0195         ACTS_VERBOSE(volInfo(state) << "Current surface set to start surface "
0196                                     << state.currentSurface->geometryId());
0197       } else {
0198         ACTS_VERBOSE(volInfo(state) << "No start surface set.");
0199       }
0200     }
0201 
0202     return Result<void>::success();
0203   }
0204 
0205  protected:
0206   /// Helper method to initialize navigation candidates for the current volume.
0207   void initializeVolumeCandidates(State& state) const {
0208     const TrackingVolume* volume = state.currentVolume;
0209     ACTS_VERBOSE(volInfo(state) << "Initialize volume");
0210 
0211     if (volume == nullptr) {
0212       state.navigationBreak = true;
0213       ACTS_VERBOSE(volInfo(state) << "No volume set. Good luck.");
0214       return;
0215     }
0216 
0217     emplaceAllVolumeCandidates(
0218         state.navigationCandidates, *volume, m_cfg.resolveSensitive,
0219         m_cfg.resolveMaterial, m_cfg.resolvePassive,
0220         m_cfg.boundaryToleranceSurfaceApproach, logger());
0221   }
0222 
0223   std::string volInfo(const State& state) const {
0224     return (state.currentVolume != nullptr ? state.currentVolume->volumeName()
0225                                            : "No Volume") +
0226            " | ";
0227   }
0228 
0229   const Logger& logger() const { return *m_logger; }
0230 
0231   Config m_cfg;
0232 
0233   std::unique_ptr<const Logger> m_logger;
0234 };
0235 
0236 /// @brief Alternative @c Navigator which tries all possible intersections
0237 ///
0238 /// See @c Navigator for more general information about the Navigator concept.
0239 ///
0240 /// This Navigator tries all possible intersections with all surfaces in the
0241 /// current volume. It does not use any information about the geometry to
0242 /// optimize the search. It is therefore very slow, but can be used as a
0243 /// reference implementation.
0244 ///
0245 class TryAllNavigator : public TryAllNavigatorBase {
0246  public:
0247   using Config = TryAllNavigatorBase::Config;
0248   using Options = TryAllNavigatorBase::Options;
0249 
0250   /// @brief Nested State struct
0251   struct State : public TryAllNavigatorBase::State {
0252     explicit State(const Options& options_)
0253         : TryAllNavigatorBase::State(options_) {}
0254 
0255     std::vector<detail::IntersectedNavigationObject> currentCandidates;
0256   };
0257 
0258   /// Constructor with configuration object
0259   ///
0260   /// @param cfg The navigator configuration
0261   /// @param logger a logger instance
0262   explicit TryAllNavigator(Config cfg, std::unique_ptr<const Logger> logger =
0263                                            getDefaultLogger("TryAllNavigator",
0264                                                             Logging::INFO))
0265       : TryAllNavigatorBase(std::move(cfg), std::move(logger)) {}
0266 
0267   State makeState(const Options& options) const {
0268     State state(options);
0269     return state;
0270   }
0271 
0272   using TryAllNavigatorBase::currentSurface;
0273   using TryAllNavigatorBase::currentVolume;
0274   using TryAllNavigatorBase::currentVolumeMaterial;
0275   using TryAllNavigatorBase::endOfWorldReached;
0276   using TryAllNavigatorBase::navigationBreak;
0277   using TryAllNavigatorBase::startSurface;
0278   using TryAllNavigatorBase::targetSurface;
0279 
0280   /// @brief Initialize the navigator
0281   ///
0282   /// This method initializes the navigator for a new propagation. It sets the
0283   /// current volume and surface to the start volume and surface, respectively.
0284   ///
0285   /// @param state The navigation state
0286   /// @param position The starting position
0287   /// @param direction The starting direction
0288   /// @param propagationDirection The propagation direction
0289   [[nodiscard]] Result<void> initialize(State& state, const Vector3& position,
0290                                         const Vector3& direction,
0291                                         Direction propagationDirection) const {
0292     auto baseRes = TryAllNavigatorBase::initialize(state, position, direction,
0293                                                    propagationDirection);
0294     if (!baseRes.ok()) {
0295       return baseRes.error();
0296     }
0297 
0298     // Initialize navigation candidates for the start volume
0299     reinitializeCandidates(state);
0300 
0301     return Result<void>::success();
0302   }
0303 
0304   /// @brief Get the next target surface
0305   ///
0306   /// This method gets the next target surface based on the current
0307   /// position and direction. It returns a none target if no target can be
0308   /// found.
0309   ///
0310   /// @param state The navigation state
0311   /// @param position The current position
0312   /// @param direction The current direction
0313   ///
0314   /// @return The next target surface
0315   NavigationTarget nextTarget(State& state, const Vector3& position,
0316                               const Vector3& direction) const {
0317     // Navigator preStep always resets the current surface
0318     state.currentSurface = nullptr;
0319 
0320     // Check if the navigator is inactive
0321     if (state.navigationBreak) {
0322       return NavigationTarget::None();
0323     }
0324 
0325     ACTS_VERBOSE(volInfo(state) << "nextTarget");
0326 
0327     double nearLimit = state.options.nearLimit;
0328     double farLimit = state.options.farLimit;
0329 
0330     // handle overstepping
0331     if (!state.currentCandidates.empty()) {
0332       const detail::IntersectedNavigationObject& previousCandidate =
0333           state.currentCandidates.front();
0334 
0335       const Surface& surface = *previousCandidate.intersection.object();
0336       std::uint8_t index = previousCandidate.intersection.index();
0337       BoundaryTolerance boundaryTolerance = previousCandidate.boundaryTolerance;
0338 
0339       auto intersection = surface.intersect(
0340           state.options.geoContext, position, direction, boundaryTolerance,
0341           state.options.surfaceTolerance)[index];
0342 
0343       if (intersection.pathLength() < 0) {
0344         nearLimit = std::min(nearLimit, intersection.pathLength() -
0345                                             state.options.surfaceTolerance);
0346         farLimit = -state.options.surfaceTolerance;
0347 
0348         ACTS_VERBOSE(volInfo(state)
0349                      << "handle overstepping with nearLimit " << nearLimit
0350                      << " and farLimit " << farLimit);
0351       }
0352     }
0353 
0354     std::vector<detail::IntersectedNavigationObject> intersectionCandidates;
0355 
0356     // Find intersections with all candidates
0357     for (const auto& candidate : state.navigationCandidates) {
0358       auto intersections =
0359           candidate.intersect(state.options.geoContext, position, direction,
0360                               state.options.surfaceTolerance);
0361       for (const auto& intersection : intersections.first.split()) {
0362         // exclude invalid intersections
0363         if (!intersection.isValid() ||
0364             !detail::checkPathLength(intersection.pathLength(), nearLimit,
0365                                      farLimit)) {
0366           continue;
0367         }
0368         // store candidate
0369         intersectionCandidates.emplace_back(intersection, intersections.second,
0370                                             candidate.boundaryTolerance);
0371       }
0372     }
0373 
0374     std::ranges::sort(intersectionCandidates,
0375                       detail::IntersectedNavigationObject::forwardOrder);
0376 
0377     ACTS_VERBOSE(volInfo(state) << "found " << intersectionCandidates.size()
0378                                 << " intersections");
0379 
0380     NavigationTarget nextTarget = NavigationTarget::None();
0381     state.currentCandidates.clear();
0382 
0383     for (const auto& candidate : intersectionCandidates) {
0384       const auto& intersection = candidate.intersection;
0385       const Surface& surface = *intersection.object();
0386       BoundaryTolerance boundaryTolerance = candidate.boundaryTolerance;
0387 
0388       if (intersection.status() == IntersectionStatus::onSurface) {
0389         ACTS_ERROR(volInfo(state)
0390                    << "We are on surface " << surface.geometryId()
0391                    << " before trying to reach it. This should not happen. "
0392                       "Good luck.");
0393         continue;
0394       }
0395 
0396       if (intersection.status() == IntersectionStatus::reachable) {
0397         nextTarget =
0398             NavigationTarget(surface, intersection.index(), boundaryTolerance);
0399         break;
0400       }
0401     }
0402 
0403     state.currentCandidates = std::move(intersectionCandidates);
0404 
0405     if (nextTarget.isNone()) {
0406       ACTS_VERBOSE(volInfo(state) << "no target found");
0407     } else {
0408       ACTS_VERBOSE(volInfo(state)
0409                    << "next target is " << nextTarget.surface->geometryId());
0410     }
0411 
0412     return nextTarget;
0413   }
0414 
0415   /// @brief Check if the target is still valid
0416   ///
0417   /// This method checks if the target is valid based on the current position
0418   /// and direction. It returns true if the target is still valid.
0419   ///
0420   /// For the TryAllNavigator, the target is always invalid since we do not want
0421   /// to assume any specific surface sequence over multiple steps.
0422   ///
0423   /// @param state The navigation state
0424   /// @param position The current position
0425   /// @param direction The current direction
0426   ///
0427   /// @return True if the target is still valid
0428   bool checkTargetValid(const State& state, const Vector3& position,
0429                         const Vector3& direction) const {
0430     (void)state;
0431     (void)position;
0432     (void)direction;
0433 
0434     return false;
0435   }
0436 
0437   /// @brief Handle the surface reached
0438   ///
0439   /// This method is called when a surface is reached. It sets the current
0440   /// surface in the navigation state and updates the navigation candidates.
0441   ///
0442   /// @param state The navigation state
0443   /// @param position The current position
0444   /// @param direction The current direction
0445   void handleSurfaceReached(State& state, const Vector3& position,
0446                             const Vector3& direction,
0447                             const Surface& /*surface*/) const {
0448     // Check if the navigator is inactive
0449     if (state.navigationBreak) {
0450       return;
0451     }
0452 
0453     ACTS_VERBOSE(volInfo(state) << "handleSurfaceReached");
0454 
0455     if (state.currentCandidates.empty()) {
0456       ACTS_VERBOSE(volInfo(state) << "No current candidate set.");
0457       return;
0458     }
0459 
0460     assert(state.currentSurface == nullptr && "Current surface must be reset.");
0461 
0462     // handle multiple surface intersections due to increased bounds
0463 
0464     std::vector<detail::IntersectedNavigationObject> hitCandidates;
0465 
0466     for (const auto& candidate : state.currentCandidates) {
0467       const Surface& surface = *candidate.intersection.object();
0468       std::uint8_t index = candidate.intersection.index();
0469       BoundaryTolerance boundaryTolerance = BoundaryTolerance::None();
0470 
0471       auto intersection = surface.intersect(
0472           state.options.geoContext, position, direction, boundaryTolerance,
0473           state.options.surfaceTolerance)[index];
0474 
0475       if (intersection.status() == IntersectionStatus::onSurface) {
0476         hitCandidates.emplace_back(candidate);
0477       }
0478     }
0479 
0480     state.currentCandidates.clear();
0481 
0482     ACTS_VERBOSE(volInfo(state)
0483                  << "Found " << hitCandidates.size()
0484                  << " intersections on surface with bounds check.");
0485 
0486     if (hitCandidates.empty()) {
0487       ACTS_VERBOSE(volInfo(state) << "No hit candidates found.");
0488       return;
0489     }
0490 
0491     if (hitCandidates.size() > 1) {
0492       ACTS_VERBOSE(volInfo(state)
0493                    << "Only using first intersection within bounds.");
0494     }
0495 
0496     // we can only handle a single surface hit so we pick the first one
0497     const auto candidate = hitCandidates.front();
0498     const auto& intersection = candidate.intersection;
0499     const Surface& surface = *intersection.object();
0500 
0501     ACTS_VERBOSE(volInfo(state) << "Surface " << surface.geometryId()
0502                                 << " successfully hit, storing it.");
0503     state.currentSurface = &surface;
0504 
0505     if (candidate.template checkType<Surface>()) {
0506       ACTS_VERBOSE(volInfo(state) << "This is a surface");
0507     } else if (candidate.template checkType<Layer>()) {
0508       ACTS_VERBOSE(volInfo(state) << "This is a layer");
0509     } else if (candidate.template checkType<BoundarySurface>()) {
0510       ACTS_VERBOSE(volInfo(state)
0511                    << "This is a boundary. Reinitialize navigation");
0512 
0513       const auto& boundary = *candidate.template object<BoundarySurface>();
0514 
0515       state.currentVolume = boundary.attachedVolume(state.options.geoContext,
0516                                                     position, direction);
0517 
0518       ACTS_VERBOSE(volInfo(state) << "Switched volume");
0519 
0520       reinitializeCandidates(state);
0521     } else {
0522       ACTS_ERROR(volInfo(state) << "Unknown intersection type");
0523     }
0524   }
0525 
0526  private:
0527   /// Helper method to reset and reinitialize the navigation candidates.
0528   void reinitializeCandidates(State& state) const {
0529     state.navigationCandidates.clear();
0530     state.currentCandidates.clear();
0531 
0532     initializeVolumeCandidates(state);
0533   }
0534 };
0535 
0536 /// @brief Alternative @c Navigator which tries all possible intersections
0537 ///
0538 /// See @c Navigator for more general information about the Navigator concept.
0539 ///
0540 /// This Navigator tries all possible intersections with all surfaces in the
0541 /// current volume. It does not use any information about the geometry to
0542 /// optimize the search. It is therefore very slow, but can be used as a
0543 /// reference implementation.
0544 ///
0545 /// Different to @c TryAllNavigator, this Navigator discovers intersections by
0546 /// stepping forward blindly and then checking for intersections with all
0547 /// surfaces in the current volume. This is slower, but more robust against
0548 /// bent tracks.
0549 ///
0550 class TryAllOverstepNavigator : public TryAllNavigatorBase {
0551  public:
0552   using Config = TryAllNavigatorBase::Config;
0553 
0554   using Options = TryAllNavigatorBase::Options;
0555 
0556   /// @brief Nested State struct
0557   ///
0558   /// It acts as an internal state which is created for every propagation and
0559   /// meant to keep thread-local navigation information.
0560   struct State : public TryAllNavigatorBase::State {
0561     explicit State(const Options& options_)
0562         : TryAllNavigatorBase::State(options_) {}
0563 
0564     /// The vector of active intersection candidates to work through
0565     std::vector<detail::IntersectedNavigationObject> activeCandidates;
0566     /// The current active candidate index of the navigation state
0567     int activeCandidateIndex = -1;
0568 
0569     /// The position before the last step
0570     std::optional<Vector3> lastPosition;
0571 
0572     /// Provides easy access to the active intersection candidate
0573     const detail::IntersectedNavigationObject& activeCandidate() const {
0574       return activeCandidates.at(activeCandidateIndex);
0575     }
0576 
0577     bool endOfCandidates() const {
0578       return activeCandidateIndex >= static_cast<int>(activeCandidates.size());
0579     }
0580   };
0581 
0582   /// Constructor with configuration object
0583   ///
0584   /// @param cfg The navigator configuration
0585   /// @param logger a logger instance
0586   explicit TryAllOverstepNavigator(
0587       Config cfg, std::unique_ptr<const Logger> logger = getDefaultLogger(
0588                       "TryAllOverstepNavigator", Logging::INFO))
0589       : TryAllNavigatorBase(std::move(cfg), std::move(logger)) {}
0590 
0591   State makeState(const Options& options) const {
0592     State state(options);
0593     return state;
0594   }
0595 
0596   using TryAllNavigatorBase::currentSurface;
0597   using TryAllNavigatorBase::currentVolume;
0598   using TryAllNavigatorBase::currentVolumeMaterial;
0599   using TryAllNavigatorBase::endOfWorldReached;
0600   using TryAllNavigatorBase::navigationBreak;
0601   using TryAllNavigatorBase::startSurface;
0602   using TryAllNavigatorBase::targetSurface;
0603 
0604   /// @brief Initialize the navigator
0605   ///
0606   /// This method initializes the navigator for a new propagation. It sets the
0607   /// current volume and surface to the start volume and surface, respectively.
0608   ///
0609   /// @param state The navigation state
0610   /// @param position The starting position
0611   /// @param direction The starting direction
0612   /// @param propagationDirection The propagation direction
0613   [[nodiscard]] Result<void> initialize(State& state, const Vector3& position,
0614                                         const Vector3& direction,
0615                                         Direction propagationDirection) const {
0616     auto baseRes = TryAllNavigatorBase::initialize(state, position, direction,
0617                                                    propagationDirection);
0618     if (!baseRes.ok()) {
0619       return baseRes.error();
0620     }
0621 
0622     // Initialize navigation candidates for the start volume
0623     reinitializeCandidates(state);
0624 
0625     state.lastPosition.reset();
0626 
0627     return Result<void>::success();
0628   }
0629 
0630   /// @brief Get the next target surface
0631   ///
0632   /// This method gets the next target surface based on the current
0633   /// position and direction. It returns an invalid target if no target can be
0634   /// found.
0635   ///
0636   /// @param state The navigation state
0637   /// @param position The current position
0638   /// @param direction The current direction
0639   ///
0640   /// @return The next target surface
0641   NavigationTarget nextTarget(State& state, const Vector3& position,
0642                               const Vector3& direction) const {
0643     (void)direction;
0644 
0645     // Navigator preStep always resets the current surface
0646     state.currentSurface = nullptr;
0647 
0648     // Check if the navigator is inactive
0649     if (state.navigationBreak) {
0650       return NavigationTarget::None();
0651     }
0652 
0653     ACTS_VERBOSE(volInfo(state) << "nextTarget");
0654 
0655     // We cannot do anything without a last position
0656     if (!state.lastPosition.has_value() && state.endOfCandidates()) {
0657       ACTS_VERBOSE(
0658           volInfo(state)
0659           << "Initial position, nothing to do, blindly stepping forward.");
0660       state.lastPosition = position;
0661       return NavigationTarget::None();
0662     }
0663 
0664     if (state.endOfCandidates()) {
0665       ACTS_VERBOSE(volInfo(state) << "evaluate blind step");
0666 
0667       Vector3 stepStart = state.lastPosition.value();
0668       Vector3 stepEnd = position;
0669       Vector3 step = stepEnd - stepStart;
0670       double stepDistance = step.norm();
0671       if (stepDistance < std::numeric_limits<double>::epsilon()) {
0672         ACTS_ERROR(volInfo(state) << "Step distance is zero. " << stepDistance);
0673       }
0674       Vector3 stepDirection = step.normalized();
0675 
0676       double nearLimit = -stepDistance + state.options.surfaceTolerance;
0677       double farLimit = 0;
0678 
0679       state.lastPosition.reset();
0680       state.activeCandidates.clear();
0681       state.activeCandidateIndex = -1;
0682 
0683       // Find intersections with all candidates
0684       for (const auto& candidate : state.navigationCandidates) {
0685         auto intersections =
0686             candidate.intersect(state.options.geoContext, stepEnd,
0687                                 stepDirection, state.options.surfaceTolerance);
0688         for (const auto& intersection : intersections.first.split()) {
0689           // exclude invalid intersections
0690           if (!intersection.isValid() ||
0691               !detail::checkPathLength(intersection.pathLength(), nearLimit,
0692                                        farLimit)) {
0693             continue;
0694           }
0695           // store candidate
0696           state.activeCandidates.emplace_back(
0697               intersection, intersections.second, candidate.boundaryTolerance);
0698         }
0699       }
0700 
0701       std::ranges::sort(state.activeCandidates,
0702                         detail::IntersectedNavigationObject::forwardOrder);
0703 
0704       ACTS_VERBOSE(volInfo(state) << "Found " << state.activeCandidates.size()
0705                                   << " intersections");
0706 
0707       for (const auto& candidate : state.activeCandidates) {
0708         ACTS_VERBOSE("found candidate "
0709                      << candidate.intersection.object()->geometryId());
0710       }
0711     }
0712 
0713     ++state.activeCandidateIndex;
0714 
0715     if (state.endOfCandidates()) {
0716       ACTS_VERBOSE(volInfo(state)
0717                    << "No target found, blindly stepping forward.");
0718       state.lastPosition = position;
0719       return NavigationTarget::None();
0720     }
0721 
0722     ACTS_VERBOSE(volInfo(state) << "handle active candidates");
0723 
0724     ACTS_VERBOSE(volInfo(state)
0725                  << (state.activeCandidates.size() - state.activeCandidateIndex)
0726                  << " out of " << state.activeCandidates.size()
0727                  << " surfaces remain to try.");
0728 
0729     const auto& candidate = state.activeCandidate();
0730     const auto& intersection = candidate.intersection;
0731     const Surface& surface = *intersection.object();
0732     BoundaryTolerance boundaryTolerance = candidate.boundaryTolerance;
0733 
0734     ACTS_VERBOSE(volInfo(state)
0735                  << "Next surface candidate will be " << surface.geometryId());
0736 
0737     return NavigationTarget(surface, intersection.index(), boundaryTolerance);
0738   }
0739 
0740   /// @brief Check if the target is still valid
0741   ///
0742   /// This method checks if the target is valid based on the current position
0743   /// and direction. It returns true if the target is still valid.
0744   ///
0745   /// @param state The navigation state
0746   /// @param position The current position
0747   /// @param direction The current direction
0748   ///
0749   /// @return True if the target is still valid
0750   bool checkTargetValid(const State& state, const Vector3& position,
0751                         const Vector3& direction) const {
0752     (void)state;
0753     (void)position;
0754     (void)direction;
0755 
0756     return true;
0757   }
0758 
0759   /// @brief Handle the surface reached
0760   ///
0761   /// This method is called when a surface is reached. It sets the current
0762   /// surface in the navigation state and updates the navigation candidates.
0763   ///
0764   /// @param state The navigation state
0765   /// @param position The current position
0766   /// @param direction The current direction
0767   void handleSurfaceReached(State& state, const Vector3& position,
0768                             const Vector3& direction,
0769                             const Surface& /*surface*/) const {
0770     if (state.navigationBreak) {
0771       return;
0772     }
0773 
0774     ACTS_VERBOSE(volInfo(state) << "handleSurfaceReached");
0775 
0776     assert(state.currentSurface == nullptr && "Current surface must be reset.");
0777 
0778     if (state.endOfCandidates()) {
0779       ACTS_VERBOSE(volInfo(state) << "No active candidate set.");
0780       return;
0781     }
0782 
0783     std::vector<detail::IntersectedNavigationObject> hitCandidates;
0784 
0785     while (!state.endOfCandidates()) {
0786       const auto& candidate = state.activeCandidate();
0787       const auto& intersection = candidate.intersection;
0788       const Surface& surface = *intersection.object();
0789       BoundaryTolerance boundaryTolerance = candidate.boundaryTolerance;
0790 
0791       // first with boundary tolerance
0792       IntersectionStatus surfaceStatus =
0793           surface
0794               .intersect(state.options.geoContext, position, direction,
0795                          boundaryTolerance,
0796                          state.options.surfaceTolerance)[intersection.index()]
0797               .status();
0798 
0799       if (surfaceStatus != IntersectionStatus::onSurface) {
0800         break;
0801       }
0802 
0803       // now without boundary tolerance
0804       boundaryTolerance = BoundaryTolerance::None();
0805       surfaceStatus =
0806           surface
0807               .intersect(state.options.geoContext, position, direction,
0808                          boundaryTolerance,
0809                          state.options.surfaceTolerance)[intersection.index()]
0810               .status();
0811 
0812       if (surfaceStatus == IntersectionStatus::onSurface) {
0813         hitCandidates.emplace_back(candidate);
0814       }
0815 
0816       ++state.activeCandidateIndex;
0817       ACTS_VERBOSE("skip candidate " << surface.geometryId());
0818     }
0819 
0820     // we increased the candidate index one too many times
0821     --state.activeCandidateIndex;
0822 
0823     ACTS_VERBOSE(volInfo(state)
0824                  << "Found " << hitCandidates.size()
0825                  << " intersections on surface with bounds check.");
0826 
0827     if (hitCandidates.empty()) {
0828       ACTS_VERBOSE(volInfo(state)
0829                    << "Surface successfully hit, but outside bounds.");
0830       return;
0831     }
0832 
0833     if (hitCandidates.size() > 1) {
0834       ACTS_VERBOSE(volInfo(state)
0835                    << "Only using first intersection within bounds.");
0836     }
0837 
0838     // we can only handle a single surface hit so we pick the first one
0839     const auto& candidate = hitCandidates.front();
0840     const auto& intersection = candidate.intersection;
0841     const Surface& surface = *intersection.object();
0842 
0843     ACTS_VERBOSE(volInfo(state) << "Surface successfully hit, storing it.");
0844     state.currentSurface = &surface;
0845 
0846     if (state.currentSurface != nullptr) {
0847       ACTS_VERBOSE(volInfo(state) << "Current surface set to surface "
0848                                   << surface.geometryId());
0849     }
0850 
0851     if (candidate.template checkType<Surface>()) {
0852       ACTS_VERBOSE(volInfo(state) << "This is a surface");
0853     } else if (candidate.template checkType<Layer>()) {
0854       ACTS_VERBOSE(volInfo(state) << "This is a layer");
0855     } else if (candidate.template checkType<BoundarySurface>()) {
0856       ACTS_VERBOSE(volInfo(state)
0857                    << "This is a boundary. Reinitialize navigation");
0858 
0859       const auto& boundary = *candidate.template object<BoundarySurface>();
0860 
0861       state.currentVolume = boundary.attachedVolume(state.options.geoContext,
0862                                                     position, direction);
0863 
0864       ACTS_VERBOSE(volInfo(state) << "Switched volume");
0865 
0866       reinitializeCandidates(state);
0867     } else {
0868       ACTS_ERROR(volInfo(state) << "Unknown intersection type");
0869     }
0870   }
0871 
0872  private:
0873   /// Helper method to reset and reinitialize the navigation candidates.
0874   void reinitializeCandidates(State& state) const {
0875     state.navigationCandidates.clear();
0876     state.activeCandidates.clear();
0877     state.activeCandidateIndex = -1;
0878 
0879     initializeVolumeCandidates(state);
0880   }
0881 };
0882 
0883 }  // namespace Acts