Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2025-08-05 08:09:43

0001 // This file is part of the Acts project.
0002 //
0003 // Copyright (C) 2019-2023 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 http://mozilla.org/MPL/2.0/.
0008 
0009 #include "Acts/Vertexing/ImpactPointEstimator.hpp"
0010 
0011 #include "Acts/Definitions/Algebra.hpp"
0012 #include "Acts/Propagator/Propagator.hpp"
0013 #include "Acts/Surfaces/PerigeeSurface.hpp"
0014 #include "Acts/Surfaces/PlaneSurface.hpp"
0015 #include "Acts/Vertexing/VertexingError.hpp"
0016 
0017 namespace Acts {
0018 
0019 namespace {
0020 template <typename vector_t>
0021 Result<double> getVertexCompatibilityImpl(const GeometryContext& gctx,
0022                                           const BoundTrackParameters* trkParams,
0023                                           const vector_t& vertexPos) {
0024   static constexpr int nDim = vector_t::RowsAtCompileTime;
0025   static_assert(nDim == 3 || nDim == 4,
0026                 "The number of dimensions nDim must be either 3 or 4.");
0027 
0028   static_assert(vector_t::RowsAtCompileTime == nDim,
0029                 "The dimension of the vertex position vector must match nDim.");
0030 
0031   if (trkParams == nullptr) {
0032     return VertexingError::EmptyInput;
0033   }
0034 
0035   // Retrieve weight matrix of the track's local x-, y-, and time-coordinate
0036   // (the latter only if nDim = 4). For this, the covariance needs to be set.
0037   if (!trkParams->covariance().has_value()) {
0038     return VertexingError::NoCovariance;
0039   }
0040   ActsSquareMatrix<nDim - 1> subCovMat;
0041   if constexpr (nDim == 3) {
0042     subCovMat = trkParams->spatialImpactParameterCovariance().value();
0043   } else {
0044     subCovMat = trkParams->impactParameterCovariance().value();
0045   }
0046   ActsSquareMatrix<nDim - 1> weight = subCovMat.inverse();
0047 
0048   // Orientation of the surface (i.e., axes of the corresponding coordinate
0049   // system)
0050   RotationMatrix3 surfaceAxes =
0051       trkParams->referenceSurface().transform(gctx).rotation();
0052   // Origin of the surface coordinate system
0053   Vector3 surfaceOrigin =
0054       trkParams->referenceSurface().transform(gctx).translation();
0055 
0056   // x- and y-axis of the surface coordinate system
0057   Vector3 xAxis = surfaceAxes.col(0);
0058   Vector3 yAxis = surfaceAxes.col(1);
0059 
0060   // Vector pointing from the surface origin to the vertex position
0061   // TODO: The vertex should always be at the surfaceOrigin since the
0062   // track parameters should be obtained by estimate3DImpactParameters.
0063   // Therefore, originToVertex should always be 0, which is currently not the
0064   // case.
0065   Vector3 originToVertex = vertexPos.template head<3>() - surfaceOrigin;
0066 
0067   // x-, y-, and possibly time-coordinate of the vertex and the track in the
0068   // surface coordinate system
0069   ActsVector<nDim - 1> localVertexCoords;
0070   localVertexCoords.template head<2>() =
0071       Vector2(originToVertex.dot(xAxis), originToVertex.dot(yAxis));
0072 
0073   ActsVector<nDim - 1> localTrackCoords;
0074   localTrackCoords.template head<2>() =
0075       Vector2(trkParams->parameters()[eX], trkParams->parameters()[eY]);
0076 
0077   // Fill time coordinates if we check the 4D vertex compatibility
0078   if constexpr (nDim == 4) {
0079     localVertexCoords(2) = vertexPos(3);
0080     localTrackCoords(2) = trkParams->parameters()[eBoundTime];
0081   }
0082 
0083   // residual
0084   ActsVector<nDim - 1> residual = localTrackCoords - localVertexCoords;
0085 
0086   // return chi2
0087   return residual.dot(weight * residual);
0088 }
0089 
0090 /// @brief Performs a Newton approximation to retrieve a point
0091 /// of closest approach in 3D to a reference position
0092 ///
0093 /// @param helixCenter Position of the helix center
0094 /// @param vtxPos Vertex position
0095 /// @param phi Azimuthal momentum angle
0096 /// @note Modifying phi corresponds to moving along the track. This function
0097 /// optimizes phi until we reach a 3D PCA.
0098 /// @param theta Polar momentum angle (constant along the track)
0099 /// @param rho Signed helix radius
0100 ///
0101 /// @return Phi value at 3D PCA
0102 Result<double> performNewtonOptimization(
0103     const Vector3& helixCenter, const Vector3& vtxPos, double phi, double theta,
0104     double rho, const ImpactPointEstimator::Config& cfg, const Logger& logger) {
0105   double sinPhi = std::sin(phi);
0106   double cosPhi = std::cos(phi);
0107 
0108   int nIter = 0;
0109   bool hasConverged = false;
0110 
0111   double cotTheta = 1. / std::tan(theta);
0112 
0113   double xO = helixCenter.x();
0114   double yO = helixCenter.y();
0115   double zO = helixCenter.z();
0116 
0117   double xVtx = vtxPos.x();
0118   double yVtx = vtxPos.y();
0119   double zVtx = vtxPos.z();
0120 
0121   // Iterate until convergence is reached or the maximum amount of iterations
0122   // is exceeded
0123   while (!hasConverged && nIter < cfg.maxIterations) {
0124     double derivative = rho * ((xVtx - xO) * cosPhi + (yVtx - yO) * sinPhi +
0125                                (zVtx - zO + rho * phi * cotTheta) * cotTheta);
0126     double secDerivative = rho * (-(xVtx - xO) * sinPhi + (yVtx - yO) * cosPhi +
0127                                   rho * cotTheta * cotTheta);
0128 
0129     if (secDerivative < 0.) {
0130       ACTS_ERROR(
0131           "Encountered negative second derivative during Newton "
0132           "optimization.");
0133       return VertexingError::NumericFailure;
0134     }
0135 
0136     double deltaPhi = -derivative / secDerivative;
0137 
0138     phi += deltaPhi;
0139     sinPhi = std::sin(phi);
0140     cosPhi = std::cos(phi);
0141 
0142     nIter += 1;
0143 
0144     if (std::abs(deltaPhi) < cfg.precision) {
0145       hasConverged = true;
0146     }
0147   }  // end while loop
0148 
0149   if (!hasConverged) {
0150     ACTS_ERROR("Newton optimization did not converge.");
0151     return VertexingError::NotConverged;
0152   }
0153   return phi;
0154 }
0155 
0156 // Note: always return Vector4, we'll chop off the last component if needed
0157 template <typename vector_t>
0158 Result<std::pair<Vector4, Vector3>> getDistanceAndMomentumImpl(
0159     const GeometryContext& gctx, const BoundTrackParameters& trkParams,
0160     const vector_t& vtxPos, const ImpactPointEstimator::Config& cfg,
0161     ImpactPointEstimator::State& state, const Logger& logger) {
0162   static constexpr int nDim = vector_t::RowsAtCompileTime;
0163   static_assert(nDim == 3 || nDim == 4,
0164                 "The number of dimensions nDim must be either 3 or 4.");
0165 
0166   // Reference point R
0167   Vector3 refPoint = trkParams.referenceSurface().center(gctx);
0168 
0169   // Extract charge-related particle parameters
0170   double absoluteCharge = trkParams.particleHypothesis().absoluteCharge();
0171   double qOvP = trkParams.parameters()[BoundIndices::eBoundQOverP];
0172 
0173   // Z-component of the B field at the reference position.
0174   // Note that we assume a constant B field here!
0175   auto fieldRes = cfg.bField->getField(refPoint, state.fieldCache);
0176   if (!fieldRes.ok()) {
0177     ACTS_ERROR("In getDistanceAndMomentum, the B field at\n"
0178                << refPoint << "\ncould not be retrieved.");
0179     return fieldRes.error();
0180   }
0181   double bZ = (*fieldRes)[eZ];
0182 
0183   // The particle moves on a straight trajectory if its charge is 0 or if there
0184   // is no B field. In that case, the 3D PCA can be calculated analytically, see
0185   // Sec 3.2 of the reference.
0186   if (absoluteCharge == 0. || bZ == 0.) {
0187     // Momentum direction (constant for straight tracks)
0188     Vector3 momDirStraightTrack = trkParams.direction();
0189 
0190     // Current position on the track
0191     Vector3 positionOnTrack = trkParams.position(gctx);
0192 
0193     // Distance between positionOnTrack and the 3D PCA
0194     ActsScalar distanceToPca =
0195         (vtxPos.template head<3>() - positionOnTrack).dot(momDirStraightTrack);
0196 
0197     // 3D PCA
0198     ActsVector<nDim> pcaStraightTrack;
0199     pcaStraightTrack.template head<3>() =
0200         positionOnTrack + distanceToPca * momDirStraightTrack;
0201     if constexpr (nDim == 4) {
0202       // Track time at positionOnTrack
0203       double timeOnTrack = trkParams.parameters()[BoundIndices::eBoundTime];
0204 
0205       ActsScalar m0 = trkParams.particleHypothesis().mass();
0206       ActsScalar p = trkParams.particleHypothesis().extractMomentum(qOvP);
0207 
0208       // Speed in units of c
0209       ActsScalar beta = p / std::hypot(p, m0);
0210 
0211       pcaStraightTrack[3] = timeOnTrack + distanceToPca / beta;
0212     }
0213 
0214     // Vector pointing from the vertex position to the 3D PCA
0215     Vector4 deltaRStraightTrack{Vector4::Zero()};
0216     deltaRStraightTrack.head<nDim>() = pcaStraightTrack - vtxPos;
0217 
0218     return std::make_pair(deltaRStraightTrack, momDirStraightTrack);
0219   }
0220 
0221   // Charged particles in a constant B field follow a helical trajectory. In
0222   // that case, we calculate the 3D PCA using the Newton method, see Sec 4.2 in
0223   // the reference.
0224 
0225   // Spatial Perigee parameters (i.e., spatial parameters of 2D PCA)
0226   double d0 = trkParams.parameters()[BoundIndices::eBoundLoc0];
0227   double z0 = trkParams.parameters()[BoundIndices::eBoundLoc1];
0228   // Momentum angles at 2D PCA
0229   double phiP = trkParams.parameters()[BoundIndices::eBoundPhi];
0230   double theta = trkParams.parameters()[BoundIndices::eBoundTheta];
0231   // Functions of the polar angle theta for later use
0232   double sinTheta = std::sin(theta);
0233   double cotTheta = 1. / std::tan(theta);
0234 
0235   // Set optimization variable phi to the angle at the 2D PCA as a first guess.
0236   // Note that phi corresponds to phiV in the reference.
0237   double phi = phiP;
0238 
0239   // Signed radius of the helix on which the particle moves
0240   double rho = sinTheta * (1. / qOvP) / bZ;
0241 
0242   // Position of the helix center.
0243   // We can set the z-position to a convenient value since it is not fixed by
0244   // the Perigee parameters. Note that phi = phiP because we did not start the
0245   // optimization yet.
0246   Vector3 helixCenter =
0247       refPoint + Vector3(-(d0 - rho) * std::sin(phi),
0248                          (d0 - rho) * std::cos(phi), z0 + rho * phi * cotTheta);
0249 
0250   // Use Newton optimization method to iteratively change phi until we arrive at
0251   // the 3D PCA
0252   auto res = performNewtonOptimization(helixCenter, vtxPos.template head<3>(),
0253                                        phi, theta, rho, cfg, logger);
0254   if (!res.ok()) {
0255     return res.error();
0256   }
0257   // Set new phi value
0258   phi = *res;
0259 
0260   double cosPhi = std::cos(phi);
0261   double sinPhi = std::sin(phi);
0262 
0263   // Momentum direction at the 3D PCA.
0264   // Note that we have thetaV = thetaP = theta since the polar angle does not
0265   // change in a constant B field.
0266   Vector3 momDir =
0267       Vector3(cosPhi * sinTheta, sinPhi * sinTheta, std::cos(theta));
0268 
0269   // 3D PCA (point P' in the reference). Note that the prefix "3D" does not
0270   // refer to the dimension of the pca variable. Rather, it indicates that we
0271   // minimized the 3D distance between the track and the reference point.
0272   ActsVector<nDim> pca;
0273   pca.template head<3>() =
0274       helixCenter + rho * Vector3(-sinPhi, cosPhi, -cotTheta * phi);
0275 
0276   if constexpr (nDim == 4) {
0277     // Time at the 2D PCA P
0278     double tP = trkParams.parameters()[BoundIndices::eBoundTime];
0279 
0280     ActsScalar m0 = trkParams.particleHypothesis().mass();
0281     ActsScalar p = trkParams.particleHypothesis().extractMomentum(qOvP);
0282 
0283     // Speed in units of c
0284     ActsScalar beta = p / std::hypot(p, m0);
0285 
0286     pca[3] = tP - rho / (beta * sinTheta) * (phi - phiP);
0287   }
0288   // Vector pointing from the vertex position to the 3D PCA
0289   Vector4 deltaR{Vector4::Zero()};
0290   deltaR.head<nDim>() = pca - vtxPos;
0291 
0292   return std::make_pair(deltaR, momDir);
0293 }
0294 
0295 }  // namespace
0296 
0297 Result<double> ImpactPointEstimator::calculateDistance(
0298     const GeometryContext& gctx, const BoundTrackParameters& trkParams,
0299     const Vector3& vtxPos, State& state) const {
0300   auto res = getDistanceAndMomentumImpl(gctx, trkParams, vtxPos, m_cfg, state,
0301                                         *m_logger);
0302 
0303   if (!res.ok()) {
0304     return res.error();
0305   }
0306 
0307   // Return distance (we get a 4D vector in all cases, but we only need the
0308   // position norm)
0309   return res.value().first.template head<3>().norm();
0310 }
0311 
0312 Result<BoundTrackParameters> ImpactPointEstimator::estimate3DImpactParameters(
0313     const GeometryContext& gctx, const MagneticFieldContext& mctx,
0314     const BoundTrackParameters& trkParams, const Vector3& vtxPos,
0315     State& state) const {
0316   auto res = getDistanceAndMomentumImpl(gctx, trkParams, vtxPos, m_cfg, state,
0317                                         *m_logger);
0318 
0319   if (!res.ok()) {
0320     return res.error();
0321   }
0322 
0323   // Vector pointing from vertex to 3D PCA
0324   Vector3 deltaR = res.value().first.head<3>();
0325 
0326   // Get corresponding unit vector
0327   deltaR.normalize();
0328 
0329   // Momentum direction at vtxPos
0330   Vector3 momDir = res.value().second;
0331 
0332   // To understand why deltaR and momDir are not orthogonal, let us look at the
0333   // x-y-plane. Since we computed the 3D PCA, the 2D distance between the vertex
0334   // and the PCA is not necessarily minimal (see Fig. 4.2 in the reference). As
0335   // a consequence, the momentum and the vector connecting the vertex and the
0336   // PCA are not orthogonal to each other.
0337   Vector3 orthogonalDeltaR = deltaR - (deltaR.dot(momDir)) * momDir;
0338 
0339   // Vector perpendicular to momDir and orthogonalDeltaR
0340   Vector3 perpDir = momDir.cross(orthogonalDeltaR);
0341 
0342   // Cartesian coordinate system with:
0343   // -) origin at the vertex position
0344   // -) z-axis in momentum direction
0345   // -) x-axis approximately in direction of the 3D PCA (slight deviations
0346   // because it was modified to make if orthogonal to momDir)
0347   // -) y-axis is calculated to be orthogonal to x- and z-axis
0348   // The transformation is represented by a 4x4 matrix with 0 0 0 1 in the last
0349   // row.
0350   Transform3 coordinateSystem;
0351   // First three columns correspond to coordinate system axes
0352   coordinateSystem.matrix().block<3, 1>(0, 0) = orthogonalDeltaR;
0353   coordinateSystem.matrix().block<3, 1>(0, 1) = perpDir;
0354   coordinateSystem.matrix().block<3, 1>(0, 2) = momDir;
0355   // Fourth column corresponds to origin of the coordinate system
0356   coordinateSystem.matrix().block<3, 1>(0, 3) = vtxPos;
0357 
0358   // Surface with normal vector in direction of the z axis of coordinateSystem
0359   std::shared_ptr<PlaneSurface> planeSurface =
0360       Surface::makeShared<PlaneSurface>(coordinateSystem);
0361 
0362   auto intersection =
0363       planeSurface
0364           ->intersect(gctx, trkParams.position(gctx), trkParams.direction(),
0365                       BoundaryCheck(false))
0366           .closest();
0367 
0368   // Create propagator options
0369   PropagatorOptions<> pOptions(gctx, mctx);
0370   pOptions.direction =
0371       Direction::fromScalarZeroAsPositive(intersection.pathLength());
0372 
0373   // Propagate to the surface; intersection corresponds to an estimate of the 3D
0374   // PCA. If deltaR and momDir were orthogonal the calculation would be exact.
0375   auto result =
0376       m_cfg.propagator->propagateToSurface(trkParams, *planeSurface, pOptions);
0377   if (result.ok()) {
0378     return *result;
0379   } else {
0380     ACTS_ERROR("Error during propagation in estimate3DImpactParameters.");
0381     ACTS_DEBUG(
0382         "The plane surface to which we tried to propagate has its origin at\n"
0383         << vtxPos);
0384     return result.error();
0385   }
0386 }
0387 
0388 Result<double> ImpactPointEstimator::getVertexCompatibility(
0389     const GeometryContext& gctx, const BoundTrackParameters* trkParams,
0390     Eigen::Map<const ActsDynamicVector> vertexPos) const {
0391   if (vertexPos.size() == 3) {
0392     return getVertexCompatibilityImpl(gctx, trkParams,
0393                                       vertexPos.template head<3>());
0394   } else if (vertexPos.size() == 4) {
0395     return getVertexCompatibilityImpl(gctx, trkParams,
0396                                       vertexPos.template head<4>());
0397   } else {
0398     return VertexingError::InvalidInput;
0399   }
0400 }
0401 
0402 Result<std::pair<Acts::Vector4, Acts::Vector3>>
0403 ImpactPointEstimator::getDistanceAndMomentum(
0404     const GeometryContext& gctx, const BoundTrackParameters& trkParams,
0405     Eigen::Map<const ActsDynamicVector> vtxPos, State& state) const {
0406   if (vtxPos.size() == 3) {
0407     return getDistanceAndMomentumImpl(
0408         gctx, trkParams, vtxPos.template head<3>(), m_cfg, state, *m_logger);
0409   } else if (vtxPos.size() == 4) {
0410     return getDistanceAndMomentumImpl(
0411         gctx, trkParams, vtxPos.template head<4>(), m_cfg, state, *m_logger);
0412   } else {
0413     return VertexingError::InvalidInput;
0414   }
0415 }
0416 
0417 Result<ImpactParametersAndSigma> ImpactPointEstimator::getImpactParameters(
0418     const BoundTrackParameters& track, const Vertex& vtx,
0419     const GeometryContext& gctx, const MagneticFieldContext& mctx,
0420     bool calculateTimeIP) const {
0421   const std::shared_ptr<PerigeeSurface> perigeeSurface =
0422       Surface::makeShared<PerigeeSurface>(vtx.position());
0423 
0424   // Create propagator options
0425   PropagatorOptions<> pOptions(gctx, mctx);
0426   auto intersection = perigeeSurface
0427                           ->intersect(gctx, track.position(gctx),
0428                                       track.direction(), BoundaryCheck(false))
0429                           .closest();
0430   pOptions.direction =
0431       Direction::fromScalarZeroAsPositive(intersection.pathLength());
0432 
0433   // Do the propagation to linPoint
0434   auto result =
0435       m_cfg.propagator->propagateToSurface(track, *perigeeSurface, pOptions);
0436 
0437   if (!result.ok()) {
0438     ACTS_ERROR("Error during propagation in getImpactParameters.");
0439     ACTS_DEBUG(
0440         "The Perigee surface to which we tried to propagate has its origin "
0441         "at\n"
0442         << vtx.position());
0443     return result.error();
0444   }
0445 
0446   const auto& params = *result;
0447 
0448   // Check if the covariance matrix of the Perigee parameters exists
0449   if (!params.covariance().has_value()) {
0450     return VertexingError::NoCovariance;
0451   }
0452 
0453   // Extract Perigee parameters and corresponding covariance matrix
0454   auto impactParams = params.impactParameters();
0455   auto impactParamCovariance = params.impactParameterCovariance().value();
0456 
0457   // Vertex variances
0458   // TODO: By looking at sigmaD0 and sigmaZ0 we neglect the offdiagonal terms
0459   // (i.e., we approximate the vertex as a sphere rather than an ellipsoid).
0460   // Using the full covariance matrix might furnish better results.
0461   double vtxVarX = vtx.covariance()(eX, eX);
0462   double vtxVarY = vtx.covariance()(eY, eY);
0463   double vtxVarZ = vtx.covariance()(eZ, eZ);
0464 
0465   ImpactParametersAndSigma ipAndSigma;
0466 
0467   ipAndSigma.d0 = impactParams[0];
0468   // Variance of the vertex extent in the x-y-plane
0469   double vtxVar2DExtent = std::max(vtxVarX, vtxVarY);
0470   // TODO: vtxVar2DExtent, vtxVarZ, and vtxVarT should always be >= 0. We need
0471   // to throw an error here once
0472   // https://github.com/acts-project/acts/issues/2231 is resolved.
0473   if (vtxVar2DExtent > 0) {
0474     ipAndSigma.sigmaD0 =
0475         std::sqrt(vtxVar2DExtent + impactParamCovariance(0, 0));
0476   } else {
0477     ipAndSigma.sigmaD0 = std::sqrt(impactParamCovariance(0, 0));
0478   }
0479 
0480   ipAndSigma.z0 = impactParams[1];
0481   if (vtxVarZ > 0) {
0482     ipAndSigma.sigmaZ0 = std::sqrt(vtxVarZ + impactParamCovariance(1, 1));
0483   } else {
0484     ipAndSigma.sigmaZ0 = std::sqrt(impactParamCovariance(1, 1));
0485   }
0486 
0487   if (calculateTimeIP) {
0488     ipAndSigma.deltaT = std::abs(vtx.time() - impactParams[2]);
0489     double vtxVarT = vtx.fullCovariance()(eTime, eTime);
0490     if (vtxVarT > 0) {
0491       ipAndSigma.sigmaDeltaT = std::sqrt(vtxVarT + impactParamCovariance(2, 2));
0492     } else {
0493       ipAndSigma.sigmaDeltaT = std::sqrt(impactParamCovariance(2, 2));
0494     }
0495   }
0496 
0497   return ipAndSigma;
0498 }
0499 
0500 Result<std::pair<double, double>> ImpactPointEstimator::getLifetimeSignOfTrack(
0501     const BoundTrackParameters& track, const Vertex& vtx,
0502     const Vector3& direction, const GeometryContext& gctx,
0503     const MagneticFieldContext& mctx) const {
0504   const std::shared_ptr<PerigeeSurface> perigeeSurface =
0505       Surface::makeShared<PerigeeSurface>(vtx.position());
0506 
0507   // Create propagator options
0508   PropagatorOptions<> pOptions(gctx, mctx);
0509   pOptions.direction = Direction::Backward;
0510 
0511   // Do the propagation to the perigeee
0512   auto result =
0513       m_cfg.propagator->propagateToSurface(track, *perigeeSurface, pOptions);
0514 
0515   if (!result.ok()) {
0516     return result.error();
0517   }
0518 
0519   const auto& params = (*result).parameters();
0520   const double d0 = params[BoundIndices::eBoundLoc0];
0521   const double z0 = params[BoundIndices::eBoundLoc1];
0522   const double phi = params[BoundIndices::eBoundPhi];
0523   const double theta = params[BoundIndices::eBoundTheta];
0524 
0525   double vs = std::sin(std::atan2(direction[1], direction[0]) - phi) * d0;
0526   double eta = -std::log(std::tan(theta / 2.));
0527   double dir_eta = VectorHelpers::eta(direction);
0528 
0529   double zs = (dir_eta - eta) * z0;
0530 
0531   std::pair<double, double> vszs;
0532 
0533   vszs.first = vs >= 0. ? 1. : -1.;
0534   vszs.second = zs >= 0. ? 1. : -1.;
0535 
0536   return vszs;
0537 }
0538 
0539 Result<double> ImpactPointEstimator::get3DLifetimeSignOfTrack(
0540     const BoundTrackParameters& track, const Vertex& vtx,
0541     const Vector3& direction, const GeometryContext& gctx,
0542     const MagneticFieldContext& mctx) const {
0543   const std::shared_ptr<PerigeeSurface> perigeeSurface =
0544       Surface::makeShared<PerigeeSurface>(vtx.position());
0545 
0546   // Create propagator options
0547   PropagatorOptions<> pOptions(gctx, mctx);
0548   pOptions.direction = Direction::Backward;
0549 
0550   // Do the propagation to the perigeee
0551   auto result =
0552       m_cfg.propagator->propagateToSurface(track, *perigeeSurface, pOptions);
0553 
0554   if (!result.ok()) {
0555     return result.error();
0556   }
0557 
0558   const auto& params = *result;
0559   const Vector3 trkpos = params.position(gctx);
0560   const Vector3 trkmom = params.momentum();
0561 
0562   double sign =
0563       (direction.cross(trkmom)).dot(trkmom.cross(vtx.position() - trkpos));
0564 
0565   return sign >= 0. ? 1. : -1.;
0566 }
0567 
0568 }  // namespace Acts