Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-07-15 08:12:06

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