Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2026-07-16 08:07:34

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/Definitions/Direction.hpp"
0013 #include "Acts/Definitions/TrackParametrization.hpp"
0014 #include "Acts/EventData/MultiComponentTrackParameters.hpp"
0015 #include "Acts/EventData/TrackParameters.hpp"
0016 #include "Acts/EventData/detail/CorrectedTransformationFreeToBound.hpp"
0017 #include "Acts/MagneticField/MagneticFieldProvider.hpp"
0018 #include "Acts/Material/IVolumeMaterial.hpp"
0019 #include "Acts/Propagator/ConstrainedStep.hpp"
0020 #include "Acts/Propagator/StepperConcept.hpp"
0021 #include "Acts/Propagator/StepperOptions.hpp"
0022 #include "Acts/Propagator/StepperStatistics.hpp"
0023 #include "Acts/Propagator/detail/LoopStepperUtils.hpp"
0024 #include "Acts/Propagator/detail/SteppingHelper.hpp"
0025 #include "Acts/Surfaces/Surface.hpp"
0026 #include "Acts/Utilities/Intersection.hpp"
0027 #include "Acts/Utilities/Logger.hpp"
0028 #include "Acts/Utilities/Result.hpp"
0029 
0030 #include <algorithm>
0031 #include <cmath>
0032 #include <cstddef>
0033 #include <limits>
0034 #include <sstream>
0035 #include <vector>
0036 
0037 #include <boost/container/small_vector.hpp>
0038 
0039 namespace Acts {
0040 
0041 namespace detail {
0042 
0043 struct MaxMomentumComponent {
0044   template <typename component_range_t>
0045   auto operator()(const component_range_t& cmps) const {
0046     return std::ranges::max_element(cmps, [&](const auto& a, const auto& b) {
0047       return std::abs(a.state.pars[eFreeQOverP]) >
0048              std::abs(b.state.pars[eFreeQOverP]);
0049     });
0050   }
0051 };
0052 
0053 struct MaxWeightComponent {
0054   template <typename component_range_t>
0055   auto operator()(const component_range_t& cmps) {
0056     return std::ranges::max_element(cmps, [&](const auto& a, const auto& b) {
0057       return a.weight < b.weight;
0058     });
0059   }
0060 };
0061 
0062 template <typename component_chooser_t>
0063 struct SingleComponentReducer {
0064   template <typename stepper_state_t>
0065   static Vector3 position(const stepper_state_t& s) {
0066     return component_chooser_t{}(s.components)
0067         ->state.pars.template segment<3>(eFreePos0);
0068   }
0069 
0070   template <typename stepper_state_t>
0071   static Vector3 direction(const stepper_state_t& s) {
0072     return component_chooser_t{}(s.components)
0073         ->state.pars.template segment<3>(eFreeDir0);
0074   }
0075 
0076   template <typename stepper_state_t>
0077   static double qOverP(const stepper_state_t& s) {
0078     const auto cmp = component_chooser_t{}(s.components);
0079     return cmp->state.pars[eFreeQOverP];
0080   }
0081 
0082   template <typename stepper_state_t>
0083   static double absoluteMomentum(const stepper_state_t& s) {
0084     const auto cmp = component_chooser_t{}(s.components);
0085     return s.particleHypothesis.extractMomentum(cmp->state.pars[eFreeQOverP]);
0086   }
0087 
0088   template <typename stepper_state_t>
0089   static Vector3 momentum(const stepper_state_t& s) {
0090     const auto cmp = component_chooser_t{}(s.components);
0091     return s.particleHypothesis.extractMomentum(cmp->state.pars[eFreeQOverP]) *
0092            cmp->state.pars.template segment<3>(eFreeDir0);
0093   }
0094 
0095   template <typename stepper_state_t>
0096   static double charge(const stepper_state_t& s) {
0097     const auto cmp = component_chooser_t{}(s.components);
0098     return s.particleHypothesis.extractCharge(cmp->state.pars[eFreeQOverP]);
0099   }
0100 
0101   template <typename stepper_state_t>
0102   static double time(const stepper_state_t& s) {
0103     return component_chooser_t{}(s.components)->state.pars[eFreeTime];
0104   }
0105 
0106   template <typename stepper_state_t>
0107   static FreeVector pars(const stepper_state_t& s) {
0108     return component_chooser_t{}(s.components)->state.pars;
0109   }
0110 
0111   template <typename stepper_state_t>
0112   static FreeVector cov(const stepper_state_t& s) {
0113     return component_chooser_t{}(s.components)->state.cov;
0114   }
0115 };
0116 
0117 }  // namespace detail
0118 
0119 using MaxMomentumReducerLoop =
0120     detail::SingleComponentReducer<detail::MaxMomentumComponent>;
0121 using MaxWeightReducerLoop =
0122     detail::SingleComponentReducer<detail::MaxWeightComponent>;
0123 
0124 /// @brief Stepper based on a single-component stepper, but can handle
0125 /// Multi-Component Tracks (e.g., for the GSF). Internally, this only
0126 /// manages a vector of states of the single stepper. This simplifies
0127 /// implementation, but has several drawbacks:
0128 /// * There are certain redundancies between the global State and the
0129 /// component states
0130 /// * The components do not share a single magnetic-field-cache
0131 /// @tparam sstepper_t The single-component stepper type to use
0132 /// @tparam component_reducer_t How to map the multi-component state to a single
0133 /// component
0134 template <Concepts::SingleStepper single_stepper_t,
0135           typename component_reducer_t = MaxWeightReducerLoop>
0136 class MultiStepperLoop : public single_stepper_t {
0137   /// Limits the number of steps after at least one component reached the
0138   /// surface
0139   std::size_t m_stepLimitAfterFirstComponentOnSurface = 50;
0140 
0141   /// The logger (used if no logger is provided by caller of methods)
0142   std::unique_ptr<const Acts::Logger> m_logger;
0143 
0144   /// Small vector type for speeding up some computations where we need to
0145   /// accumulate stuff of components. We think 16 is a reasonable amount here.
0146   template <typename T>
0147   using SmallVector = boost::container::small_vector<T, 16>;
0148 
0149  public:
0150   /// @brief Typedef to the Single-Component Eigen Stepper
0151   using SingleStepper = single_stepper_t;
0152 
0153   /// @brief Typedef to the Single-Component Stepper Options
0154   using SingleOptions = typename SingleStepper::Options;
0155 
0156   /// @brief Typedef to the State of the single component Stepper
0157   using SingleState = typename SingleStepper::State;
0158 
0159   /// @brief Typedef to the Config of the single component Stepper
0160   using SingleConfig = typename SingleStepper::Config;
0161 
0162   /// @brief Use the definitions from the Single-stepper
0163   using typename SingleStepper::Covariance;
0164   using typename SingleStepper::Jacobian;
0165 
0166   /// @brief Define an own bound state
0167   using BoundState =
0168       std::tuple<MultiComponentBoundTrackParameters, Jacobian, double>;
0169 
0170   /// @brief The reducer type
0171   using Reducer = component_reducer_t;
0172 
0173   /// @brief How many components can this stepper manage?
0174   static constexpr int maxComponents = std::numeric_limits<int>::max();
0175 
0176   struct Config : public SingleStepper::Config {
0177     /// Limits the number of steps after at least one component reached the
0178     /// surface
0179     std::size_t stepLimitAfterFirstComponentOnSurface = 50;
0180   };
0181 
0182   struct Options : public SingleOptions {
0183     using SingleOptions::SingleOptions;
0184   };
0185 
0186   struct State {
0187     /// The struct that stores the individual components
0188     struct Component {
0189       /// Individual component state for propagation
0190       SingleState state;
0191       /// Statistical weight of this component
0192       double weight;
0193       /// Intersection status of this component
0194       IntersectionStatus status;
0195 
0196       /// Constructor for a multi-stepper component
0197       /// @param state_ The single state for this component
0198       /// @param weight_ The weight of this component
0199       /// @param status_ The intersection status of this component
0200       Component(SingleState state_, double weight_, IntersectionStatus status_)
0201           : state(std::move(state_)), weight(weight_), status(status_) {}
0202     };
0203 
0204     Options options;
0205 
0206     /// Particle hypothesis
0207     ParticleHypothesis particleHypothesis = ParticleHypothesis::pion();
0208 
0209     /// The components of which the state consists
0210     SmallVector<Component> components;
0211 
0212     bool covTransport = false;
0213     double pathAccumulated = 0.;
0214     std::size_t steps = 0;
0215 
0216     /// Step-limit counter which limits the number of steps when one component
0217     /// reached a surface
0218     std::optional<std::size_t> stepCounterAfterFirstComponentOnSurface;
0219 
0220     /// The stepper statistics
0221     StepperStatistics statistics;
0222 
0223     /// Constructor from the initial bound track parameters
0224     ///
0225     /// @param [in] optionsIn is the options object for the stepper
0226     ///
0227     /// @note the covariance matrix is copied when needed
0228     explicit State(const Options& optionsIn) : options(optionsIn) {}
0229   };
0230 
0231   /// Constructor from a magnetic field and a optionally provided Logger
0232   /// TODO this requires that every stepper can be constructed like this...
0233   /// @param bField Magnetic field provider to use for propagation
0234   /// @param logger Logger instance for debugging output
0235   explicit MultiStepperLoop(std::shared_ptr<const MagneticFieldProvider> bField,
0236                             std::unique_ptr<const Logger> logger =
0237                                 getDefaultLogger("GSF", Logging::INFO))
0238       : SingleStepper(std::move(bField)), m_logger(std::move(logger)) {}
0239 
0240   /// Constructor from a configuration and optionally provided Logger
0241   /// @param config Configuration object containing stepper settings
0242   /// @param logger Logger instance for debugging output
0243   explicit MultiStepperLoop(const Config& config,
0244                             std::unique_ptr<const Logger> logger =
0245                                 getDefaultLogger("MultiStepperLoop",
0246                                                  Logging::INFO))
0247       : SingleStepper(config),
0248         m_stepLimitAfterFirstComponentOnSurface(
0249             config.stepLimitAfterFirstComponentOnSurface),
0250         m_logger(std::move(logger)) {}
0251 
0252   /// Create a state object for multi-stepping
0253   /// @param options The propagation options
0254   /// @return Initialized state object for multi-stepper
0255   State makeState(const Options& options) const {
0256     State state(options);
0257     return state;
0258   }
0259 
0260   /// Initialize the stepper state from multi-component bound track parameters
0261   /// @param state The stepper state to initialize
0262   /// @param par The multi-component bound track parameters
0263   void initialize(State& state,
0264                   const MultiComponentBoundTrackParameters& par) const {
0265     if (par.components().empty()) {
0266       throw std::invalid_argument(
0267           "Cannot construct MultiEigenStepperLoop::State with empty "
0268           "multi-component parameters");
0269     }
0270 
0271     state.particleHypothesis = par.particleHypothesis();
0272 
0273     const auto surface = par.referenceSurface().getSharedPtr();
0274 
0275     for (auto i = 0ul; i < par.components().size(); ++i) {
0276       const auto& [weight, singlePars] = par[i];
0277       auto& cmp =
0278           state.components.emplace_back(SingleStepper::makeState(state.options),
0279                                         weight, IntersectionStatus::onSurface);
0280       SingleStepper::initialize(cmp.state, singlePars);
0281     }
0282 
0283     if (std::get<2>(par.components().front())) {
0284       state.covTransport = true;
0285     }
0286   }
0287 
0288   /// A proxy struct which allows access to a single component of the
0289   /// multi-component state. It has the semantics of a const reference, i.e.
0290   /// it requires a const reference of the single-component state it
0291   /// represents
0292   using ConstComponentProxy =
0293       detail::LoopComponentProxyBase<const typename State::Component,
0294                                      MultiStepperLoop>;
0295 
0296   /// A proxy struct which allows access to a single component of the
0297   /// multi-component state. It has the semantics of a mutable reference, i.e.
0298   /// it requires a mutable reference of the single-component state it
0299   /// represents
0300   using ComponentProxy =
0301       detail::LoopComponentProxy<typename State::Component, MultiStepperLoop>;
0302 
0303   /// Creates an iterable which can be plugged into a range-based for-loop to
0304   /// iterate over components
0305   /// @param state Multi-component stepper state to iterate over
0306   /// @note Use a for-loop with by-value semantics, since the Iterable returns a
0307   /// proxy internally holding a reference
0308   /// @return Iterable range object that can be used in range-based for-loops
0309   auto componentIterable(State& state) const {
0310     struct Iterator {
0311       using difference_type [[maybe_unused]] = std::ptrdiff_t;
0312       using value_type [[maybe_unused]] = ComponentProxy;
0313       using reference [[maybe_unused]] = ComponentProxy;
0314       using pointer [[maybe_unused]] = void;
0315       using iterator_category [[maybe_unused]] = std::forward_iterator_tag;
0316 
0317       typename decltype(state.components)::iterator it;
0318       const State& s;
0319 
0320       // clang-format off
0321       auto& operator++() { ++it; return *this; }
0322       auto operator==(const Iterator& other) const { return it == other.it; }
0323       auto operator*() const { return ComponentProxy(*it, s); }
0324       // clang-format on
0325     };
0326 
0327     struct Iterable {
0328       State& s;
0329 
0330       // clang-format off
0331       auto begin() { return Iterator{s.components.begin(), s}; }
0332       auto end() { return Iterator{s.components.end(), s}; }
0333       // clang-format on
0334     };
0335 
0336     return Iterable{state};
0337   }
0338 
0339   /// Creates an constant iterable which can be plugged into a range-based
0340   /// for-loop to iterate over components
0341   /// @param state Multi-component stepper state to iterate over (const)
0342   /// @note Use a for-loop with by-value semantics, since the Iterable returns a
0343   /// proxy internally holding a reference
0344   /// @return Const iterable range object for read-only iteration over components
0345   auto constComponentIterable(const State& state) const {
0346     struct ConstIterator {
0347       using difference_type [[maybe_unused]] = std::ptrdiff_t;
0348       using value_type [[maybe_unused]] = ConstComponentProxy;
0349       using reference [[maybe_unused]] = ConstComponentProxy;
0350       using pointer [[maybe_unused]] = void;
0351       using iterator_category [[maybe_unused]] = std::forward_iterator_tag;
0352 
0353       typename decltype(state.components)::const_iterator it;
0354       const State& s;
0355 
0356       // clang-format off
0357       auto& operator++() { ++it; return *this; }
0358       auto operator==(const ConstIterator& other) const { return it == other.it; }
0359       auto operator*() const { return ConstComponentProxy{*it}; }
0360       // clang-format on
0361     };
0362 
0363     struct Iterable {
0364       const State& s;
0365 
0366       // clang-format off
0367       auto begin() const { return ConstIterator{s.components.cbegin(), s}; }
0368       auto end() const { return ConstIterator{s.components.cend(), s}; }
0369       // clang-format on
0370     };
0371 
0372     return Iterable{state};
0373   }
0374 
0375   /// Get the number of components
0376   ///
0377   /// @param state [in,out] The stepping state (thread-local cache)
0378   /// @return Number of components in the multi-component state
0379   std::size_t numberComponents(const State& state) const {
0380     return state.components.size();
0381   }
0382 
0383   /// Remove missed components from the component state
0384   ///
0385   /// @param state [in,out] The stepping state (thread-local cache)
0386   void removeMissedComponents(State& state) const {
0387     auto [beg, end] =
0388         std::ranges::remove_if(state.components, [](const auto& cmp) {
0389           return cmp.status == IntersectionStatus::unreachable;
0390         });
0391 
0392     state.components.erase(beg, end);
0393   }
0394 
0395   /// Reweight the components
0396   ///
0397   /// @param [in,out] state The stepping state (thread-local cache)
0398   void reweightComponents(State& state) const {
0399     double sumOfWeights = 0.0;
0400     for (const auto& cmp : state.components) {
0401       sumOfWeights += cmp.weight;
0402     }
0403     for (auto& cmp : state.components) {
0404       cmp.weight /= sumOfWeights;
0405     }
0406   }
0407 
0408   /// Reset the number of components
0409   ///
0410   /// @param [in,out] state  The stepping state (thread-local cache)
0411   void clearComponents(State& state) const { state.components.clear(); }
0412 
0413   /// Add a component to the Multistepper
0414   ///
0415   /// @param [in,out] state  The stepping state (thread-local cache)
0416   /// @param [in] pars Parameters of the component to add
0417   /// @param [in] weight Weight of the component to add
0418   ///
0419   /// @note: It is not ensured that the weights are normalized afterwards
0420   /// @note This function makes no garantuees about how new components are
0421   /// initialized, it is up to the caller to ensure that all components are
0422   /// valid in the end.
0423   /// @note The returned component-proxy is only garantueed to be valid until
0424   /// the component number is again modified
0425   /// @return ComponentProxy for the newly added component or error
0426   Result<ComponentProxy> addComponent(State& state,
0427                                       const BoundTrackParameters& pars,
0428                                       double weight) const {
0429     auto& cmp =
0430         state.components.emplace_back(SingleStepper::makeState(state.options),
0431                                       weight, IntersectionStatus::onSurface);
0432     SingleStepper::initialize(cmp.state, pars);
0433 
0434     return ComponentProxy{state.components.back(), state};
0435   }
0436 
0437   /// Get the field for the stepping, it checks first if the access is still
0438   /// within the Cell, and updates the cell if necessary.
0439   ///
0440   /// @param [in,out] state is the propagation state associated with the track
0441   ///                 the magnetic field cell is used (and potentially updated)
0442   /// @param [in] pos is the field position
0443   ///
0444   /// @note This uses the cache of the first component stored in the state
0445   /// @return Magnetic field vector at the given position or error
0446   Result<Vector3> getField(State& state, const Vector3& pos) const {
0447     return SingleStepper::getField(state.components.front().state, pos);
0448   }
0449 
0450   /// Global particle position accessor
0451   ///
0452   /// @param state [in] The stepping state (thread-local cache)
0453   /// @return Global position vector from the reduced component state
0454   Vector3 position(const State& state) const {
0455     return Reducer::position(state);
0456   }
0457 
0458   /// Momentum direction accessor
0459   ///
0460   /// @param state [in] The stepping state (thread-local cache)
0461   /// @return Normalized momentum direction vector from the reduced component state
0462   Vector3 direction(const State& state) const {
0463     return Reducer::direction(state);
0464   }
0465 
0466   /// QoP access
0467   ///
0468   /// @param state [in] The stepping state (thread-local cache)
0469   /// @return Charge over momentum (q/p) value from the reduced component state
0470   double qOverP(const State& state) const { return Reducer::qOverP(state); }
0471 
0472   /// Absolute momentum accessor
0473   ///
0474   /// @param state [in] The stepping state (thread-local cache)
0475   /// @return Absolute momentum magnitude from the reduced component state
0476   double absoluteMomentum(const State& state) const {
0477     return Reducer::absoluteMomentum(state);
0478   }
0479 
0480   /// Momentum accessor
0481   ///
0482   /// @param state [in] The stepping state (thread-local cache)
0483   /// @return Momentum vector from the reduced component state
0484   Vector3 momentum(const State& state) const {
0485     return Reducer::momentum(state);
0486   }
0487 
0488   /// Charge access
0489   ///
0490   /// @param state [in] The stepping state (thread-local cache)
0491   /// @return Electric charge value from the reduced component state
0492   double charge(const State& state) const { return Reducer::charge(state); }
0493 
0494   /// Particle hypothesis
0495   ///
0496   /// @param state [in] The stepping state (thread-local cache)
0497   /// @return Particle hypothesis used for this multi-component state
0498   ParticleHypothesis particleHypothesis(const State& state) const {
0499     return state.particleHypothesis;
0500   }
0501 
0502   /// Time access
0503   ///
0504   /// @param state [in] The stepping state (thread-local cache)
0505   /// @return Time coordinate from the reduced component state
0506   double time(const State& state) const { return Reducer::time(state); }
0507 
0508   /// Update surface status
0509   ///
0510   /// It checks the status to the reference surface & updates
0511   /// the step size accordingly
0512   ///
0513   /// @param [in,out] state The stepping state (thread-local cache)
0514   /// @param [in] surface The surface provided
0515   /// @param [in] index The surface intersection index
0516   /// @param [in] navDir The navigation direction
0517   /// @param [in] boundaryTolerance The boundary check for this status update
0518   /// @param [in] surfaceTolerance Surface tolerance used for intersection
0519   /// @param [in] stype The step size type to be set
0520   /// @param [in] logger A @c Logger instance
0521   /// @return IntersectionStatus indicating the overall status of all components relative to the surface
0522   IntersectionStatus updateSurfaceStatus(
0523       State& state, const Surface& surface, std::uint8_t index,
0524       Direction navDir, const BoundaryTolerance& boundaryTolerance,
0525       double surfaceTolerance, ConstrainedStep::Type stype,
0526       const Logger& logger = getDummyLogger()) const {
0527     using Status = IntersectionStatus;
0528 
0529     std::array<int, 3> counts = {0, 0, 0};
0530 
0531     for (auto& component : state.components) {
0532       component.status = detail::updateSingleSurfaceStatus<SingleStepper>(
0533           *this, component.state, surface, index, navDir, boundaryTolerance,
0534           surfaceTolerance, stype, logger);
0535       ++counts[static_cast<std::size_t>(component.status)];
0536     }
0537 
0538     // If at least one component is on a surface, we can remove all missed
0539     // components before the step. If not, we must keep them for the case that
0540     // all components miss and we need to retarget
0541     if (counts[static_cast<std::size_t>(Status::onSurface)] > 0) {
0542       removeMissedComponents(state);
0543       reweightComponents(state);
0544     }
0545 
0546     ACTS_VERBOSE("Component status wrt "
0547                  << surface.geometryId() << " at {"
0548                  << surface.center(state.options.geoContext).transpose()
0549                  << "}:\t" << [&]() {
0550                       std::stringstream ss;
0551                       for (auto& component : state.components) {
0552                         ss << component.status << "\t";
0553                       }
0554                       return ss.str();
0555                     }());
0556 
0557     // Switch on stepCounter if one or more components reached a surface, but
0558     // some are still in progress of reaching the surface
0559     if (!state.stepCounterAfterFirstComponentOnSurface &&
0560         counts[static_cast<std::size_t>(Status::onSurface)] > 0 &&
0561         counts[static_cast<std::size_t>(Status::reachable)] > 0) {
0562       state.stepCounterAfterFirstComponentOnSurface = 0;
0563       ACTS_VERBOSE("started stepCounterAfterFirstComponentOnSurface");
0564     }
0565 
0566     // If there are no components onSurface, but the counter is switched on
0567     // (e.g., if the navigator changes the target surface), we need to switch it
0568     // off again
0569     if (state.stepCounterAfterFirstComponentOnSurface &&
0570         counts[static_cast<std::size_t>(Status::onSurface)] == 0) {
0571       state.stepCounterAfterFirstComponentOnSurface.reset();
0572       ACTS_VERBOSE("switch off stepCounterAfterFirstComponentOnSurface");
0573     }
0574 
0575     // This is a 'any_of' criterium. As long as any of the components has a
0576     // certain state, this determines the total state (in the order of a
0577     // somewhat importance)
0578     if (counts[static_cast<std::size_t>(Status::reachable)] > 0) {
0579       return Status::reachable;
0580     } else if (counts[static_cast<std::size_t>(Status::onSurface)] > 0) {
0581       state.stepCounterAfterFirstComponentOnSurface.reset();
0582       return Status::onSurface;
0583     } else {
0584       return Status::unreachable;
0585     }
0586   }
0587 
0588   /// Update step size
0589   ///
0590   /// This method intersects the provided surface and update the navigation
0591   /// step estimation accordingly (hence it changes the state). It also
0592   /// returns the status of the intersection to trigger onSurface in case
0593   /// the surface is reached.
0594   ///
0595   /// @param state [in,out] The stepping state (thread-local cache)
0596   /// @param oIntersection [in] The ObjectIntersection to layer, boundary, etc
0597   /// @param direction [in] The propagation direction
0598   /// @param stype [in] The step size type to be set
0599   template <typename object_intersection_t>
0600   void updateStepSize(State& state, const object_intersection_t& oIntersection,
0601                       Direction direction, ConstrainedStep::Type stype) const {
0602     const Surface& surface = *oIntersection.object();
0603 
0604     for (auto& component : state.components) {
0605       auto intersection = surface.intersect(
0606           component.state.options.geoContext,
0607           SingleStepper::position(component.state),
0608           direction * SingleStepper::direction(component.state),
0609           BoundaryTolerance::None())[oIntersection.index()];
0610 
0611       SingleStepper::updateStepSize(component.state, intersection, direction,
0612                                     stype);
0613     }
0614   }
0615 
0616   /// Update step size - explicitly with a double
0617   ///
0618   /// @param state [in,out] The stepping state (thread-local cache)
0619   /// @param stepSize [in] The step size value
0620   /// @param stype [in] The step size type to be set
0621   void updateStepSize(State& state, double stepSize,
0622                       ConstrainedStep::Type stype) const {
0623     for (auto& component : state.components) {
0624       SingleStepper::updateStepSize(component.state, stepSize, stype);
0625     }
0626   }
0627 
0628   /// Get the step size
0629   ///
0630   /// @param state [in] The stepping state (thread-local cache)
0631   /// @param stype [in] The step size type to be returned
0632   /// @note This returns the smallest step size of all components. It uses
0633   /// std::abs for comparison to handle backward propagation and negative
0634   /// step sizes correctly.
0635   /// @return Smallest step size among all components for the requested type
0636   double getStepSize(const State& state, ConstrainedStep::Type stype) const {
0637     return std::ranges::min_element(
0638                state.components,
0639                [=](const auto& a, const auto& b) {
0640                  return std::abs(a.state.stepSize.value(stype)) <
0641                         std::abs(b.state.stepSize.value(stype));
0642                })
0643         ->state.stepSize.value(stype);
0644   }
0645 
0646   /// Release the step-size for all components
0647   ///
0648   /// @param state [in,out] The stepping state (thread-local cache)
0649   /// @param [in] stype The step size type to be released
0650   void releaseStepSize(State& state, ConstrainedStep::Type stype) const {
0651     for (auto& component : state.components) {
0652       SingleStepper::releaseStepSize(component.state, stype);
0653     }
0654   }
0655 
0656   /// Output the Step Size of all components into one std::string
0657   ///
0658   /// @param state [in,out] The stepping state (thread-local cache)
0659   /// @return String representation of all component step sizes concatenated
0660   std::string outputStepSize(const State& state) const {
0661     std::stringstream ss;
0662     for (const auto& component : state.components) {
0663       ss << component.state.stepSize.toString() << " || ";
0664     }
0665 
0666     return ss.str();
0667   }
0668 
0669   /// Create and return the bound state at the current position
0670   ///
0671   /// @brief This transports (if necessary) the covariance
0672   /// to the surface and creates a bound state. It does not check
0673   /// if the transported state is at the surface, this needs to
0674   /// be guaranteed by the propagator.
0675   /// @note This is done by combining the gaussian mixture on the specified
0676   /// surface. If the conversion to bound states of some components
0677   /// fails, these components are ignored unless all components fail. In this
0678   /// case an error code is returned.
0679   ///
0680   /// @param [in] state State that will be presented as @c BoundState
0681   /// @param [in] surface The surface to which we bind the state
0682   /// @param [in] transportCov Flag steering covariance transport
0683   /// @param [in] freeToBoundCorrection Flag steering non-linear correction during global to local correction
0684   ///
0685   /// @return A bound state:
0686   ///   - the parameters at the surface
0687   ///   - the stepwise jacobian towards it (from last bound)
0688   ///   - and the path length (from start - for ordering)
0689   Result<BoundState> boundState(
0690       State& state, const Surface& surface, bool transportCov = true,
0691       const FreeToBoundCorrection& freeToBoundCorrection =
0692           FreeToBoundCorrection(false)) const;
0693 
0694   /// @brief If necessary fill additional members needed for curvilinearState
0695   ///
0696   /// Compute path length derivatives in case they have not been computed
0697   /// yet, which is the case if no step has been executed yet.
0698   ///
0699   /// @param [in, out] state The stepping state (thread-local cache)
0700   /// @return true if nothing is missing after this call, false otherwise.
0701   bool prepareCurvilinearState(State& state) const {
0702     static_cast<void>(state);
0703     return true;
0704   }
0705 
0706   /// Create and return a curvilinear state at the current position
0707   ///
0708   /// @brief This transports (if necessary) the covariance
0709   /// to the current position and creates a curvilinear state.
0710   /// @note This is done as a simple average over the free representation
0711   /// and covariance of the components.
0712   ///
0713   /// @param [in] state State that will be presented as @c CurvilinearState
0714   /// @param [in] transportCov Flag steering covariance transport
0715   ///
0716   /// @return A curvilinear state:
0717   ///   - the curvilinear parameters at given position
0718   ///   - the stepweise jacobian towards it (from last bound)
0719   ///   - and the path length (from start - for ordering)
0720   BoundState curvilinearState(State& state, bool transportCov = true) const;
0721 
0722   /// Method for on-demand transport of the covariance
0723   /// to a new curvilinear frame at current  position,
0724   /// or direction of the state
0725   ///
0726   /// @param [in,out] state State of the stepper
0727   void transportCovarianceToCurvilinear(State& state) const {
0728     for (auto& component : state.components) {
0729       SingleStepper::transportCovarianceToCurvilinear(component.state);
0730     }
0731   }
0732 
0733   /// Method for on-demand transport of the covariance
0734   /// to a new curvilinear frame at current position,
0735   /// or direction of the state
0736   ///
0737   /// @tparam surface_t the Surface type
0738   ///
0739   /// @param [in,out] state State of the stepper
0740   /// @param [in] surface is the surface to which the covariance is forwarded
0741   /// @param [in] freeToBoundCorrection Flag steering non-linear correction during global to local correction
0742   /// to
0743   /// @note no check is done if the position is actually on the surface
0744   void transportCovarianceToBound(
0745       State& state, const Surface& surface,
0746       const FreeToBoundCorrection& freeToBoundCorrection =
0747           FreeToBoundCorrection(false)) const {
0748     for (auto& component : state.components) {
0749       SingleStepper::transportCovarianceToBound(component.state, surface,
0750                                                 freeToBoundCorrection);
0751     }
0752   }
0753 
0754   /// Perform a Runge-Kutta track parameter propagation step
0755   ///
0756   /// @param [in,out] state The state of the stepper
0757   /// @param propDir is the direction of propagation
0758   /// @param material is the material properties
0759   /// @return the result of the step
0760   ///
0761   /// The state contains the desired step size. It can be negative during
0762   /// backwards track propagation, and since we're using an adaptive
0763   /// algorithm, it can be modified by the stepper class during propagation.
0764   Result<double> step(State& state, Direction propDir,
0765                       const IVolumeMaterial* material) const;
0766 };
0767 
0768 }  // namespace Acts
0769 
0770 #include "MultiStepperLoop.ipp"