Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-22 07:58:35

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