Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-01-18 09:11:33

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