Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-10-23 08:23:00

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