Back to home page

EIC code displayed by LXR

 
 

    


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

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/Seeding/EstimateTrackParamsFromSeed.hpp"
0010 
0011 #include "Acts/Definitions/Algebra.hpp"
0012 #include "Acts/Definitions/TrackParametrization.hpp"
0013 #include "Acts/EventData/TransformationHelpers.hpp"
0014 #include "Acts/Seeding/TrackParamsEstimationError.hpp"
0015 #include "Acts/Seeding/detail/CircleFit.hpp"
0016 #include "Acts/Utilities/MathHelpers.hpp"
0017 #include "Acts/Utilities/TransformHelpers.hpp"
0018 
0019 #include <cassert>
0020 #include <cmath>
0021 #include <limits>
0022 #include <numbers>
0023 #include <optional>
0024 #include <span>
0025 #include <vector>
0026 
0027 #include <Eigen/Eigenvalues>
0028 
0029 namespace Acts {
0030 
0031 namespace {
0032 
0033 Transform3 estimationFrameLocalToGlobal(const Vector3& sp0, const Vector3& sp1,
0034                                         const Vector3& bField) {
0035   // Define a new coordinate frame with its origin at the bottom space point, z
0036   // axis long the magnetic field direction and y axis perpendicular to vector
0037   // from the bottom to middle space point. Hence, the projection of the middle
0038   // space point on the transverse plane will be located at the x axis of the
0039   // new frame.
0040   //
0041   // A vanishing field falls back to global z, a reference along the field to
0042   // any orthogonal y.
0043   const Vector3 relVec = sp1 - sp0;
0044   const double bMag = bField.norm();
0045   const Vector3 newZAxis = (bMag > std::numeric_limits<double>::epsilon())
0046                                ? Vector3(bField / bMag)
0047                                : Vector3(Vector3::UnitZ());
0048   Vector3 newYAxis = newZAxis.cross(relVec);
0049   if (newYAxis.norm() < std::numeric_limits<double>::epsilon()) {
0050     newYAxis = newZAxis.unitOrthogonal();
0051   }
0052   newYAxis.normalize();
0053   const Vector3 newXAxis = newYAxis.cross(newZAxis);
0054   RotationMatrix3 rotation;
0055   rotation.col(0) = newXAxis;
0056   rotation.col(1) = newYAxis;
0057   rotation.col(2) = newZAxis;
0058   // The new frame, centered at the bottom space point
0059   return makeTransform3(rotation, sp0);
0060 }
0061 
0062 double computeDzDs(double A, double B, const Vector3& local0,
0063                    const Vector3& local2) {
0064   const auto computeLocalPhi = [&](const Vector2& local) -> double {
0065     // Scaled radius vector from circle center
0066     const Vector2 r = 2 * B * local - Vector2(-A, 1);
0067 
0068     return std::atan2(r.y(), r.x());
0069   };
0070 
0071   const double localPhi0 = computeLocalPhi(local0.head<2>());
0072   const double localPhi2 = computeLocalPhi(local2.head<2>());
0073 
0074   const double dZ = local2.z() - local0.z();
0075   const double dPhi = localPhi2 - localPhi0;
0076 
0077   // Apply the sinc correction to account for the fact that the particle do not
0078   // follow a straight line in the R-Z plane. This is especially important for
0079   // high delta phi and/or strongly bent tracks.
0080   const double sincCorrection = sinc(dPhi / 2);
0081 
0082   const double dzds =
0083       sincCorrection * dZ / (local2.head<2>() - local0.head<2>()).norm();
0084 
0085   return dzds;
0086 }
0087 
0088 struct ConformalMappingResult {
0089   Vector2 uv1;
0090   Vector2 uv2;
0091   Vector2 duv;
0092   double A;
0093   double B;
0094   double bOverS;
0095   double dzds;
0096 };
0097 
0098 ConformalMappingResult performConformalMapping(const Vector3& local1,
0099                                                const Vector3& local2) {
0100   ConformalMappingResult r{};
0101   r.uv1 = local1.head<2>() / local1.head<2>().squaredNorm();
0102   r.uv2 = local2.head<2>() / local2.head<2>().squaredNorm();
0103   r.duv = r.uv2 - r.uv1;
0104   r.A = r.duv.y() / r.duv.x();
0105   r.B = r.uv1.y() - r.A * r.uv1.x();
0106   r.bOverS = (r.uv1.y() * r.uv2.x() - r.uv2.y() * r.uv1.x()) / r.duv.norm();
0107   r.dzds = computeDzDs(r.A, r.B, local1, local2);
0108   return r;
0109 }
0110 
0111 Vector3 computeLocalTangent(const ConformalMappingResult& cm,
0112                             const Vector2& local) {
0113   // Scaled radius vector from circle center
0114   const Vector2 r = 2 * cm.B * local - Vector2(-cm.A, 1);
0115 
0116   // Tangent perpendicular to radius
0117   const Vector3 t(-r.y(), r.x(), r.norm() * cm.dzds);
0118 
0119   return t.normalized();
0120 }
0121 
0122 }  // namespace
0123 
0124 }  // namespace Acts
0125 
0126 Acts::FreeVector Acts::estimateTrackParamsFromSeed(
0127     const Vector3& sp0, const double t0, const Vector3& sp1, const Vector3& sp2,
0128     const Vector3& bField, Vector3* tangent0, Vector3* tangent1,
0129     Vector3* tangent2) {
0130   const Transform3 transform = estimationFrameLocalToGlobal(sp0, sp1, bField);
0131 
0132   // Local coordinates
0133   const Vector3 local0 = Vector3::Zero();
0134   const Vector3 local1 = transform.inverse() * sp1;
0135   const Vector3 local2 = transform.inverse() * sp2;
0136 
0137   // Conformal mapping
0138   const ConformalMappingResult cm = performConformalMapping(local1, local2);
0139 
0140   // The tangent vector at the bottom space point
0141   const Vector3 direction =
0142       transform.linear() * computeLocalTangent(cm, local0.head<2>());
0143 
0144   // Initialize the free parameters vector
0145   FreeVector params = FreeVector::Zero();
0146 
0147   // The bottom space point position
0148   params.segment<3>(eFreePos0) = sp0;
0149 
0150   // The estimated direction
0151   params.segment<3>(eFreeDir0) = direction;
0152 
0153   // The estimated q/pt in [GeV/c]^-1 (note that the pt is the projection of
0154   // momentum on the transverse plane of the new frame)
0155   const double qOverPt = 2 * cm.bOverS / bField.norm();
0156   // The estimated q/p in [GeV/c]^-1
0157   params[eFreeQOverP] = qOverPt / fastHypot(1, cm.dzds);
0158 
0159   // The time parameter is set to the time of the bottom space point
0160   params[eFreeTime] = t0;
0161 
0162   if (tangent0 != nullptr) {
0163     *tangent0 = direction;
0164   }
0165   if (tangent1 != nullptr) {
0166     *tangent1 = transform.linear() * computeLocalTangent(cm, local1.head<2>());
0167   }
0168   if (tangent2 != nullptr) {
0169     *tangent2 = transform.linear() * computeLocalTangent(cm, local2.head<2>());
0170   }
0171 
0172   return params;
0173 }
0174 
0175 Acts::Result<Acts::BoundVector> Acts::estimateTrackParamsFromSeed(
0176     const GeometryContext& gctx, const Surface& surface, const Vector3& sp0,
0177     const double t0, const Vector3& sp1, const Vector3& sp2,
0178     const Vector3& bField) {
0179   const FreeVector freeParams =
0180       estimateTrackParamsFromSeed(sp0, t0, sp1, sp2, bField);
0181   return transformFreeToBoundParameters(freeParams, surface, gctx);
0182 }
0183 
0184 Acts::BoundMatrix Acts::estimateTrackParamCovariance(
0185     const EstimateTrackParamCovarianceConfig& config, const BoundVector& params,
0186     bool hasTime) {
0187   assert((params[eBoundTheta] > 0 && params[eBoundTheta] < std::numbers::pi) &&
0188          "Theta must be in the range (0, pi)");
0189 
0190   BoundMatrix result = BoundMatrix::Zero();
0191 
0192   for (std::size_t i = eBoundLoc0; i < eBoundSize; ++i) {
0193     double sigma = config.initialSigmas[i];
0194     double variance = sigma * sigma;
0195 
0196     if (i == eBoundQOverP) {
0197       // note that we rely on the fact that sigma theta is already computed
0198       double varianceTheta = result(eBoundTheta, eBoundTheta);
0199 
0200       // contribution from sigma(q/pt)
0201       variance += std::pow(
0202           config.initialSigmaQoverPt * std::sin(params[eBoundTheta]), 2);
0203 
0204       // contribution from sigma(pt)/pt
0205       variance += std::pow(config.initialSigmaPtRel * params[eBoundQOverP], 2);
0206 
0207       // contribution from sigma(theta)
0208       variance +=
0209           varianceTheta *
0210           std::pow(params[eBoundQOverP] / std::tan(params[eBoundTheta]), 2);
0211     }
0212 
0213     if (i == eBoundTime && !hasTime) {
0214       // Inflate the time uncertainty if no time measurement is available
0215       variance *= config.noTimeVarInflation;
0216     }
0217 
0218     // Inflate the initial covariance
0219     variance *= config.initialVarInflation[i];
0220 
0221     result(i, i) = variance;
0222   }
0223 
0224   return result;
0225 }
0226 
0227 Acts::Result<Acts::FreeVector> Acts::estimateTrackParamsFromSpacePoints(
0228     std::span<const Vector3> spacePoints, const Vector3& bField, double t0,
0229     std::size_t geometricRefineIterations, std::span<const double> weights,
0230     std::size_t referenceIndex) {
0231   if (spacePoints.size() < 3) {
0232     return Result<FreeVector>::failure(
0233         TrackParamsEstimationError::NotEnoughSpacePoints);
0234   }
0235   assert((weights.empty() || weights.size() == spacePoints.size()) &&
0236          "weights must be empty or match the space points");
0237   assert(referenceIndex < spacePoints.size() &&
0238          "reference index must point into the space points");
0239 
0240   const auto w = [&](std::size_t i) {
0241     return weights.empty() ? 1 : weights[i];
0242   };
0243 
0244   const Vector3& reference = spacePoints[referenceIndex];
0245 
0246   // Estimation frame: field along local +z, fixed by the first two points. It
0247   // is anchored to the seed rather than to the reference point, so that the
0248   // fitted helix does not depend on where the parameters are reported.
0249   const Transform3 transform =
0250       estimationFrameLocalToGlobal(spacePoints.front(), spacePoints[1], bField);
0251   const Transform3 toLocal = transform.inverse();
0252   std::vector<Vector3> local;
0253   local.reserve(spacePoints.size());
0254   for (const Vector3& sp : spacePoints) {
0255     local.push_back(toLocal * sp);
0256   }
0257 
0258   FreeVector params = FreeVector::Zero();
0259   params.segment<3>(eFreePos0) = reference;
0260   params[eFreeTime] = t0;
0261 
0262   const double bMag = bField.norm();
0263 
0264   std::optional<detail::CircleFit> circle =
0265       detail::fitCircleTaubin(local, weights);
0266   if (circle.has_value() && geometricRefineIterations > 0) {
0267     circle = detail::refineCircleGeometric(*circle, local,
0268                                            geometricRefineIterations, weights);
0269   }
0270 
0271   if (!circle.has_value()) {
0272     // Straight-line limit: the direction is the principal axis, q/p stays zero.
0273     double sumW = 0;
0274     Vector3 mean = Vector3::Zero();
0275     for (std::size_t i = 0; i < local.size(); ++i) {
0276       sumW += w(i);
0277       mean += w(i) * local[i];
0278     }
0279     if (sumW <= 0) {
0280       return Result<FreeVector>::failure(
0281           TrackParamsEstimationError::DegenerateFit);
0282     }
0283     mean /= sumW;
0284     SquareMatrix3 cov = SquareMatrix3::Zero();
0285     for (std::size_t i = 0; i < local.size(); ++i) {
0286       const Vector3 d = local[i] - mean;
0287       cov += w(i) * d * d.transpose();
0288     }
0289     Eigen::SelfAdjointEigenSolver<SquareMatrix3> solver(cov);
0290     // A non-positive largest eigenvalue means the points coincide.
0291     if (solver.info() != Eigen::Success || (solver.eigenvalues()[2] <= 0)) {
0292       return Result<FreeVector>::failure(
0293           TrackParamsEstimationError::DegenerateFit);
0294     }
0295     Vector3 localDir = solver.eigenvectors().col(2);
0296     if (localDir.dot(local.back() - local.front()) < 0) {
0297       localDir = -localDir;
0298     }
0299     localDir.normalize();
0300     params.segment<3>(eFreeDir0) = transform.linear() * localDir;
0301     return Result<FreeVector>::success(params);
0302   }
0303 
0304   const Vector2 center = circle->center;
0305   const double radius = circle->radius;
0306 
0307   const Vector2 firstXy = local.front().head<2>();
0308   const Vector2 refXy = local[referenceIndex].head<2>();
0309 
0310   // Sense of travel around the circle, +1 for counterclockwise. Taken from the
0311   // first two points so that it does not depend on the reference point, which
0312   // may be the last one and so have no successor.
0313   const Vector2 firstRadial = firstXy - center;
0314   const Vector2 firstCcwTangent(-firstRadial.y(), firstRadial.x());
0315   const double rotSense =
0316       (firstCcwTangent.dot(local[1].head<2>() - firstXy) >= 0) ? 1 : -1;
0317 
0318   // Tangent at the reference: its radial vector rotated by +90 degrees,
0319   // oriented along travel.
0320   const Vector2 radial = refXy - center;
0321   const Vector2 tangent =
0322       (rotSense * Vector2(-radial.y(), radial.x())).normalized();
0323 
0324   // q v x B points to the center; with B along local +z, v x B = (v_y, -v_x).
0325   const double toCenterProj = tangent.y() * (center.x() - refXy.x()) -
0326                               tangent.x() * (center.y() - refXy.y());
0327   const double qSign = (toCenterProj >= 0) ? 1 : -1;
0328 
0329   // z is linear in the arc length s = radius * turning angle. Consecutive
0330   // points are assumed less than half a turn apart. The slope is invariant
0331   // under a shift of s, so s is measured from the first point rather than from
0332   // the reference, which keeps the unwrapping sequential for any reference.
0333   const double phiFirst =
0334       std::atan2(firstXy.y() - center.y(), firstXy.x() - center.x());
0335   double phiPrev = phiFirst;
0336   double sumW = 0;
0337   double sumS = 0;
0338   double sumZ = 0;
0339   double sumSS = 0;
0340   double sumSZ = 0;
0341   for (std::size_t i = 0; i < local.size(); ++i) {
0342     const Vector3& p = local[i];
0343     double phi = std::atan2(p.y() - center.y(), p.x() - center.x());
0344     while (phi - phiPrev > std::numbers::pi) {
0345       phi -= 2 * std::numbers::pi;
0346     }
0347     while (phi - phiPrev < -std::numbers::pi) {
0348       phi += 2 * std::numbers::pi;
0349     }
0350     phiPrev = phi;
0351     const double wi = w(i);
0352     const double s = rotSense * radius * (phi - phiFirst);
0353     const double z = p.z();
0354     sumW += wi;
0355     sumS += wi * s;
0356     sumZ += wi * z;
0357     sumSS += wi * s * s;
0358     sumSZ += wi * s * z;
0359   }
0360   const double denom = sumW * sumSS - sumS * sumS;
0361   const double lambda =
0362       (std::abs(denom) > std::numeric_limits<double>::epsilon())
0363           ? (sumW * sumSZ - sumS * sumZ) / denom
0364           : 0;
0365 
0366   // s is the transverse arc length, so the local direction is (t_x, t_y,
0367   // lambda).
0368   Vector3 localDir(tangent.x(), tangent.y(), lambda);
0369   localDir.normalize();
0370   params.segment<3>(eFreeDir0) = transform.linear() * localDir;
0371 
0372   // p_T = |B| * R, so q/p_T = qSign / (R * |B|). Without a field q/p stays
0373   // zero.
0374   if (bMag > std::numeric_limits<double>::epsilon()) {
0375     const double qOverPt = qSign / (radius * bMag);
0376     params[eFreeQOverP] = qOverPt / fastHypot(1, lambda);
0377   }
0378 
0379   return Result<FreeVector>::success(params);
0380 }