File indexing completed on 2025-08-05 08:09:43
0001
0002
0003
0004
0005
0006
0007
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
0036
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
0049
0050 RotationMatrix3 surfaceAxes =
0051 trkParams->referenceSurface().transform(gctx).rotation();
0052
0053 Vector3 surfaceOrigin =
0054 trkParams->referenceSurface().transform(gctx).translation();
0055
0056
0057 Vector3 xAxis = surfaceAxes.col(0);
0058 Vector3 yAxis = surfaceAxes.col(1);
0059
0060
0061
0062
0063
0064
0065 Vector3 originToVertex = vertexPos.template head<3>() - surfaceOrigin;
0066
0067
0068
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
0078 if constexpr (nDim == 4) {
0079 localVertexCoords(2) = vertexPos(3);
0080 localTrackCoords(2) = trkParams->parameters()[eBoundTime];
0081 }
0082
0083
0084 ActsVector<nDim - 1> residual = localTrackCoords - localVertexCoords;
0085
0086
0087 return residual.dot(weight * residual);
0088 }
0089
0090
0091
0092
0093
0094
0095
0096
0097
0098
0099
0100
0101
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
0122
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 }
0148
0149 if (!hasConverged) {
0150 ACTS_ERROR("Newton optimization did not converge.");
0151 return VertexingError::NotConverged;
0152 }
0153 return phi;
0154 }
0155
0156
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
0167 Vector3 refPoint = trkParams.referenceSurface().center(gctx);
0168
0169
0170 double absoluteCharge = trkParams.particleHypothesis().absoluteCharge();
0171 double qOvP = trkParams.parameters()[BoundIndices::eBoundQOverP];
0172
0173
0174
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
0184
0185
0186 if (absoluteCharge == 0. || bZ == 0.) {
0187
0188 Vector3 momDirStraightTrack = trkParams.direction();
0189
0190
0191 Vector3 positionOnTrack = trkParams.position(gctx);
0192
0193
0194 ActsScalar distanceToPca =
0195 (vtxPos.template head<3>() - positionOnTrack).dot(momDirStraightTrack);
0196
0197
0198 ActsVector<nDim> pcaStraightTrack;
0199 pcaStraightTrack.template head<3>() =
0200 positionOnTrack + distanceToPca * momDirStraightTrack;
0201 if constexpr (nDim == 4) {
0202
0203 double timeOnTrack = trkParams.parameters()[BoundIndices::eBoundTime];
0204
0205 ActsScalar m0 = trkParams.particleHypothesis().mass();
0206 ActsScalar p = trkParams.particleHypothesis().extractMomentum(qOvP);
0207
0208
0209 ActsScalar beta = p / std::hypot(p, m0);
0210
0211 pcaStraightTrack[3] = timeOnTrack + distanceToPca / beta;
0212 }
0213
0214
0215 Vector4 deltaRStraightTrack{Vector4::Zero()};
0216 deltaRStraightTrack.head<nDim>() = pcaStraightTrack - vtxPos;
0217
0218 return std::make_pair(deltaRStraightTrack, momDirStraightTrack);
0219 }
0220
0221
0222
0223
0224
0225
0226 double d0 = trkParams.parameters()[BoundIndices::eBoundLoc0];
0227 double z0 = trkParams.parameters()[BoundIndices::eBoundLoc1];
0228
0229 double phiP = trkParams.parameters()[BoundIndices::eBoundPhi];
0230 double theta = trkParams.parameters()[BoundIndices::eBoundTheta];
0231
0232 double sinTheta = std::sin(theta);
0233 double cotTheta = 1. / std::tan(theta);
0234
0235
0236
0237 double phi = phiP;
0238
0239
0240 double rho = sinTheta * (1. / qOvP) / bZ;
0241
0242
0243
0244
0245
0246 Vector3 helixCenter =
0247 refPoint + Vector3(-(d0 - rho) * std::sin(phi),
0248 (d0 - rho) * std::cos(phi), z0 + rho * phi * cotTheta);
0249
0250
0251
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
0258 phi = *res;
0259
0260 double cosPhi = std::cos(phi);
0261 double sinPhi = std::sin(phi);
0262
0263
0264
0265
0266 Vector3 momDir =
0267 Vector3(cosPhi * sinTheta, sinPhi * sinTheta, std::cos(theta));
0268
0269
0270
0271
0272 ActsVector<nDim> pca;
0273 pca.template head<3>() =
0274 helixCenter + rho * Vector3(-sinPhi, cosPhi, -cotTheta * phi);
0275
0276 if constexpr (nDim == 4) {
0277
0278 double tP = trkParams.parameters()[BoundIndices::eBoundTime];
0279
0280 ActsScalar m0 = trkParams.particleHypothesis().mass();
0281 ActsScalar p = trkParams.particleHypothesis().extractMomentum(qOvP);
0282
0283
0284 ActsScalar beta = p / std::hypot(p, m0);
0285
0286 pca[3] = tP - rho / (beta * sinTheta) * (phi - phiP);
0287 }
0288
0289 Vector4 deltaR{Vector4::Zero()};
0290 deltaR.head<nDim>() = pca - vtxPos;
0291
0292 return std::make_pair(deltaR, momDir);
0293 }
0294
0295 }
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
0308
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
0324 Vector3 deltaR = res.value().first.head<3>();
0325
0326
0327 deltaR.normalize();
0328
0329
0330 Vector3 momDir = res.value().second;
0331
0332
0333
0334
0335
0336
0337 Vector3 orthogonalDeltaR = deltaR - (deltaR.dot(momDir)) * momDir;
0338
0339
0340 Vector3 perpDir = momDir.cross(orthogonalDeltaR);
0341
0342
0343
0344
0345
0346
0347
0348
0349
0350 Transform3 coordinateSystem;
0351
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
0356 coordinateSystem.matrix().block<3, 1>(0, 3) = vtxPos;
0357
0358
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
0369 PropagatorOptions<> pOptions(gctx, mctx);
0370 pOptions.direction =
0371 Direction::fromScalarZeroAsPositive(intersection.pathLength());
0372
0373
0374
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
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
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
0449 if (!params.covariance().has_value()) {
0450 return VertexingError::NoCovariance;
0451 }
0452
0453
0454 auto impactParams = params.impactParameters();
0455 auto impactParamCovariance = params.impactParameterCovariance().value();
0456
0457
0458
0459
0460
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
0469 double vtxVar2DExtent = std::max(vtxVarX, vtxVarY);
0470
0471
0472
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
0508 PropagatorOptions<> pOptions(gctx, mctx);
0509 pOptions.direction = Direction::Backward;
0510
0511
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
0547 PropagatorOptions<> pOptions(gctx, mctx);
0548 pOptions.direction = Direction::Backward;
0549
0550
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 }