Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-05 08:17:31

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 #pragma once
0010 
0011 #include "Acts/Definitions/Algebra.hpp"
0012 
0013 #include <cassert>
0014 #include <cmath>
0015 #include <cstddef>
0016 #include <limits>
0017 #include <optional>
0018 #include <span>
0019 
0020 #include <Eigen/Eigenvalues>
0021 
0022 namespace Acts::detail {
0023 
0024 /// A circle in a plane, as fitted to a set of points.
0025 struct CircleFit {
0026   /// Circle center.
0027   Vector2 center = Vector2::Zero();
0028   /// Circle radius.
0029   double radius = 0;
0030 };
0031 
0032 /// Algebraic circle fit (Taubin) over the transverse `(x, y)` projection.
0033 ///
0034 /// Fits `A(x^2+y^2) + B*x + C*y + D = 0` with `A` free to reach zero, so
0035 /// near-collinear input degrades to a line, reported as `std::nullopt`,
0036 /// instead of becoming ill-conditioned. Weights are relative factors on the
0037 /// residuals; an empty span means uniform.
0038 ///
0039 /// @param points the points whose transverse projection is fitted
0040 /// @param weights optional per-point weights (empty span = uniform)
0041 /// @return the fitted circle, or `std::nullopt` if no finite circle is defined
0042 inline std::optional<CircleFit> fitCircleTaubin(
0043     std::span<const Vector3> points, std::span<const double> weights = {}) {
0044   const std::size_t n = points.size();
0045   if (n < 3) {
0046     return std::nullopt;
0047   }
0048   assert((weights.empty() || weights.size() == n) &&
0049          "weights must be empty or match the number of points");
0050 
0051   const auto weightAt = [&](std::size_t i) {
0052     return weights.empty() ? 1 : weights[i];
0053   };
0054 
0055   // Weighted centroid of the transverse projection.
0056   double sumW = 0;
0057   double meanX = 0;
0058   double meanY = 0;
0059   for (std::size_t i = 0; i < n; ++i) {
0060     const double w = weightAt(i);
0061     sumW += w;
0062     meanX += w * points[i].x();
0063     meanY += w * points[i].y();
0064   }
0065   if (sumW <= 0) {
0066     return std::nullopt;
0067   }
0068   const double invW = 1 / sumW;
0069   meanX *= invW;
0070   meanY *= invW;
0071 
0072   // Centroid-relative weighted moments, z = x^2 + y^2.
0073   double mxx = 0;
0074   double myy = 0;
0075   double mxy = 0;
0076   double mxz = 0;
0077   double myz = 0;
0078   double mzz = 0;
0079   double mz = 0;
0080   for (std::size_t i = 0; i < n; ++i) {
0081     const double w = weightAt(i);
0082     const double x = points[i].x() - meanX;
0083     const double y = points[i].y() - meanY;
0084     const double z = x * x + y * y;
0085     mxx += w * x * x;
0086     myy += w * y * y;
0087     mxy += w * x * y;
0088     mxz += w * x * z;
0089     myz += w * y * z;
0090     mzz += w * z * z;
0091     mz += w * z;
0092   }
0093   mxx *= invW;
0094   myy *= invW;
0095   mxy *= invW;
0096   mxz *= invW;
0097   myz *= invW;
0098   mzz *= invW;
0099   mz *= invW;
0100   if (mz <= 0) {
0101     // All points coincide.
0102     return std::nullopt;
0103   }
0104 
0105   // Taubin eigenproblem `M a = lambda N a` for a = (A, B, C), with D = -A*mz.
0106   SquareMatrix3 m;
0107   m << mzz - mz * mz, mxz, myz,  //
0108       mxz, mxx, mxy,             //
0109       myz, mxy, myy;
0110   SquareMatrix3 nMat = SquareMatrix3::Zero();
0111   nMat(0, 0) = 4 * mz;
0112   nMat(1, 1) = 1;
0113   nMat(2, 2) = 1;
0114 
0115   Eigen::GeneralizedSelfAdjointEigenSolver<SquareMatrix3> solver(m, nMat);
0116   if (solver.info() != Eigen::Success) {
0117     return std::nullopt;
0118   }
0119   // Ascending eigenvalues, so the first eigenvector minimizes the Taubin cost.
0120   const Vector3 a = solver.eigenvectors().col(0);
0121   const double coeffA = a(0);
0122   const double coeffB = a(1);
0123   const double coeffC = a(2);
0124   if (coeffA == 0) {
0125     // Perfectly straight: no finite circle.
0126     return std::nullopt;
0127   }
0128 
0129   const double coeffD = -coeffA * mz;
0130   const double r2 = (coeffB * coeffB + coeffC * coeffC - 4 * coeffA * coeffD) /
0131                     (4 * coeffA * coeffA);
0132   if (!std::isfinite(r2) || r2 <= 0) {
0133     return std::nullopt;
0134   }
0135   const double radius = std::sqrt(r2);
0136 
0137   // A radius far beyond the point spread sqrt(mz) is a line.
0138   if (constexpr double maxRadiusToSpread = 1e6;
0139       radius > maxRadiusToSpread * std::sqrt(mz)) {
0140     return std::nullopt;
0141   }
0142 
0143   const Vector2 centerRel(-coeffB / (2 * coeffA), -coeffC / (2 * coeffA));
0144   return CircleFit{centerRel + Vector2(meanX, meanY), radius};
0145 }
0146 
0147 /// Refine a circle fit by minimizing the radial residuals
0148 /// `sum_i (|p_i - center| - R)^2` with Gauss-Newton steps, on the transverse
0149 /// `(x, y)` projection. Weights as in @ref fitCircleTaubin.
0150 ///
0151 /// @param fit the circle fit to refine, e.g. from @ref fitCircleTaubin
0152 /// @param points the points whose transverse projection is fitted
0153 /// @param iterations the maximum number of Gauss-Newton iterations
0154 /// @param weights optional per-point weights (empty span = uniform)
0155 /// @return the refined circle, or `std::nullopt` if a step drives the radius
0156 ///         non-positive, i.e. the refinement collapses the circle
0157 inline std::optional<CircleFit> refineCircleGeometric(
0158     CircleFit fit, std::span<const Vector3> points,
0159     const std::size_t iterations, std::span<const double> weights = {}) {
0160   assert((weights.empty() || weights.size() == points.size()) &&
0161          "weights must be empty or match the number of points");
0162 
0163   const auto weightAt = [&](std::size_t i) {
0164     return weights.empty() ? 1 : weights[i];
0165   };
0166 
0167   for (std::size_t it = 0; it < iterations; ++it) {
0168     SquareMatrix3 jtj = SquareMatrix3::Zero();
0169     Vector3 jtr = Vector3::Zero();
0170     for (std::size_t i = 0; i < points.size(); ++i) {
0171       const Vector2 d = points[i].head<2>() - fit.center;
0172       const double dist = d.norm();
0173       if (dist < std::numeric_limits<double>::epsilon()) {
0174         continue;
0175       }
0176       const double w = weightAt(i);
0177       const double residual = dist - fit.radius;
0178       // Jacobian of the residual w.r.t. (cx, cy, R).
0179       const Vector3 j(-d.x() / dist, -d.y() / dist, -1);
0180       jtj += w * j * j.transpose();
0181       jtr += w * j * residual;
0182     }
0183     const Vector3 delta = jtj.ldlt().solve(-jtr);
0184     if (!delta.allFinite()) {
0185       // Singular normal equations: keep the fit as it is.
0186       break;
0187     }
0188     fit.center.x() += delta.x();
0189     fit.center.y() += delta.y();
0190     fit.radius += delta.z();
0191     if (fit.radius <= 0) {
0192       return std::nullopt;
0193     }
0194     if (delta.norm() < 1e-9) {
0195       break;
0196     }
0197   }
0198   return fit;
0199 }
0200 
0201 }  // namespace Acts::detail