Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-12-08 09:26:28

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