Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-05-25 07:47:09

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/Common.hpp"
0013 #include "Acts/EventData/MeasurementHelpers.hpp"
0014 #include "Acts/EventData/MultiTrajectory.hpp"
0015 #include "Acts/EventData/SourceLink.hpp"
0016 #include "Acts/EventData/TrackParameters.hpp"
0017 #include "Acts/EventData/TrackProxyConcept.hpp"
0018 #include "Acts/EventData/Types.hpp"
0019 #include "Acts/EventData/VectorMultiTrajectory.hpp"
0020 #include "Acts/EventData/VectorTrackContainer.hpp"
0021 #include "Acts/EventData/detail/CorrectedTransformationFreeToBound.hpp"
0022 #include "Acts/Geometry/GeometryContext.hpp"
0023 #include "Acts/Geometry/TrackingVolume.hpp"
0024 #include "Acts/MagneticField/MagneticFieldContext.hpp"
0025 #include "Acts/Material/Interactions.hpp"
0026 #include "Acts/Propagator/DirectNavigator.hpp"
0027 #include "Acts/Propagator/PropagatorOptions.hpp"
0028 #include "Acts/Propagator/StandardAborters.hpp"
0029 #include "Acts/Propagator/detail/PointwiseMaterialInteraction.hpp"
0030 #include "Acts/TrackFitting/GlobalChiSquareFitterError.hpp"
0031 #include "Acts/TrackFitting/detail/VoidFitterComponents.hpp"
0032 #include "Acts/Utilities/CalibrationContext.hpp"
0033 #include "Acts/Utilities/Delegate.hpp"
0034 #include "Acts/Utilities/Logger.hpp"
0035 #include "Acts/Utilities/Result.hpp"
0036 #include "Acts/Utilities/TrackHelpers.hpp"
0037 
0038 #include <functional>
0039 #include <limits>
0040 #include <memory>
0041 #include <type_traits>
0042 #include <unordered_map>
0043 
0044 namespace Acts::Experimental {
0045 
0046 /// @addtogroup track_fitting
0047 /// @{
0048 
0049 namespace Gx2fConstants {
0050 constexpr std::string_view gx2fnUpdateColumn = "Gx2fnUpdateColumn";
0051 
0052 // Mask for the track states. We don't need Predicted and Filtered
0053 constexpr TrackStatePropMask trackStateMask = TrackStatePropMask::Smoothed |
0054                                               TrackStatePropMask::Jacobian |
0055                                               TrackStatePropMask::Calibrated;
0056 
0057 // A projector used for scattering. By using Jacobian * phiThetaProjector one
0058 // gets only the derivatives for the variables phi and theta.
0059 const Eigen::Matrix<double, eBoundSize, 2> phiThetaProjector = [] {
0060   Eigen::Matrix<double, eBoundSize, 2> m =
0061       Eigen::Matrix<double, eBoundSize, 2>::Zero();
0062   m(eBoundPhi, 0) = 1.0;
0063   m(eBoundTheta, 1) = 1.0;
0064   return m;
0065 }();
0066 }  // namespace Gx2fConstants
0067 
0068 /// Extension struct which holds delegates to customise the GX2F behaviour
0069 template <typename traj_t>
0070 struct Gx2FitterExtensions {
0071   /// Type alias for mutable track state proxy from multi-trajectory
0072   using TrackStateProxy = typename MultiTrajectory<traj_t>::TrackStateProxy;
0073   /// Type alias for const track state proxy from multi-trajectory
0074   using ConstTrackStateProxy =
0075       typename MultiTrajectory<traj_t>::ConstTrackStateProxy;
0076   /// Type alias for track parameters from track state proxy
0077   using Parameters = typename TrackStateProxy::Parameters;
0078 
0079   /// Type alias for calibrator delegate to process measurements
0080   using Calibrator =
0081       Delegate<void(const GeometryContext&, const CalibrationContext&,
0082                     const SourceLink&, TrackStateProxy)>;
0083 
0084   /// Type alias for updater delegate to incorporate measurements into track
0085   /// parameters
0086   using Updater = Delegate<Result<void>(const GeometryContext&, TrackStateProxy,
0087                                         const Logger&)>;
0088 
0089   /// Type alias for outlier finder delegate to identify measurement outliers
0090   using OutlierFinder = Delegate<bool(ConstTrackStateProxy)>;
0091 
0092   /// The Calibrator is a dedicated calibration algorithm that allows
0093   /// to calibrate measurements using track information, this could be
0094   /// e.g. sagging for wires, module deformations, etc.
0095   Calibrator calibrator;
0096 
0097   /// The updater incorporates measurement information into the track parameters
0098   Updater updater;
0099 
0100   /// Determines whether a measurement is supposed to be considered as an
0101   /// outlier
0102   OutlierFinder outlierFinder;
0103 
0104   /// Retrieves the associated surface from a source link
0105   SourceLinkSurfaceAccessor surfaceAccessor;
0106 
0107   /// Default constructor which connects the default void components
0108   Gx2FitterExtensions() {
0109     calibrator.template connect<&Acts::detail::voidFitterCalibrator<traj_t>>();
0110     updater.template connect<&Acts::detail::voidFitterUpdater<traj_t>>();
0111     outlierFinder.template connect<&Acts::detail::voidOutlierFinder<traj_t>>();
0112     surfaceAccessor.connect<&Acts::detail::voidSurfaceAccessor>();
0113   }
0114 };
0115 
0116 /// Combined options for the Global-Chi-Square fitter.
0117 ///
0118 /// @tparam traj_t The trajectory type
0119 template <typename traj_t>
0120 struct Gx2FitterOptions {
0121   /// PropagatorOptions with context.
0122   ///
0123   /// @param gctx The geometry context for this fit
0124   /// @param mctx The magnetic context for this fit
0125   /// @param cctx The calibration context for this fit
0126   /// @param extensions_ The KF extensions
0127   /// @param pOptions The plain propagator options
0128   /// @param rSurface The reference surface for the fit to be expressed at
0129   /// @param mScattering Whether to include multiple scattering
0130   /// @param eLoss Whether to include energy loss
0131   /// @param freeToBoundCorrection_ Correction for non-linearity effect during transform from free to bound
0132   /// @param nUpdateMax_ Max number of iterations for updating the parameters
0133   /// @param relChi2changeCutOff_ Check for convergence (abort condition). Set to 0 to skip.
0134   Gx2FitterOptions(const GeometryContext& gctx,
0135                    const MagneticFieldContext& mctx,
0136                    std::reference_wrapper<const CalibrationContext> cctx,
0137                    Gx2FitterExtensions<traj_t> extensions_,
0138                    const PropagatorPlainOptions& pOptions,
0139                    const Surface* rSurface = nullptr, bool mScattering = false,
0140                    bool eLoss = false,
0141                    const FreeToBoundCorrection& freeToBoundCorrection_ =
0142                        FreeToBoundCorrection(false),
0143                    const std::size_t nUpdateMax_ = 5,
0144                    double relChi2changeCutOff_ = 1e-5)
0145       : geoContext(gctx),
0146         magFieldContext(mctx),
0147         calibrationContext(cctx),
0148         extensions(extensions_),
0149         propagatorPlainOptions(pOptions),
0150         referenceSurface(rSurface),
0151         multipleScattering(mScattering),
0152         energyLoss(eLoss),
0153         freeToBoundCorrection(freeToBoundCorrection_),
0154         nUpdateMax(nUpdateMax_),
0155         relChi2changeCutOff(relChi2changeCutOff_) {}
0156 
0157   /// Contexts are required and the options must not be default-constructible.
0158   Gx2FitterOptions() = delete;
0159 
0160   /// Context object for the geometry
0161   std::reference_wrapper<const GeometryContext> geoContext;
0162   /// Context object for the magnetic field
0163   std::reference_wrapper<const MagneticFieldContext> magFieldContext;
0164   /// context object for the calibration
0165   std::reference_wrapper<const CalibrationContext> calibrationContext;
0166 
0167   /// Extensions for calibration and outlier finding
0168   Gx2FitterExtensions<traj_t> extensions;
0169 
0170   /// The trivial propagator options
0171   PropagatorPlainOptions propagatorPlainOptions;
0172 
0173   /// The reference Surface
0174   const Surface* referenceSurface = nullptr;
0175 
0176   /// Whether to consider multiple scattering
0177   bool multipleScattering = false;
0178 
0179   /// Whether to consider energy loss
0180   bool energyLoss = false;
0181 
0182   /// Whether to include non-linear correction during global to local
0183   /// transformation
0184   FreeToBoundCorrection freeToBoundCorrection;
0185 
0186   /// Max number of iterations during the fit (abort condition)
0187   std::size_t nUpdateMax = 5;
0188 
0189   /// Check for convergence (abort condition). Set to 0 to skip.
0190   double relChi2changeCutOff = 1e-7;
0191 };
0192 
0193 /// Result container for a global chi-square fit.
0194 template <typename traj_t>
0195 struct Gx2FitterResult {
0196   /// Fitted states that the actor has handled.
0197   traj_t* fittedStates{nullptr};
0198 
0199   /// This is the index of the 'tip' of the track stored in multitrajectory.
0200   /// This corresponds to the last measurement state in the multitrajectory.
0201   /// Since this KF only stores one trajectory, it is unambiguous.
0202   /// Acts::TrackTraits::kInvalid is the start of a trajectory.
0203   std::size_t lastMeasurementIndex = Acts::kTrackIndexInvalid;
0204 
0205   /// This is the index of the 'tip' of the states stored in multitrajectory.
0206   /// This corresponds to the last state in the multitrajectory.
0207   /// Since this KF only stores one trajectory, it is unambiguous.
0208   /// Acts::TrackTraits::kInvalid is the start of a trajectory.
0209   std::size_t lastTrackIndex = Acts::kTrackIndexInvalid;
0210 
0211   /// The optional Parameters at the provided surface
0212   std::optional<BoundTrackParameters> fittedParameters;
0213 
0214   /// Counter for states with non-outlier measurements
0215   std::size_t measurementStates = 0;
0216 
0217   /// Counter for measurements holes
0218   /// A hole correspond to a surface with an associated detector element with no
0219   /// associated measurement. Holes are only taken into account if they are
0220   /// between the first and last measurements.
0221   std::size_t measurementHoles = 0;
0222 
0223   /// Counter for handled states
0224   std::size_t processedStates = 0;
0225 
0226   /// Counter for handled measurements
0227   std::size_t processedMeasurements = 0;
0228 
0229   /// Indicator if track fitting has been done
0230   bool finished = false;
0231 
0232   /// Measurement surfaces without hits
0233   std::vector<const Surface*> missedActiveSurfaces;
0234 
0235   /// Measurement surfaces handled in both forward and
0236   /// backward filtering
0237   std::vector<const Surface*> passedAgainSurfaces;
0238 
0239   /// Count how many surfaces have been hit
0240   std::size_t surfaceCount = 0;
0241 };
0242 
0243 /// @brief A container to store scattering properties for each material surface
0244 ///
0245 /// This struct holds the scattering angles, the inverse covariance of the
0246 /// material, and a validity flag indicating whether the material is valid for
0247 /// the scattering process.
0248 struct ScatteringProperties {
0249  public:
0250   /// @brief Constructor to initialize scattering properties.
0251   ///
0252   /// @param scatteringAngles_ The vector of scattering angles.
0253   /// @param invCovarianceMaterial_ The inverse covariance of the material.
0254   /// @param materialIsValid_ A boolean flag indicating whether the material is valid.
0255   ScatteringProperties(const BoundVector& scatteringAngles_,
0256                        const double invCovarianceMaterial_,
0257                        const bool materialIsValid_)
0258       : m_scatteringAngles(scatteringAngles_),
0259         m_invCovarianceMaterial(invCovarianceMaterial_),
0260         m_materialIsValid(materialIsValid_) {}
0261 
0262   /// @brief Accessor for the scattering angles (const version)
0263   /// @return Const reference to the vector of scattering angles
0264   const BoundVector& scatteringAngles() const { return m_scatteringAngles; }
0265 
0266   /// @brief Accessor for the scattering angles (mutable version)
0267   /// @return Mutable reference to the vector of scattering angles for modification
0268   BoundVector& scatteringAngles() { return m_scatteringAngles; }
0269 
0270   /// @brief Accessor for the inverse covariance of the material
0271   /// @return Inverse covariance value computed from material properties (e.g., Highland formula)
0272   double invCovarianceMaterial() const { return m_invCovarianceMaterial; }
0273 
0274   /// @brief Accessor for the material validity flag
0275   /// @return True if material is valid for scattering calculations, false for vacuum or zero thickness
0276   bool materialIsValid() const { return m_materialIsValid; }
0277 
0278  private:
0279   /// Vector of scattering angles. The vector is usually all zeros except for
0280   /// eBoundPhi and eBoundTheta.
0281   BoundVector m_scatteringAngles;
0282 
0283   /// Inverse covariance of the material. Compute with e.g. the Highland
0284   /// formula.
0285   double m_invCovarianceMaterial;
0286 
0287   /// Flag indicating whether the material is valid. Commonly vacuum and zero
0288   /// thickness material will be ignored.
0289   bool m_materialIsValid;
0290 };
0291 
0292 /// @brief A container to manage all properties of a gx2f system
0293 ///
0294 /// This struct manages the mathematical infrastructure for the gx2f. It
0295 /// initializes and maintains the extended aMatrix and extended bVector.
0296 struct Gx2fSystem {
0297  public:
0298   /// @brief Constructor to initialize matrices and vectors to zero based on specified dimensions.
0299   ///
0300   /// @param nDims Number of dimensions for the extended matrix and vector.
0301   explicit Gx2fSystem(std::size_t nDims)
0302       : m_nDims{nDims},
0303         m_aMatrix{Eigen::MatrixXd::Zero(nDims, nDims)},
0304         m_bVector{Eigen::VectorXd::Zero(nDims)} {}
0305 
0306   /// @brief Accessor for the number of dimensions of the extended system
0307   /// @return Number of dimensions for the aMatrix and bVector (bound parameters + scattering angles)
0308   std::size_t nDims() const { return m_nDims; }
0309 
0310   /// @brief Accessor for the accumulated chi-squared value (const version)
0311   /// @return Current sum of chi-squared contributions from measurements and material
0312   double chi2() const { return m_chi2; }
0313 
0314   /// @brief Accessor for the accumulated chi-squared value (mutable version)
0315   /// @return Mutable reference to chi-squared sum for modification during fitting
0316   double& chi2() { return m_chi2; }
0317 
0318   /// @brief Accessor for the extended system matrix (const version)
0319   /// @return Const reference to the aMatrix containing measurement and material contributions
0320   const Eigen::MatrixXd& aMatrix() const { return m_aMatrix; }
0321 
0322   /// @brief Accessor for the extended system matrix (mutable version)
0323   /// @return Mutable reference to the aMatrix for adding measurement and material contributions
0324   Eigen::MatrixXd& aMatrix() { return m_aMatrix; }
0325 
0326   /// @brief Accessor for the extended system vector (const version)
0327   /// @return Const reference to the bVector containing measurement and material contributions
0328   const Eigen::VectorXd& bVector() const { return m_bVector; }
0329 
0330   /// @brief Accessor for the extended system vector (mutable version)
0331   /// @return Mutable reference to the bVector for adding measurement and material contributions
0332   Eigen::VectorXd& bVector() { return m_bVector; }
0333 
0334   /// @brief Accessor for the number of degrees of freedom (const version)
0335   /// @return Current number of degrees of freedom from processed measurements
0336   std::size_t ndf() const { return m_ndf; }
0337 
0338   /// @brief Accessor for the number of degrees of freedom (mutable version)
0339   /// @return Mutable reference to NDF counter for incrementing during measurement processing
0340   std::size_t& ndf() { return m_ndf; }
0341 
0342   /// @brief Determines the minimum number of degrees of freedom required for the fit
0343   ///
0344   /// Automatically deduces the required NDF based on the system configuration.
0345   /// We have only 3 cases, because we always have l0, l1, phi, theta:
0346   /// - 4: no magnetic field -> q/p is empty
0347   /// - 5: no time measurement -> time is not fittable
0348   /// - 6: full fit with all parameters
0349   ///
0350   /// @return Required NDF based on which parameters can be fitted
0351   std::size_t findRequiredNdf() {
0352     std::size_t ndfSystem = 0;
0353     if (m_aMatrix(4, 4) == 0) {
0354       ndfSystem = 4;
0355     } else if (m_aMatrix(5, 5) == 0) {
0356       ndfSystem = 5;
0357     } else {
0358       ndfSystem = 6;
0359     }
0360 
0361     return ndfSystem;
0362   }
0363 
0364   /// @brief Checks if the system has sufficient degrees of freedom for fitting
0365   /// @return True if NDF exceeds the minimum required for the parameter configuration
0366   bool isWellDefined() { return m_ndf > findRequiredNdf(); }
0367 
0368  private:
0369   /// Number of dimensions of the (extended) system
0370   std::size_t m_nDims;
0371 
0372   /// Sum of chi-squared values.
0373   double m_chi2 = 0.;
0374 
0375   /// Extended matrix for accumulation.
0376   Eigen::MatrixXd m_aMatrix;
0377 
0378   /// Extended vector for accumulation.
0379   Eigen::VectorXd m_bVector;
0380 
0381   /// Number of degrees of freedom of the system
0382   std::size_t m_ndf = 0u;
0383 };
0384 
0385 /// @brief Adds a measurement to the GX2F equation system in a modular backend function.
0386 ///
0387 /// This function processes measurement data and integrates it into the GX2F
0388 /// system.
0389 ///
0390 /// @param extendedSystem All parameters of the current equation system to update.
0391 /// @param jacobianFromStart The Jacobian matrix from the start to the current state.
0392 /// @param covarianceMeasurement The covariance matrix of the measurement.
0393 /// @param predicted The predicted state vector based on the track state.
0394 /// @param measurement The measurement vector.
0395 /// @param projector The projection matrix.
0396 /// @param logger A logger instance.
0397 ///
0398 /// @note The dynamic Eigen matrices are suboptimal. We could think of
0399 /// templating again in the future on kMeasDims. We currently use dynamic
0400 /// matrices to reduce the memory during compile time.
0401 void addMeasurementToGx2fSumsBackend(
0402     Gx2fSystem& extendedSystem,
0403     const std::vector<BoundMatrix>& jacobianFromStart,
0404     const Eigen::MatrixXd& covarianceMeasurement, const BoundVector& predicted,
0405     const Eigen::VectorXd& measurement, const Eigen::MatrixXd& projector,
0406     const Logger& logger);
0407 
0408 /// @brief Process measurements and fill the aMatrix and bVector
0409 ///
0410 /// The function processes each measurement for the GX2F Actor fitting process.
0411 /// It extracts the information from the track state and adds it to aMatrix,
0412 /// bVector, and chi2sum.
0413 ///
0414 /// @tparam kMeasDim Number of dimensions of the measurement
0415 /// @tparam track_state_t The type of the track state
0416 ///
0417 /// @param extendedSystem All parameters of the current equation system to update
0418 /// @param jacobianFromStart The Jacobian matrix from start to the current state
0419 /// @param trackState The track state to analyse
0420 /// @param logger A logger instance
0421 template <std::size_t kMeasDim, typename track_state_t>
0422 void addMeasurementToGx2fSums(Gx2fSystem& extendedSystem,
0423                               const std::vector<BoundMatrix>& jacobianFromStart,
0424                               const track_state_t& trackState,
0425                               const Logger& logger) {
0426   const ActsSquareMatrix<kMeasDim> covarianceMeasurement =
0427       trackState.template calibratedCovariance<kMeasDim>();
0428 
0429   const BoundVector predicted = trackState.smoothed();
0430 
0431   const ActsVector<kMeasDim> measurement =
0432       trackState.template calibrated<kMeasDim>();
0433 
0434   const ActsMatrix<kMeasDim, eBoundSize> projector =
0435       trackState.template projectorSubspaceHelper<kMeasDim>().projector();
0436 
0437   addMeasurementToGx2fSumsBackend(extendedSystem, jacobianFromStart,
0438                                   covarianceMeasurement, predicted, measurement,
0439                                   projector, logger);
0440 }
0441 
0442 /// @brief Process material and fill the aMatrix and bVector
0443 ///
0444 /// The function processes each material for the GX2F Actor fitting process.
0445 /// It extracts the information from the track state and adds it to aMatrix,
0446 /// bVector, and chi2sum.
0447 ///
0448 /// @tparam track_state_t The type of the track state
0449 ///
0450 /// @param extendedSystem All parameters of the current equation system
0451 /// @param nMaterialsHandled How many materials we already handled. Used for the offset.
0452 /// @param scatteringMap The scattering map, containing all scattering angles and covariances
0453 /// @param trackState The track state to analyse
0454 /// @param logger A logger instance
0455 template <typename track_state_t>
0456 void addMaterialToGx2fSums(
0457     Gx2fSystem& extendedSystem, const std::size_t nMaterialsHandled,
0458     const std::unordered_map<GeometryIdentifier, ScatteringProperties>&
0459         scatteringMap,
0460     const track_state_t& trackState, const Logger& logger) {
0461   // Get and store geoId for the current material surface
0462   const GeometryIdentifier geoId = trackState.referenceSurface().geometryId();
0463   const auto scatteringMapId = scatteringMap.find(geoId);
0464   if (scatteringMapId == scatteringMap.end()) {
0465     ACTS_ERROR("No scattering angles found for material surface " << geoId);
0466     throw std::runtime_error(
0467         "No scattering angles found for material surface.");
0468   }
0469 
0470   const double sinThetaLoc = std::sin(trackState.smoothed()[eBoundTheta]);
0471 
0472   // The position, where we need to insert the values in aMatrix and bVector
0473   const std::size_t deltaPosition = eBoundSize + 2 * nMaterialsHandled;
0474 
0475   const BoundVector& scatteringAngles =
0476       scatteringMapId->second.scatteringAngles();
0477 
0478   const double invCov = scatteringMapId->second.invCovarianceMaterial();
0479 
0480   // Phi contribution
0481   extendedSystem.aMatrix()(deltaPosition, deltaPosition) +=
0482       invCov * sinThetaLoc * sinThetaLoc;
0483   extendedSystem.bVector()(deltaPosition, 0) -=
0484       invCov * scatteringAngles[eBoundPhi] * sinThetaLoc;
0485   extendedSystem.chi2() += invCov * scatteringAngles[eBoundPhi] * sinThetaLoc *
0486                            scatteringAngles[eBoundPhi] * sinThetaLoc;
0487 
0488   // Theta Contribution
0489   extendedSystem.aMatrix()(deltaPosition + 1, deltaPosition + 1) += invCov;
0490   extendedSystem.bVector()(deltaPosition + 1, 0) -=
0491       invCov * scatteringAngles[eBoundTheta];
0492   extendedSystem.chi2() +=
0493       invCov * scatteringAngles[eBoundTheta] * scatteringAngles[eBoundTheta];
0494 
0495   ACTS_VERBOSE(
0496       "Contributions in addMaterialToGx2fSums:\n"
0497       << "    invCov:        " << invCov << "\n"
0498       << "    sinThetaLoc:   " << sinThetaLoc << "\n"
0499       << "    deltaPosition: " << deltaPosition << "\n"
0500       << "    Phi:\n"
0501       << "        scattering angle:     " << scatteringAngles[eBoundPhi] << "\n"
0502       << "        aMatrix contribution: " << invCov * sinThetaLoc * sinThetaLoc
0503       << "\n"
0504       << "        bVector contribution: "
0505       << invCov * scatteringAngles[eBoundPhi] * sinThetaLoc << "\n"
0506       << "        chi2sum contribution: "
0507       << invCov * scatteringAngles[eBoundPhi] * sinThetaLoc *
0508              scatteringAngles[eBoundPhi] * sinThetaLoc
0509       << "\n"
0510       << "    Theta:\n"
0511       << "        scattering angle:     " << scatteringAngles[eBoundTheta]
0512       << "\n"
0513       << "        aMatrix contribution: " << invCov << "\n"
0514       << "        bVector contribution: "
0515       << invCov * scatteringAngles[eBoundTheta] << "\n"
0516       << "        chi2sum contribution: "
0517       << invCov * scatteringAngles[eBoundTheta] * scatteringAngles[eBoundTheta]
0518       << "\n");
0519 
0520   return;
0521 }
0522 
0523 /// @brief Fill the GX2F system with data from a track
0524 ///
0525 /// This function processes a track proxy and updates the aMatrix, bVector, and
0526 /// chi2 values for the GX2F fitting system. It considers material only if
0527 /// multiple scattering is enabled.
0528 ///
0529 /// @tparam track_proxy_t The type of the track proxy
0530 ///
0531 /// @param track A constant track proxy to inspect
0532 /// @param extendedSystem All parameters of the current equation system
0533 /// @param multipleScattering Flag to consider multiple scattering in the calculation
0534 /// @param scatteringMap Map of geometry identifiers to scattering properties,
0535 ///        containing scattering angles and validation status
0536 /// @param geoIdVector A vector to store geometry identifiers for tracking processed elements
0537 /// @param logger A logger instance
0538 template <TrackProxyConcept track_proxy_t>
0539 void fillGx2fSystem(
0540     const track_proxy_t track, Gx2fSystem& extendedSystem,
0541     const bool multipleScattering,
0542     const std::unordered_map<GeometryIdentifier, ScatteringProperties>&
0543         scatteringMap,
0544     std::vector<GeometryIdentifier>& geoIdVector, const Logger& logger) {
0545   std::vector<BoundMatrix> jacobianFromStart;
0546   jacobianFromStart.emplace_back(BoundMatrix::Identity());
0547 
0548   for (const auto& trackState : track.trackStates()) {
0549     // Get and store geoId for the current surface
0550     const GeometryIdentifier geoId = trackState.referenceSurface().geometryId();
0551     ACTS_DEBUG("Start to investigate trackState on surface " << geoId);
0552     const auto typeFlags = trackState.typeFlags();
0553     const bool stateHasMeasurement = typeFlags.hasMeasurement();
0554     const bool stateHasMaterial = typeFlags.hasMaterial();
0555 
0556     // First we figure out, if we would need to look into material
0557     // surfaces at all. Later, we also check, if the material slab is
0558     // valid, otherwise we modify this flag to ignore the material
0559     // completely.
0560     bool doMaterial = multipleScattering && stateHasMaterial;
0561     if (doMaterial) {
0562       const auto scatteringMapId = scatteringMap.find(geoId);
0563       assert(scatteringMapId != scatteringMap.end() &&
0564              "No scattering angles found for material surface.");
0565       doMaterial = doMaterial && scatteringMapId->second.materialIsValid();
0566     }
0567 
0568     // We only consider states with a measurement (and/or material)
0569     if (!stateHasMeasurement && !doMaterial) {
0570       ACTS_DEBUG("    Skip state.");
0571       continue;
0572     }
0573 
0574     // update all Jacobians from start
0575     for (auto& jac : jacobianFromStart) {
0576       jac = trackState.jacobian() * jac;
0577     }
0578 
0579     // Handle measurement
0580     if (stateHasMeasurement) {
0581       ACTS_DEBUG("    Handle measurement.");
0582 
0583       const auto measDim = trackState.calibratedSize();
0584 
0585       if (measDim < 1 || 6 < measDim) {
0586         ACTS_ERROR("Can not process state with measurement with "
0587                    << measDim << " dimensions.");
0588         throw std::domain_error(
0589             "Found measurement with less than 1 or more than 6 dimension(s).");
0590       }
0591 
0592       extendedSystem.ndf() += measDim;
0593 
0594       visit_measurement(measDim, [&](auto N) {
0595         addMeasurementToGx2fSums<N>(extendedSystem, jacobianFromStart,
0596                                     trackState, logger);
0597       });
0598     }
0599 
0600     // Handle material
0601     if (doMaterial) {
0602       ACTS_DEBUG("    Handle material");
0603       // Add for this material a new Jacobian, starting from this surface.
0604       jacobianFromStart.emplace_back(BoundMatrix::Identity());
0605 
0606       // Add the material contribution to the system
0607       addMaterialToGx2fSums(extendedSystem, geoIdVector.size(), scatteringMap,
0608                             trackState, logger);
0609 
0610       geoIdVector.emplace_back(geoId);
0611     }
0612   }
0613 }
0614 
0615 /// @brief Count the valid material states in a track for scattering calculations.
0616 ///
0617 /// This function counts the valid material surfaces encountered in a track
0618 /// by examining each track state. The count is based on the presence of
0619 /// material flags and the availability of scattering information for each
0620 /// surface.
0621 ///
0622 /// @tparam track_proxy_t The type of the track proxy
0623 ///
0624 /// @param track A constant track proxy to inspect
0625 /// @param scatteringMap Map of geometry identifiers to scattering properties,
0626 ///        containing scattering angles and validation status
0627 /// @param logger A logger instance
0628 /// @return Number of valid material states in the track
0629 template <TrackProxyConcept track_proxy_t>
0630 std::size_t countMaterialStates(
0631     const track_proxy_t track,
0632     const std::unordered_map<GeometryIdentifier, ScatteringProperties>&
0633         scatteringMap,
0634     const Logger& logger) {
0635   std::size_t nMaterialSurfaces = 0;
0636   ACTS_DEBUG("Count the valid material surfaces.");
0637   for (const auto& trackState : track.trackStates()) {
0638     const auto typeFlags = trackState.typeFlags();
0639     const bool stateHasMaterial = typeFlags.hasMaterial();
0640 
0641     if (!stateHasMaterial) {
0642       continue;
0643     }
0644 
0645     // Get and store geoId for the current material surface
0646     const GeometryIdentifier geoId = trackState.referenceSurface().geometryId();
0647 
0648     const auto scatteringMapId = scatteringMap.find(geoId);
0649     assert(scatteringMapId != scatteringMap.end() &&
0650            "No scattering angles found for material surface.");
0651     if (!scatteringMapId->second.materialIsValid()) {
0652       continue;
0653     }
0654 
0655     nMaterialSurfaces++;
0656   }
0657 
0658   return nMaterialSurfaces;
0659 }
0660 
0661 /// @brief Solve the gx2f system to get the delta parameters for the update
0662 ///
0663 /// This function computes the delta parameters for the GX2F Actor fitting
0664 /// process by solving the linear equation system [a] * delta = b. It uses the
0665 /// column-pivoting Householder QR decomposition for numerical stability.
0666 ///
0667 /// @param extendedSystem All parameters of the current equation system
0668 /// @return Delta parameters for the GX2F update
0669 Eigen::VectorXd computeGx2fDeltaParams(const Gx2fSystem& extendedSystem);
0670 
0671 /// @brief Update parameters (and scattering angles if applicable)
0672 ///
0673 /// @param params Parameters to be updated
0674 /// @param deltaParamsExtended Delta parameters for bound parameter and scattering angles
0675 /// @param nMaterialSurfaces Number of material surfaces in the track
0676 /// @param scatteringMap Map of geometry identifiers to scattering properties,
0677 ///        containing all scattering angles and covariances
0678 /// @param geoIdVector Vector of geometry identifiers corresponding to material surfaces
0679 void updateGx2fParams(
0680     BoundTrackParameters& params, const Eigen::VectorXd& deltaParamsExtended,
0681     const std::size_t nMaterialSurfaces,
0682     std::unordered_map<GeometryIdentifier, ScatteringProperties>& scatteringMap,
0683     const std::vector<GeometryIdentifier>& geoIdVector);
0684 
0685 /// @brief Calculate and update the covariance of the fitted parameters
0686 ///
0687 /// This function calculates the covariance of the fitted parameters using
0688 /// cov = inv([a])
0689 /// It then updates the first square block of size ndfSystem. This ensures,
0690 /// that we only update the covariance for fitted parameters. (In case of
0691 /// no qop/time fit)
0692 ///
0693 /// @param fullCovariancePredicted The covariance matrix to update
0694 /// @param extendedSystem All parameters of the current equation system
0695 void updateGx2fCovarianceParams(BoundMatrix& fullCovariancePredicted,
0696                                 Gx2fSystem& extendedSystem);
0697 
0698 /// Global Chi Square fitter (GX2F) implementation.
0699 ///
0700 /// @tparam propagator_t Type of the propagation class
0701 ///
0702 /// TODO Write description
0703 template <typename propagator_t, typename traj_t>
0704 class Gx2Fitter {
0705   /// The navigator type
0706   using Gx2fNavigator = typename propagator_t::Navigator;
0707 
0708   /// The navigator has DirectNavigator type or not
0709   static constexpr bool isDirectNavigator =
0710       std::is_same_v<Gx2fNavigator, DirectNavigator>;
0711 
0712   static constexpr auto kInvalid = kTrackIndexInvalid;
0713 
0714  public:
0715   /// @brief Constructor for the Global Chi-Square Fitter
0716   ///
0717   /// Initializes the fitter with a propagator and optional logger.
0718   /// The fitter uses iterative fitting with a linear equation system
0719   /// to minimize chi-squared including multiple scattering effects.
0720   ///
0721   /// @param pPropagator The propagator instance for track propagation
0722   /// @param _logger Logger instance for debugging output (optional)
0723   explicit Gx2Fitter(propagator_t pPropagator,
0724                      std::unique_ptr<const Logger> _logger =
0725                          getDefaultLogger("Gx2Fitter", Logging::INFO))
0726       : m_propagator(std::move(pPropagator)),
0727         m_logger{std::move(_logger)},
0728         m_actorLogger{m_logger->cloneWithSuffix("Actor")},
0729         m_addToSumLogger{m_logger->cloneWithSuffix("AddToSum")} {}
0730 
0731  private:
0732   /// The propagator for the transport and material update
0733   propagator_t m_propagator;
0734 
0735   /// The logger instance
0736   std::unique_ptr<const Logger> m_logger;
0737   std::unique_ptr<const Logger> m_actorLogger;
0738   std::unique_ptr<const Logger> m_addToSumLogger;
0739 
0740   const Logger& logger() const { return *m_logger; }
0741 
0742   /// @brief Propagator Actor plugin for the GX2F
0743   ///
0744   /// @tparam parameters_t The type of parameters used for "local" parameters.
0745   /// @tparam calibrator_t The type of calibrator
0746   /// @tparam outlier_finder_t Type of the outlier finder class
0747   ///
0748   /// The GX2F Actor does not rely on the measurements to be sorted along the
0749   /// track.
0750   class Actor {
0751    public:
0752     /// Broadcast the result_type
0753     using result_type = Gx2FitterResult<traj_t>;
0754 
0755     /// The target surface
0756     const Surface* targetSurface = nullptr;
0757 
0758     /// Allows retrieving measurements for a surface
0759     const std::unordered_map<const Surface*, SourceLink>* inputMeasurements{};
0760 
0761     /// Whether to consider multiple scattering.
0762     bool multipleScattering = false;
0763 
0764     /// Whether to consider energy loss.
0765     bool energyLoss = false;  /// TODO implement later
0766 
0767     /// Whether to include non-linear correction during global to local
0768     /// transformation
0769     FreeToBoundCorrection freeToBoundCorrection;
0770 
0771     /// Input MultiTrajectory
0772     std::shared_ptr<MultiTrajectory<traj_t>> outputStates;
0773 
0774     /// The logger instance
0775     const Logger* actorLogger{nullptr};
0776 
0777     /// Logger helper
0778     const Logger& logger() const { return *actorLogger; }
0779 
0780     Gx2FitterExtensions<traj_t> extensions;
0781 
0782     /// The Surface being
0783     SurfaceReached targetReached;
0784 
0785     /// Calibration context for the fit
0786     const CalibrationContext* calibrationContext{nullptr};
0787 
0788     /// The particle hypothesis is needed for estimating scattering angles
0789     const BoundTrackParameters* parametersWithHypothesis = nullptr;
0790 
0791     /// The scatteringMap stores for each visited surface their scattering
0792     /// properties
0793     std::unordered_map<GeometryIdentifier, ScatteringProperties>*
0794         scatteringMap = nullptr;
0795 
0796     /// @brief Gx2f actor operation
0797     ///
0798     /// @tparam propagator_state_t is the type of Propagator state
0799     /// @tparam stepper_t Type of the stepper
0800     /// @tparam navigator_t Type of the navigator
0801     ///
0802     /// @param state is the mutable propagator state object
0803     /// @param stepper The stepper in use
0804     /// @param navigator The navigator in use
0805     /// @param result is the mutable result state object
0806     template <typename propagator_state_t, typename stepper_t,
0807               typename navigator_t>
0808     Result<void> act(propagator_state_t& state, const stepper_t& stepper,
0809                      const navigator_t& navigator, result_type& result,
0810                      const Logger& /*logger*/) const {
0811       assert(result.fittedStates && "No MultiTrajectory set");
0812 
0813       // Check if we can stop to propagate
0814       if (result.measurementStates == inputMeasurements->size()) {
0815         ACTS_DEBUG("Actor: finish: All measurements have been found.");
0816         result.finished = true;
0817       } else if (state.navigation.navigationBreak) {
0818         ACTS_DEBUG("Actor: finish: navigationBreak.");
0819         result.finished = true;
0820       }
0821 
0822       // End the propagation and return to the fitter
0823       if (result.finished) {
0824         // Remove the missing surfaces that occur after the last measurement
0825         if (result.measurementStates > 0) {
0826           result.missedActiveSurfaces.resize(result.measurementHoles);
0827         }
0828 
0829         return Result<void>::success();
0830       }
0831 
0832       // We are only interested in surfaces. If we are not on a surface, we
0833       // continue the navigation
0834       auto surface = navigator.currentSurface(state.navigation);
0835       if (surface == nullptr) {
0836         return Result<void>::success();
0837       }
0838 
0839       ++result.surfaceCount;
0840       const GeometryIdentifier geoId = surface->geometryId();
0841       ACTS_DEBUG("Surface " << geoId << " detected.");
0842 
0843       const bool surfaceIsSensitive = surface->isSensitive();
0844       const bool surfaceHasMaterial = (surface->surfaceMaterial() != nullptr);
0845       // First we figure out, if we would need to look into material surfaces at
0846       // all. Later, we also check, if the material slab is valid, otherwise we
0847       // modify this flag to ignore the material completely.
0848       bool doMaterial = multipleScattering && surfaceHasMaterial;
0849 
0850       // Found material - add a scatteringAngles entry if not done yet.
0851       // Handling will happen later
0852       if (doMaterial) {
0853         ACTS_DEBUG("    The surface contains material, ...");
0854 
0855         auto scatteringMapId = scatteringMap->find(geoId);
0856         if (scatteringMapId == scatteringMap->end()) {
0857           ACTS_DEBUG("    ... create entry in scattering map.");
0858 
0859           const MaterialSlab slab = Acts::detail::evaluateMaterialSlab(
0860               state, stepper, *surface,
0861               Acts::detail::determineMaterialUpdateMode(
0862                   state, navigator, MaterialUpdateMode::FullUpdate));
0863           const bool slabIsValid = !slab.isVacuum();
0864 
0865           double invSigma2 = 0.;
0866           if (slabIsValid) {
0867             const auto& particle =
0868                 parametersWithHypothesis->particleHypothesis();
0869 
0870             const double sigma =
0871                 static_cast<double>(Acts::computeMultipleScatteringTheta0(
0872                     slab, particle.absolutePdg(), particle.mass(),
0873                     static_cast<float>(
0874                         parametersWithHypothesis->parameters()[eBoundQOverP]),
0875                     particle.absoluteCharge()));
0876             ACTS_VERBOSE(
0877                 "        The Highland formula gives sigma = " << sigma);
0878             invSigma2 = 1. / std::pow(sigma, 2);
0879           } else {
0880             ACTS_VERBOSE("        Material slab is not valid.");
0881           }
0882 
0883           scatteringMap->emplace(
0884               geoId, ScatteringProperties{BoundVector::Zero(), invSigma2,
0885                                           slabIsValid});
0886           scatteringMapId = scatteringMap->find(geoId);
0887         } else {
0888           ACTS_DEBUG("    ... found entry in scattering map.");
0889         }
0890 
0891         doMaterial = doMaterial && scatteringMapId->second.materialIsValid();
0892       }
0893 
0894       // Here we handle all measurements
0895       if (auto sourceLinkIt = inputMeasurements->find(surface);
0896           sourceLinkIt != inputMeasurements->end()) {
0897         ACTS_DEBUG("    The surface contains a measurement.");
0898 
0899         // Transport the covariance to the surface
0900         stepper.transportCovarianceToBound(state.stepping, *surface,
0901                                            freeToBoundCorrection);
0902 
0903         // TODO generalize the update of the currentTrackIndex
0904         auto& fittedStates = *result.fittedStates;
0905 
0906         // Add a <trackStateMask> TrackState entry multi trajectory. This
0907         // allocates storage for all components, which we will set later.
0908         typename traj_t::TrackStateProxy trackStateProxy =
0909             fittedStates.makeTrackState(Gx2fConstants::trackStateMask,
0910                                         result.lastTrackIndex);
0911         const std::size_t currentTrackIndex = trackStateProxy.index();
0912 
0913         // Set the trackStateProxy components with the state from the ongoing
0914         // propagation
0915         {
0916           trackStateProxy.setReferenceSurface(surface->getSharedPtr());
0917           // Bind the transported state to the current surface
0918           auto res = stepper.boundState(state.stepping, *surface, false,
0919                                         freeToBoundCorrection);
0920           if (!res.ok()) {
0921             return res.error();
0922           }
0923           // Not const since, we might need to update with scattering angles
0924           auto& [boundParams, jacobian, pathLength] = *res;
0925 
0926           // For material surfaces, we also update the angles with the
0927           // available scattering information
0928           if (doMaterial) {
0929             ACTS_DEBUG("    Update parameters with scattering angles.");
0930             const auto scatteringMapId = scatteringMap->find(geoId);
0931             ACTS_VERBOSE(
0932                 "        scatteringAngles: "
0933                 << scatteringMapId->second.scatteringAngles().transpose());
0934             ACTS_VERBOSE("        boundParams before the update: "
0935                          << boundParams.parameters().transpose());
0936             boundParams.parameters() +=
0937                 scatteringMapId->second.scatteringAngles();
0938             ACTS_VERBOSE("        boundParams after the update: "
0939                          << boundParams.parameters().transpose());
0940           }
0941 
0942           // Fill the track state
0943           trackStateProxy.smoothed() = boundParams.parameters();
0944           trackStateProxy.smoothedCovariance() = state.stepping.cov;
0945 
0946           trackStateProxy.jacobian() = jacobian;
0947           trackStateProxy.pathLength() = pathLength;
0948 
0949           if (doMaterial) {
0950             stepper.update(state.stepping,
0951                            transformBoundToFreeParameters(
0952                                trackStateProxy.referenceSurface(),
0953                                state.geoContext, trackStateProxy.smoothed()),
0954                            trackStateProxy.smoothed(),
0955                            trackStateProxy.smoothedCovariance(), *surface);
0956           }
0957         }
0958 
0959         // We have smoothed parameters, so calibrate the uncalibrated input
0960         // measurement
0961         extensions.calibrator(state.geoContext, *calibrationContext,
0962                               sourceLinkIt->second, trackStateProxy);
0963 
0964         // Get and set the type flags
0965         auto typeFlags = trackStateProxy.typeFlags();
0966         typeFlags.setHasParameters();
0967         if (surfaceHasMaterial) {
0968           typeFlags.setHasMaterial();
0969         }
0970 
0971         // Set the measurement type flag
0972         typeFlags.setIsMeasurement();
0973         // We count the processed measurement
0974         ++result.processedMeasurements;
0975 
0976         result.lastMeasurementIndex = currentTrackIndex;
0977         result.lastTrackIndex = currentTrackIndex;
0978 
0979         // TODO check for outlier first
0980         // We count the state with measurement
0981         ++result.measurementStates;
0982 
0983         // We count the processed state
0984         ++result.processedStates;
0985 
0986         // Update the number of holes count only when encountering a
0987         // measurement
0988         result.measurementHoles = result.missedActiveSurfaces.size();
0989 
0990         return Result<void>::success();
0991       }
0992 
0993       if (doMaterial) {
0994         // Here we handle material for multipleScattering. If holes exist, we
0995         // also handle them already. We create a full trackstate (unlike for
0996         // simple holes), since we need to evaluate the material later
0997         ACTS_DEBUG(
0998             "    The surface contains no measurement, but material and maybe "
0999             "a hole.");
1000 
1001         // Transport the covariance to the surface
1002         stepper.transportCovarianceToBound(state.stepping, *surface,
1003                                            freeToBoundCorrection);
1004 
1005         // TODO generalize the update of the currentTrackIndex
1006         auto& fittedStates = *result.fittedStates;
1007 
1008         // Add a <trackStateMask> TrackState entry multi trajectory. This
1009         // allocates storage for all components, which we will set later.
1010         typename traj_t::TrackStateProxy trackStateProxy =
1011             fittedStates.makeTrackState(Gx2fConstants::trackStateMask,
1012                                         result.lastTrackIndex);
1013         const std::size_t currentTrackIndex = trackStateProxy.index();
1014 
1015         // Set the trackStateProxy components with the state from the ongoing
1016         // propagation
1017         {
1018           trackStateProxy.setReferenceSurface(surface->getSharedPtr());
1019           // Bind the transported state to the current surface
1020           auto res = stepper.boundState(state.stepping, *surface, false,
1021                                         freeToBoundCorrection);
1022           if (!res.ok()) {
1023             return res.error();
1024           }
1025           // Not const since, we might need to update with scattering angles
1026           auto& [boundParams, jacobian, pathLength] = *res;
1027 
1028           // For material surfaces, we also update the angles with the
1029           // available scattering information
1030           // We can skip the if here, since we already know, that we do
1031           // multipleScattering and have material
1032           ACTS_DEBUG("    Update parameters with scattering angles.");
1033           const auto scatteringMapId = scatteringMap->find(geoId);
1034           ACTS_VERBOSE(
1035               "        scatteringAngles: "
1036               << scatteringMapId->second.scatteringAngles().transpose());
1037           ACTS_VERBOSE("        boundParams before the update: "
1038                        << boundParams.parameters().transpose());
1039           boundParams.parameters() +=
1040               scatteringMapId->second.scatteringAngles();
1041           ACTS_VERBOSE("        boundParams after the update: "
1042                        << boundParams.parameters().transpose());
1043 
1044           // Fill the track state
1045           trackStateProxy.smoothed() = boundParams.parameters();
1046           trackStateProxy.smoothedCovariance() = state.stepping.cov;
1047 
1048           trackStateProxy.jacobian() = jacobian;
1049           trackStateProxy.pathLength() = pathLength;
1050 
1051           stepper.update(state.stepping,
1052                          transformBoundToFreeParameters(
1053                              trackStateProxy.referenceSurface(),
1054                              state.geoContext, trackStateProxy.smoothed()),
1055                          trackStateProxy.smoothed(),
1056                          trackStateProxy.smoothedCovariance(), *surface);
1057         }
1058 
1059         // Get and set the type flags
1060         auto typeFlags = trackStateProxy.typeFlags();
1061         typeFlags.setHasParameters();
1062         typeFlags.setHasMaterial();
1063 
1064         // Set hole only, if we are on a sensitive surface and had
1065         // measurements before (no holes before the first measurement)
1066         const bool precedingMeasurementExists = (result.measurementStates > 0);
1067         if (surfaceIsSensitive && precedingMeasurementExists) {
1068           ACTS_DEBUG("    Surface is also sensitive. Marked as hole.");
1069           typeFlags.setIsHole();
1070 
1071           // Count the missed surface
1072           result.missedActiveSurfaces.push_back(surface);
1073         }
1074 
1075         result.lastTrackIndex = currentTrackIndex;
1076 
1077         ++result.processedStates;
1078 
1079         return Result<void>::success();
1080       }
1081 
1082       if (surfaceIsSensitive || surfaceHasMaterial) {
1083         // Here we handle holes. If material hasn't been handled before
1084         // (because multipleScattering is turned off), we will also handle it
1085         // here
1086         if (multipleScattering) {
1087           ACTS_DEBUG(
1088               "    The surface contains no measurement, but maybe a hole.");
1089         } else {
1090           ACTS_DEBUG(
1091               "    The surface contains no measurement, but maybe a hole "
1092               "and/or material.");
1093         }
1094 
1095         // We only create track states here if there is already a measurement
1096         // detected (no holes before the first measurement) or if we encounter
1097         // material
1098         const bool precedingMeasurementExists = (result.measurementStates > 0);
1099         if (!precedingMeasurementExists && !surfaceHasMaterial) {
1100           ACTS_DEBUG(
1101               "    Ignoring hole, because there are no preceding "
1102               "measurements.");
1103           return Result<void>::success();
1104         }
1105 
1106         auto& fittedStates = *result.fittedStates;
1107 
1108         // Add a <trackStateMask> TrackState entry multi trajectory. This
1109         // allocates storage for all components, which we will set later.
1110         typename traj_t::TrackStateProxy trackStateProxy =
1111             fittedStates.makeTrackState(Gx2fConstants::trackStateMask,
1112                                         result.lastTrackIndex);
1113         const std::size_t currentTrackIndex = trackStateProxy.index();
1114 
1115         // Set the trackStateProxy components with the state from the
1116         // ongoing propagation
1117         {
1118           trackStateProxy.setReferenceSurface(surface->getSharedPtr());
1119           // Bind the transported state to the current surface
1120           auto res = stepper.boundState(state.stepping, *surface, false,
1121                                         freeToBoundCorrection);
1122           if (!res.ok()) {
1123             return res.error();
1124           }
1125           const auto& [boundParams, jacobian, pathLength] = *res;
1126 
1127           // Fill the track state
1128           trackStateProxy.smoothed() = boundParams.parameters();
1129           trackStateProxy.smoothedCovariance() = state.stepping.cov;
1130 
1131           trackStateProxy.jacobian() = jacobian;
1132           trackStateProxy.pathLength() = pathLength;
1133         }
1134 
1135         // Get and set the type flags
1136         auto typeFlags = trackStateProxy.typeFlags();
1137         typeFlags.setHasParameters();
1138         if (surfaceHasMaterial) {
1139           ACTS_DEBUG("    It is material.");
1140           typeFlags.setHasMaterial();
1141         }
1142 
1143         // Set hole only, if we are on a sensitive surface
1144         if (surfaceIsSensitive && precedingMeasurementExists) {
1145           ACTS_DEBUG("    It is a hole.");
1146           typeFlags.setIsHole();
1147           // Count the missed surface
1148           result.missedActiveSurfaces.push_back(surface);
1149         }
1150 
1151         result.lastTrackIndex = currentTrackIndex;
1152 
1153         ++result.processedStates;
1154 
1155         return Result<void>::success();
1156       }
1157 
1158       ACTS_DEBUG("    The surface contains no measurement/material/hole.");
1159       return Result<void>::success();
1160     }
1161 
1162     template <typename propagator_state_t, typename stepper_t,
1163               typename navigator_t, typename result_t>
1164     bool checkAbort(propagator_state_t& /*state*/, const stepper_t& /*stepper*/,
1165                     const navigator_t& /*navigator*/, const result_t& result,
1166                     const Logger& /*logger*/) const {
1167       if (result.finished) {
1168         return true;
1169       }
1170       return false;
1171     }
1172   };
1173 
1174  public:
1175   /// Fit implementation
1176   ///
1177   /// @tparam source_link_iterator_t Iterator type used to pass source links
1178   /// @tparam track_container_t Type of the track container backend
1179   /// @tparam holder_t Type defining track container backend ownership
1180   ///
1181   /// @param it Begin iterator for the fittable uncalibrated measurements
1182   /// @param end End iterator for the fittable uncalibrated measurements
1183   /// @param sParameters The initial track parameters
1184   /// @param gx2fOptions Gx2FitterOptions steering the fit
1185   /// @param trackContainer Input track container storage to append into
1186   /// @note The input measurements are given in the form of @c SourceLink s.
1187   /// It's the calibrators job to turn them into calibrated measurements used in
1188   /// the fit.
1189   ///
1190   /// @return the output as an output track
1191   template <typename source_link_iterator_t,
1192             TrackContainerFrontend track_container_t>
1193   Result<typename track_container_t::TrackProxy> fit(
1194       source_link_iterator_t it, source_link_iterator_t end,
1195       const BoundTrackParameters& sParameters,
1196       const Gx2FitterOptions<traj_t>& gx2fOptions,
1197       track_container_t& trackContainer) const
1198     requires(!isDirectNavigator)
1199   {
1200     // Preprocess Measurements (SourceLinks -> map)
1201     // To be able to find measurements later, we put them into a map.
1202     // We need to copy input SourceLinks anyway, so the map can own them.
1203     ACTS_VERBOSE("Preparing " << std::distance(it, end)
1204                               << " input measurements");
1205     std::unordered_map<const Surface*, SourceLink> inputMeasurements{};
1206 
1207     for (; it != end; ++it) {
1208       inputMeasurements.try_emplace(gx2fOptions.extensions.surfaceAccessor(*it),
1209                                     *it);
1210     }
1211 
1212     // Store, if we want to do multiple scattering. We still need to pass this
1213     // option to the Actor.
1214     const bool multipleScattering = gx2fOptions.multipleScattering;
1215 
1216     // Create the ActorList
1217     using GX2FActor = Actor;
1218 
1219     using GX2FResult = typename GX2FActor::result_type;
1220     using Actors = Acts::ActorList<GX2FActor>;
1221 
1222     using PropagatorOptions = typename propagator_t::template Options<Actors>;
1223 
1224     BoundTrackParameters params = sParameters;
1225     double chi2sum = 0;
1226     double oldChi2sum = std::numeric_limits<double>::max();
1227 
1228     // We need to create a temporary track container. We create several times a
1229     // new track and delete it after updating the parameters. However, if we
1230     // would work on the externally provided track container, it would be
1231     // difficult to remove the correct track, if it contains more than one.
1232     typename track_container_t::TrackContainerBackend trackContainerTempBackend;
1233     traj_t trajectoryTempBackend;
1234     TrackContainer trackContainerTemp{trackContainerTempBackend,
1235                                       trajectoryTempBackend};
1236 
1237     // Create an index of the 'tip' of the track stored in multitrajectory. It
1238     // is needed outside the update loop. It will be updated with each iteration
1239     // and used for the final track
1240     std::size_t tipIndex = kInvalid;
1241 
1242     // The scatteringMap stores for each visited surface their scattering
1243     // properties
1244     std::unordered_map<GeometryIdentifier, ScatteringProperties> scatteringMap;
1245 
1246     // This will be filled during the updates with the final covariance of the
1247     // track parameters.
1248     BoundMatrix fullCovariancePredicted = BoundMatrix::Identity();
1249 
1250     ACTS_VERBOSE("Initial parameters: " << params.parameters().transpose());
1251 
1252     /// Actual Fitting /////////////////////////////////////////////////////////
1253     ACTS_DEBUG("Start to iterate");
1254 
1255     // Iterate the fit and improve result. Abort after n steps or after
1256     // convergence.
1257     // nUpdate is initialized outside to save its state for the track.
1258     std::size_t nUpdate = 0;
1259     for (nUpdate = 0; nUpdate < gx2fOptions.nUpdateMax; nUpdate++) {
1260       ACTS_DEBUG("nUpdate = " << nUpdate + 1 << "/" << gx2fOptions.nUpdateMax);
1261 
1262       // set up propagator and co
1263       PropagatorOptions propagatorOptions{gx2fOptions.propagatorPlainOptions};
1264 
1265       // Add the measurement surface as external surface to the navigator.
1266       // We will try to hit those surface by ignoring boundary checks.
1267       for (const auto& [surface, _] : inputMeasurements) {
1268         propagatorOptions.navigation.insertExternalSurface(*surface);
1269       }
1270 
1271       auto& gx2fActor = propagatorOptions.actorList.template get<GX2FActor>();
1272       gx2fActor.inputMeasurements = &inputMeasurements;
1273       gx2fActor.multipleScattering = false;
1274       gx2fActor.extensions = gx2fOptions.extensions;
1275       gx2fActor.calibrationContext = &gx2fOptions.calibrationContext.get();
1276       gx2fActor.actorLogger = m_actorLogger.get();
1277       gx2fActor.scatteringMap = &scatteringMap;
1278       gx2fActor.parametersWithHypothesis = &params;
1279 
1280       auto propagatorState = m_propagator.makeState(propagatorOptions);
1281 
1282       auto propagatorInitResult =
1283           m_propagator.initialize(propagatorState, params);
1284       if (!propagatorInitResult.ok()) {
1285         ACTS_DEBUG("Propagation initialization failed: "
1286                    << propagatorInitResult.error());
1287         return propagatorInitResult.error();
1288       }
1289 
1290       auto& r = propagatorState.template get<Gx2FitterResult<traj_t>>();
1291       r.fittedStates = &trajectoryTempBackend;
1292 
1293       // Clear the track container. It could be more performant to update the
1294       // existing states, but this needs some more thinking.
1295       trackContainerTemp.clear();
1296 
1297       auto propagationResult = m_propagator.propagate(propagatorState);
1298 
1299       // Run the fitter
1300       auto result =
1301           m_propagator.makeResult(std::move(propagatorState), propagationResult,
1302                                   propagatorOptions, false);
1303 
1304       if (!result.ok()) {
1305         ACTS_DEBUG("Propagation failed: " << result.error());
1306         return result.error();
1307       }
1308 
1309       // TODO Improve Propagator + Actor [allocate before loop], rewrite
1310       // makeMeasurements
1311       auto& propRes = *result;
1312       GX2FResult gx2fResult = std::move(propRes.template get<GX2FResult>());
1313 
1314       auto track = trackContainerTemp.makeTrack();
1315       tipIndex = gx2fResult.lastMeasurementIndex;
1316 
1317       // It could happen, that no measurements were found. Then the track would
1318       // be empty and the following operations would be invalid. Usually, this
1319       // only happens during the first iteration, due to bad initial parameters.
1320       if (tipIndex == kInvalid) {
1321         ACTS_INFO("Did not find any measurements in nUpdate "
1322                   << nUpdate + 1 << "/" << gx2fOptions.nUpdateMax);
1323         return Experimental::GlobalChiSquareFitterError::NotEnoughMeasurements;
1324       }
1325 
1326       track.tipIndex() = tipIndex;
1327       track.linkForward();
1328 
1329       // Count the material surfaces, to set up the system. In the multiple
1330       // scattering case, we need to extend our system.
1331       const std::size_t nMaterialSurfaces = 0u;
1332 
1333       // We need 6 dimensions for the bound parameters and 2 * nMaterialSurfaces
1334       // dimensions for the scattering angles.
1335       const std::size_t dimsExtendedParams = eBoundSize + 2 * nMaterialSurfaces;
1336 
1337       // System that we fill with the information gathered by the actor and
1338       // evaluate later
1339       Gx2fSystem extendedSystem{dimsExtendedParams};
1340 
1341       // This vector stores the IDs for each visited material in order. We use
1342       // it later for updating the scattering angles. We cannot use
1343       // scatteringMap directly, since we cannot guarantee, that we will visit
1344       // all stored material in each propagation.
1345       std::vector<GeometryIdentifier> geoIdVector;
1346 
1347       fillGx2fSystem(track, extendedSystem, false, scatteringMap, geoIdVector,
1348                      *m_addToSumLogger);
1349 
1350       chi2sum = extendedSystem.chi2();
1351 
1352       // This check takes into account the evaluated dimensions of the
1353       // measurements. To fit, we need at least NDF+1 measurements. However, we
1354       // count n-dimensional measurements for n measurements, reducing the
1355       // effective number of needed measurements. We might encounter the case,
1356       // where we cannot use some (parts of a) measurements, maybe if we do not
1357       // support that kind of measurement. This is also taken into account here.
1358       // We skip the check during the first iteration, since we cannot guarantee
1359       // to hit all/enough measurement surfaces with the initial parameter
1360       // guess.
1361       // We skip the check during the first iteration, since we cannot guarantee
1362       // to hit all/enough measurement surfaces with the initial parameter
1363       // guess.
1364       if ((nUpdate > 0) && !extendedSystem.isWellDefined()) {
1365         ACTS_INFO("Not enough measurements. Require "
1366                   << extendedSystem.findRequiredNdf() + 1 << ", but only "
1367                   << extendedSystem.ndf() << " could be used.");
1368         return Experimental::GlobalChiSquareFitterError::NotEnoughMeasurements;
1369       }
1370 
1371       Eigen::VectorXd deltaParamsExtended =
1372           computeGx2fDeltaParams(extendedSystem);
1373 
1374       ACTS_VERBOSE("aMatrix:\n"
1375                    << extendedSystem.aMatrix() << "\n"
1376                    << "bVector:\n"
1377                    << extendedSystem.bVector() << "\n"
1378                    << "deltaParamsExtended:\n"
1379                    << deltaParamsExtended << "\n"
1380                    << "oldChi2sum = " << oldChi2sum << "\n"
1381                    << "chi2sum = " << extendedSystem.chi2());
1382 
1383       if ((gx2fOptions.relChi2changeCutOff != 0) && (nUpdate > 0) &&
1384           (std::abs(extendedSystem.chi2() / oldChi2sum - 1) <
1385            gx2fOptions.relChi2changeCutOff)) {
1386         ACTS_DEBUG("Abort with relChi2changeCutOff after "
1387                    << nUpdate + 1 << "/" << gx2fOptions.nUpdateMax
1388                    << " iterations.");
1389         updateGx2fCovarianceParams(fullCovariancePredicted, extendedSystem);
1390         break;
1391       }
1392 
1393       if (extendedSystem.chi2() > oldChi2sum + 1e-5) {
1394         ACTS_DEBUG("chi2 not converging monotonically in update " << nUpdate);
1395       }
1396 
1397       // If this is the final iteration, update the covariance and break.
1398       // Otherwise, we would update the scattering angles too much.
1399       if (nUpdate == gx2fOptions.nUpdateMax - 1) {
1400         // Since currently most of our tracks converge in 4-5 updates, we want
1401         // to set nUpdateMax higher than that to guarantee convergence for most
1402         // tracks. In cases, where we set a smaller nUpdateMax, it's because we
1403         // want to investigate the behaviour of the fitter before it converges,
1404         // like in some unit-tests.
1405         if (gx2fOptions.nUpdateMax > 5) {
1406           ACTS_INFO("Did not converge in " << gx2fOptions.nUpdateMax
1407                                            << " updates.");
1408           return Experimental::GlobalChiSquareFitterError::DidNotConverge;
1409         }
1410 
1411         updateGx2fCovarianceParams(fullCovariancePredicted, extendedSystem);
1412         break;
1413       }
1414 
1415       updateGx2fParams(params, deltaParamsExtended, nMaterialSurfaces,
1416                        scatteringMap, geoIdVector);
1417       ACTS_VERBOSE("Updated parameters: " << params.parameters().transpose());
1418 
1419       oldChi2sum = extendedSystem.chi2();
1420     }
1421     ACTS_DEBUG("Finished to iterate");
1422     ACTS_VERBOSE("Final parameters: " << params.parameters().transpose());
1423     /// Finish Fitting /////////////////////////////////////////////////////////
1424 
1425     /// Actual MATERIAL Fitting ////////////////////////////////////////////////
1426     ACTS_DEBUG("Start to evaluate material");
1427     if (multipleScattering) {
1428       // Set up the propagator
1429       PropagatorOptions propagatorOptions{gx2fOptions.propagatorPlainOptions};
1430 
1431       // Add the measurement surface as external surface to the navigator.
1432       // We will try to hit those surface by ignoring boundary checks.
1433       for (const auto& [surface, _] : inputMeasurements) {
1434         propagatorOptions.navigation.insertExternalSurface(*surface);
1435       }
1436 
1437       auto& gx2fActor = propagatorOptions.actorList.template get<GX2FActor>();
1438       gx2fActor.inputMeasurements = &inputMeasurements;
1439       gx2fActor.multipleScattering = true;
1440       gx2fActor.extensions = gx2fOptions.extensions;
1441       gx2fActor.calibrationContext = &gx2fOptions.calibrationContext.get();
1442       gx2fActor.actorLogger = m_actorLogger.get();
1443       gx2fActor.scatteringMap = &scatteringMap;
1444       gx2fActor.parametersWithHypothesis = &params;
1445 
1446       auto propagatorState = m_propagator.makeState(propagatorOptions);
1447 
1448       auto propagatorInitResult =
1449           m_propagator.initialize(propagatorState, params);
1450       if (!propagatorInitResult.ok()) {
1451         ACTS_DEBUG("Propagation initialization failed: "
1452                    << propagatorInitResult.error());
1453         return propagatorInitResult.error();
1454       }
1455 
1456       auto& r = propagatorState.template get<Gx2FitterResult<traj_t>>();
1457       r.fittedStates = &trajectoryTempBackend;
1458 
1459       // Clear the track container. It could be more performant to update the
1460       // existing states, but this needs some more thinking.
1461       trackContainerTemp.clear();
1462 
1463       auto propagationResult = m_propagator.propagate(propagatorState);
1464 
1465       // Run the fitter
1466       auto result =
1467           m_propagator.makeResult(std::move(propagatorState), propagationResult,
1468                                   propagatorOptions, false);
1469 
1470       if (!result.ok()) {
1471         ACTS_DEBUG("Propagation failed: " << result.error());
1472         return result.error();
1473       }
1474 
1475       // TODO Improve Propagator + Actor [allocate before loop], rewrite
1476       // makeMeasurements
1477       auto& propRes = *result;
1478       GX2FResult gx2fResult = std::move(propRes.template get<GX2FResult>());
1479 
1480       auto track = trackContainerTemp.makeTrack();
1481       tipIndex = gx2fResult.lastMeasurementIndex;
1482 
1483       // It could happen, that no measurements were found. Then the track would
1484       // be empty and the following operations would be invalid. Usually, this
1485       // only happens during the first iteration, due to bad initial parameters.
1486       if (tipIndex == kInvalid) {
1487         ACTS_INFO("Did not find any measurements in material fit.");
1488         return Experimental::GlobalChiSquareFitterError::NotEnoughMeasurements;
1489       }
1490 
1491       track.tipIndex() = tipIndex;
1492       track.linkForward();
1493 
1494       // Count the material surfaces, to set up the system. In the multiple
1495       // scattering case, we need to extend our system.
1496       const std::size_t nMaterialSurfaces =
1497           countMaterialStates(track, scatteringMap, *m_addToSumLogger);
1498 
1499       // We need 6 dimensions for the bound parameters and 2 * nMaterialSurfaces
1500       // dimensions for the scattering angles.
1501       const std::size_t dimsExtendedParams = eBoundSize + 2 * nMaterialSurfaces;
1502 
1503       // System that we fill with the information gathered by the actor and
1504       // evaluate later
1505       Gx2fSystem extendedSystem{dimsExtendedParams};
1506 
1507       // This vector stores the IDs for each visited material in order. We use
1508       // it later for updating the scattering angles. We cannot use
1509       // scatteringMap directly, since we cannot guarantee, that we will visit
1510       // all stored material in each propagation.
1511       std::vector<GeometryIdentifier> geoIdVector;
1512 
1513       fillGx2fSystem(track, extendedSystem, true, scatteringMap, geoIdVector,
1514                      *m_addToSumLogger);
1515 
1516       chi2sum = extendedSystem.chi2();
1517 
1518       // This check takes into account the evaluated dimensions of the
1519       // measurements. To fit, we need at least NDF+1 measurements. However, we
1520       // count n-dimensional measurements for n measurements, reducing the
1521       // effective number of needed measurements. We might encounter the case,
1522       // where we cannot use some (parts of a) measurements, maybe if we do not
1523       // support that kind of measurement. This is also taken into account here.
1524       // We skip the check during the first iteration, since we cannot guarantee
1525       // to hit all/enough measurement surfaces with the initial parameter
1526       // guess.
1527       if ((nUpdate > 0) && !extendedSystem.isWellDefined()) {
1528         ACTS_INFO("Not enough measurements. Require "
1529                   << extendedSystem.findRequiredNdf() + 1 << ", but only "
1530                   << extendedSystem.ndf() << " could be used.");
1531         return Experimental::GlobalChiSquareFitterError::NotEnoughMeasurements;
1532       }
1533 
1534       Eigen::VectorXd deltaParamsExtended =
1535           computeGx2fDeltaParams(extendedSystem);
1536 
1537       ACTS_VERBOSE("aMatrix:\n"
1538                    << extendedSystem.aMatrix() << "\n"
1539                    << "bVector:\n"
1540                    << extendedSystem.bVector() << "\n"
1541                    << "deltaParamsExtended:\n"
1542                    << deltaParamsExtended << "\n"
1543                    << "oldChi2sum = " << oldChi2sum << "\n"
1544                    << "chi2sum = " << extendedSystem.chi2());
1545 
1546       chi2sum = extendedSystem.chi2();
1547 
1548       updateGx2fParams(params, deltaParamsExtended, nMaterialSurfaces,
1549                        scatteringMap, geoIdVector);
1550       ACTS_VERBOSE("Updated parameters: " << params.parameters().transpose());
1551 
1552       updateGx2fCovarianceParams(fullCovariancePredicted, extendedSystem);
1553     }
1554     ACTS_DEBUG("Finished to evaluate material");
1555     ACTS_VERBOSE(
1556         "Final parameters after material: " << params.parameters().transpose());
1557     /// Finish MATERIAL Fitting ////////////////////////////////////////////////
1558 
1559     ACTS_VERBOSE("Final scattering angles:");
1560     for (const auto& [key, value] : scatteringMap) {
1561       if (!value.materialIsValid()) {
1562         continue;
1563       }
1564       const auto& angles = value.scatteringAngles();
1565       ACTS_VERBOSE("    ( " << angles[eBoundTheta] << " | " << angles[eBoundPhi]
1566                             << " )");
1567     }
1568 
1569     ACTS_VERBOSE("Final covariance:\n" << fullCovariancePredicted);
1570 
1571     // Propagate again with the final covariance matrix. This is necessary to
1572     // obtain the propagated covariance for each state.
1573     // We also need to recheck the result and find the tipIndex, because at this
1574     // step, we will not ignore the boundary checks for measurement surfaces. We
1575     // want to create trackstates only on surfaces, that we actually hit.
1576     if (gx2fOptions.nUpdateMax > 0) {
1577       ACTS_VERBOSE("Propagate with the final covariance.");
1578       // update covariance
1579       params.covariance() = fullCovariancePredicted;
1580 
1581       // set up the propagator
1582       PropagatorOptions propagatorOptions{gx2fOptions.propagatorPlainOptions};
1583       auto& gx2fActor = propagatorOptions.actorList.template get<GX2FActor>();
1584       gx2fActor.inputMeasurements = &inputMeasurements;
1585       gx2fActor.multipleScattering = multipleScattering;
1586       gx2fActor.extensions = gx2fOptions.extensions;
1587       gx2fActor.calibrationContext = &gx2fOptions.calibrationContext.get();
1588       gx2fActor.actorLogger = m_actorLogger.get();
1589       gx2fActor.scatteringMap = &scatteringMap;
1590       gx2fActor.parametersWithHypothesis = &params;
1591 
1592       auto propagatorState = m_propagator.makeState(propagatorOptions);
1593 
1594       auto propagatorInitResult =
1595           m_propagator.initialize(propagatorState, params);
1596       if (!propagatorInitResult.ok()) {
1597         ACTS_DEBUG("Propagation initialization failed: "
1598                    << propagatorInitResult.error());
1599         return propagatorInitResult.error();
1600       }
1601 
1602       auto& r = propagatorState.template get<Gx2FitterResult<traj_t>>();
1603       r.fittedStates = &trackContainer.trackStateContainer();
1604 
1605       auto propagationResult = m_propagator.propagate(propagatorState);
1606 
1607       // Run the fitter
1608       auto result =
1609           m_propagator.makeResult(std::move(propagatorState), propagationResult,
1610                                   propagatorOptions, false);
1611 
1612       if (!result.ok()) {
1613         ACTS_DEBUG("Propagation failed: " << result.error());
1614         return result.error();
1615       }
1616 
1617       auto& propRes = *result;
1618       GX2FResult gx2fResult = std::move(propRes.template get<GX2FResult>());
1619 
1620       if (tipIndex != gx2fResult.lastMeasurementIndex) {
1621         ACTS_INFO("Final fit used unreachable measurements.");
1622         tipIndex = gx2fResult.lastMeasurementIndex;
1623 
1624         // It could happen, that no measurements were found. Then the track
1625         // would be empty and the following operations would be invalid.
1626         if (tipIndex == kInvalid) {
1627           ACTS_INFO("Did not find any measurements in final propagation.");
1628           return Experimental::GlobalChiSquareFitterError::
1629               NotEnoughMeasurements;
1630         }
1631       }
1632     }
1633 
1634     if (!trackContainer.hasColumn(
1635             Acts::hashString(Gx2fConstants::gx2fnUpdateColumn))) {
1636       trackContainer.template addColumn<std::uint32_t>("Gx2fnUpdateColumn");
1637     }
1638 
1639     // Prepare track for return
1640     auto track = trackContainer.makeTrack();
1641     track.tipIndex() = tipIndex;
1642     track.parameters() = params.parameters();
1643     track.covariance() = fullCovariancePredicted;
1644     track.setReferenceSurface(params.referenceSurface().getSharedPtr());
1645 
1646     if (trackContainer.hasColumn(
1647             Acts::hashString(Gx2fConstants::gx2fnUpdateColumn))) {
1648       ACTS_DEBUG("Add nUpdate to track");
1649       track.template component<std::uint32_t>("Gx2fnUpdateColumn") =
1650           static_cast<std::uint32_t>(nUpdate);
1651     }
1652 
1653     // TODO write test for calculateTrackQuantities
1654     calculateTrackQuantities(track);
1655 
1656     // Set the chi2sum for the track summary manually, since we don't calculate
1657     // it for each state
1658     track.chi2() = chi2sum;
1659 
1660     // Return the converted Track
1661     return track;
1662   }
1663 };
1664 
1665 /// @}
1666 
1667 }  // namespace Acts::Experimental