Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2026-08-31 08:21:26

0001 #include "TpcTrackKalmanFitter.h"
0002 
0003 #include "TpcTrackHelixFitter.h"
0004 
0005 #include <phfield/PHField.h>
0006 
0007 #include <CLHEP/Units/SystemOfUnits.h>
0008 
0009 #include <Eigen/Dense>
0010 
0011 #include <algorithm>
0012 #include <chrono>
0013 #include <cmath>
0014 #include <limits>
0015 
0016 namespace
0017 {
0018   constexpr double kPi = 3.14159265358979323846;
0019   constexpr double kCurvaturePerCm = 0.003;
0020 
0021   template <class T>
0022   constexpr T square(const T &value)
0023   {
0024     return value * value;
0025   }
0026 
0027   double normalize_phi(const double phi)
0028   {
0029     return std::atan2(std::sin(phi), std::cos(phi));
0030   }
0031 
0032   using StateVector = Eigen::Matrix<double, 6, 1>;
0033   using StateMatrix = Eigen::Matrix<double, 6, 6>;
0034   using Vector3 = Eigen::Matrix<double, 3, 1>;
0035   using MeasurementVector = Eigen::Matrix<double, 3, 1>;
0036   using MeasurementMatrix = Eigen::Matrix<double, 3, 6>;
0037   using MeasurementCov = Eigen::Matrix<double, 3, 3>;
0038 
0039   StateVector to_eigen(const std::array<double, 6> &state)
0040   {
0041     StateVector output;
0042     for (int i = 0; i < 6; ++i)
0043     {
0044       output(i) = state[static_cast<std::size_t>(i)];
0045     }
0046     return output;
0047   }
0048 
0049   std::array<double, 6> to_array(const StateVector &state)
0050   {
0051     std::array<double, 6> output{};
0052     for (int i = 0; i < 6; ++i)
0053     {
0054       output[static_cast<std::size_t>(i)] = state(i);
0055     }
0056     return output;
0057   }
0058 
0059   std::array<double, 36> to_array(const StateMatrix &matrix)
0060   {
0061     std::array<double, 36> output{};
0062     for (int row = 0; row < 6; ++row)
0063     {
0064       for (int col = 0; col < 6; ++col)
0065       {
0066         const auto offset = static_cast<std::size_t>(row) * 6U + static_cast<std::size_t>(col);
0067         output[offset] = matrix(row, col);
0068       }
0069     }
0070     return output;
0071   }
0072 
0073   StateVector residual(const StateVector &lhs, const StateVector &rhs)
0074   {
0075     StateVector diff = lhs - rhs;
0076     diff(TpcTrackKalmanFitter::Phi) = normalize_phi(diff(TpcTrackKalmanFitter::Phi));
0077     return diff;
0078   }
0079 
0080   double omega_from_state(const StateVector &state, const double bfield_t)
0081   {
0082     return kCurvaturePerCm * bfield_t * state(TpcTrackKalmanFitter::QOverPt);
0083   }
0084 
0085   StateVector apply_mean_energy_loss_path3d(const StateVector &state,
0086                                             const double signed_path3d_cm,
0087                                             const TpcKalmanConfig &config,
0088                                             const double mass_gev)
0089   {
0090     if (config.energy_loss_gev_per_cm <= 0.0 || signed_path3d_cm == 0.0)
0091     {
0092       return state;
0093     }
0094 
0095     StateVector output = state;
0096     const double qop_t = output(TpcTrackKalmanFitter::QOverPt);
0097     if (std::abs(qop_t) < 1.0e-12)
0098     {
0099       return output;
0100     }
0101 
0102     const double tanl = output(TpcTrackKalmanFitter::TanLambda);
0103     const double path3d_cm = std::abs(signed_path3d_cm);
0104     if (path3d_cm <= 0.0)
0105     {
0106       return output;
0107     }
0108 
0109     const double pt = 1.0 / std::abs(qop_t);
0110     const double momentum = pt * std::sqrt(1.0 + tanl * tanl);
0111     const double energy = std::sqrt(momentum * momentum + mass_gev * mass_gev);
0112     const double signed_loss = std::copysign(config.energy_loss_gev_per_cm * path3d_cm,
0113                                              signed_path3d_cm);
0114     const double new_energy = std::max(mass_gev + 1.0e-9, energy - signed_loss);
0115     const double new_momentum = std::sqrt(std::max(0.0, new_energy * new_energy - mass_gev * mass_gev));
0116     if (new_momentum <= 0.0)
0117     {
0118       return output;
0119     }
0120 
0121     const double min_pt = std::max(config.min_pt_gev, 1.0e-6);
0122     const double new_pt = std::max(min_pt, new_momentum / std::sqrt(1.0 + tanl * tanl));
0123     output(TpcTrackKalmanFitter::QOverPt) = std::copysign(1.0 / new_pt, qop_t);
0124     return output;
0125   }
0126 
0127   StateVector apply_mean_energy_loss(const StateVector &state,
0128                                      const double ds_cm,
0129                                      const TpcKalmanConfig &config,
0130                                      const double mass_gev)
0131   {
0132     const double tanl = state(TpcTrackKalmanFitter::TanLambda);
0133     return apply_mean_energy_loss_path3d(state, ds_cm * std::sqrt(1.0 + tanl * tanl),
0134                                          config, mass_gev);
0135   }
0136 
0137   Vector3 direction_from_state(const StateVector &state)
0138   {
0139     Vector3 direction(std::cos(state(TpcTrackKalmanFitter::Phi)),
0140                       std::sin(state(TpcTrackKalmanFitter::Phi)),
0141                       state(TpcTrackKalmanFitter::TanLambda));
0142     const double norm = direction.norm();
0143     if (norm <= 0.0 || !std::isfinite(norm))
0144     {
0145       return Vector3(1.0, 0.0, 0.0);
0146     }
0147     return direction / norm;
0148   }
0149 
0150   Vector3 magnetic_field_tesla(const Vector3 &position_cm,
0151                                const TpcKalmanConfig &config)
0152   {
0153     if (config.magnetic_field != nullptr)
0154     {
0155       const double point[4] = {position_cm.x() * CLHEP::cm,
0156                                position_cm.y() * CLHEP::cm,
0157                                position_cm.z() * CLHEP::cm,
0158                                0.0};
0159       double field[3] = {0.0, 0.0, 0.0};
0160       config.magnetic_field->GetFieldValue(point, field);
0161       Vector3 output(field[0] / CLHEP::tesla,
0162                      field[1] / CLHEP::tesla,
0163                      field[2] / CLHEP::tesla);
0164       if (std::isfinite(output.x()) && std::isfinite(output.y()) &&
0165           std::isfinite(output.z()))
0166       {
0167         return output;
0168       }
0169       return Vector3::Zero();
0170     }
0171 
0172     return Vector3(0.0, 0.0, config.bfield_t);
0173   }
0174 
0175   Vector3 rkn_acceleration(const Vector3 &direction,
0176                            const Vector3 &bfield_tesla,
0177                            const double q_over_p)
0178   {
0179     return kCurvaturePerCm * q_over_p * direction.cross(bfield_tesla);
0180   }
0181 
0182   struct RknStep
0183   {
0184     Vector3 position;
0185     Vector3 direction;
0186     double error{0.0};
0187   };
0188 
0189   struct RknDiagnostics
0190   {
0191     std::size_t propagations{0};
0192     std::size_t accepted_steps{0};
0193     std::size_t rejected_trials{0};
0194     std::size_t max_trial_accepts{0};
0195     std::size_t failures{0};
0196     double seconds{0.0};
0197   };
0198 
0199   RknStep try_rkn4_step(const Vector3 &position_cm,
0200                         const Vector3 &direction,
0201                         const double q_over_p,
0202                         const double h_cm,
0203                         const TpcKalmanConfig &config)
0204   {
0205     const double h2 = h_cm * h_cm;
0206     const double half_h = 0.5 * h_cm;
0207 
0208     const Vector3 b_first = magnetic_field_tesla(position_cm, config);
0209     const Vector3 k1 = rkn_acceleration(direction, b_first, q_over_p);
0210 
0211     const Vector3 pos1 = position_cm + half_h * direction + 0.125 * h2 * k1;
0212     const Vector3 b_middle = magnetic_field_tesla(pos1, config);
0213     const Vector3 k2 = rkn_acceleration(direction + half_h * k1, b_middle, q_over_p);
0214     const Vector3 k3 = rkn_acceleration(direction + half_h * k2, b_middle, q_over_p);
0215 
0216     const Vector3 pos2 = position_cm + h_cm * direction + 0.5 * h2 * k3;
0217     const Vector3 b_last = magnetic_field_tesla(pos2, config);
0218     const Vector3 k4 = rkn_acceleration(direction + h_cm * k3, b_last, q_over_p);
0219 
0220     RknStep output;
0221     output.position = position_cm + h_cm * direction + h2 / 6.0 * (k1 + k2 + k3);
0222     output.direction = direction + h_cm / 6.0 * (k1 + 2.0 * (k2 + k3) + k4);
0223     const double direction_norm = output.direction.norm();
0224     if (direction_norm > 0.0 && std::isfinite(direction_norm))
0225     {
0226       output.direction /= direction_norm;
0227     }
0228     else
0229     {
0230       output.direction = direction;
0231     }
0232     output.error = std::max(1.0e-20,
0233                             h2 * (k1 - k2 - k3 + k4).template lpNorm<1>());
0234     return output;
0235   }
0236 
0237   double rkn_step_scale(const double tolerance, const double error)
0238   {
0239     if (error <= 0.0 || !std::isfinite(error))
0240     {
0241       return 1.0;
0242     }
0243     return std::clamp(std::sqrt(std::sqrt(tolerance / error)), 0.25, 4.0);
0244   }
0245 
0246   bool advance_rkn4(Vector3 &position_cm,
0247                     Vector3 &direction,
0248                     const double q_over_p,
0249                     const double signed_path3d_cm,
0250                     const TpcKalmanConfig &config,
0251                     RknDiagnostics *diagnostics)
0252   {
0253     double remaining = signed_path3d_cm;
0254     if (remaining == 0.0)
0255     {
0256       return true;
0257     }
0258 
0259     constexpr double min_step_cm = 1.0e-5;
0260     const std::size_t max_total_steps =
0261         static_cast<std::size_t>(std::max(config.rkn_max_total_steps, 1));
0262     const double max_step_cm = std::max(std::abs(config.rkn_max_step_cm), min_step_cm);
0263     const double tolerance = std::max(config.rkn_step_tolerance, 1.0e-20);
0264     const int max_trials = std::max(config.rkn_max_step_trials, 1);
0265     const bool adaptive = config.rkn_step_tolerance > 0.0;
0266 
0267     double h = std::copysign(std::min(std::abs(remaining), max_step_cm), remaining);
0268     for (std::size_t nsteps = 0;
0269          std::abs(remaining) > min_step_cm && nsteps < max_total_steps;
0270          ++nsteps)
0271     {
0272       if (std::abs(h) > std::abs(remaining))
0273       {
0274         h = remaining;
0275       }
0276 
0277       RknStep trial;
0278       int ntrials = 0;
0279       while (true)
0280       {
0281         trial = try_rkn4_step(position_cm, direction, q_over_p, h, config);
0282         if (!trial.position.allFinite() || !trial.direction.allFinite() ||
0283             !std::isfinite(trial.error))
0284         {
0285           if (diagnostics != nullptr)
0286           {
0287             ++diagnostics->failures;
0288           }
0289           return false;
0290         }
0291         if (!adaptive || trial.error <= 4.0 * tolerance ||
0292             std::abs(h) <= min_step_cm || ntrials >= max_trials)
0293         {
0294           if (adaptive && trial.error > 4.0 * tolerance && ntrials >= max_trials &&
0295               diagnostics != nullptr)
0296           {
0297             ++diagnostics->max_trial_accepts;
0298           }
0299           break;
0300         }
0301         h *= rkn_step_scale(tolerance, trial.error);
0302         ++ntrials;
0303         if (diagnostics != nullptr)
0304         {
0305           ++diagnostics->rejected_trials;
0306         }
0307       }
0308 
0309       position_cm = trial.position;
0310       direction = trial.direction;
0311       remaining -= h;
0312       if (diagnostics != nullptr)
0313       {
0314         ++diagnostics->accepted_steps;
0315       }
0316 
0317       if (std::abs(remaining) <= min_step_cm)
0318       {
0319         break;
0320       }
0321 
0322       double next_h = std::abs(h);
0323       if (adaptive)
0324       {
0325         next_h *= rkn_step_scale(tolerance, trial.error);
0326       }
0327       next_h = std::min({next_h, max_step_cm, std::abs(remaining)});
0328       h = std::copysign(std::max(next_h, min_step_cm), remaining);
0329     }
0330 
0331     const bool complete = std::abs(remaining) <= min_step_cm;
0332     if (!complete && diagnostics != nullptr)
0333     {
0334       ++diagnostics->failures;
0335     }
0336     return complete;
0337   }
0338 
0339   StateVector propagate_rkn4(const StateVector &state,
0340                              const double ds_cm,
0341                              const TpcKalmanConfig &config,
0342                              const double mass_gev,
0343                              RknDiagnostics *diagnostics = nullptr)
0344   {
0345     const auto propagation_start = std::chrono::steady_clock::now();
0346     const auto record_seconds = [&]()
0347     {
0348       if (diagnostics != nullptr)
0349       {
0350         diagnostics->seconds += std::chrono::duration<double>(
0351                                     std::chrono::steady_clock::now() - propagation_start)
0352                                     .count();
0353       }
0354     };
0355     if (diagnostics != nullptr)
0356     {
0357       ++diagnostics->propagations;
0358     }
0359     StateVector output = state;
0360     if (ds_cm == 0.0)
0361     {
0362       output = apply_mean_energy_loss(output, ds_cm, config, mass_gev);
0363       record_seconds();
0364       return output;
0365     }
0366 
0367     Vector3 position_cm(state(TpcTrackKalmanFitter::X),
0368                         state(TpcTrackKalmanFitter::Y),
0369                         state(TpcTrackKalmanFitter::Z));
0370     Vector3 direction = direction_from_state(state);
0371     const double transverse = std::hypot(direction.x(), direction.y());
0372     if (transverse <= 1.0e-12 || !std::isfinite(transverse))
0373     {
0374       output = apply_mean_energy_loss(output, ds_cm, config, mass_gev);
0375       record_seconds();
0376       return output;
0377     }
0378 
0379     const double q_over_p = -state(TpcTrackKalmanFitter::QOverPt) * transverse;
0380     const double signed_path3d_cm = ds_cm / transverse;
0381     if (!advance_rkn4(position_cm, direction, q_over_p, signed_path3d_cm,
0382                       config, diagnostics))
0383     {
0384       record_seconds();
0385       return StateVector::Constant(std::numeric_limits<double>::quiet_NaN());
0386     }
0387 
0388     const double final_transverse = std::hypot(direction.x(), direction.y());
0389     output(TpcTrackKalmanFitter::X) = position_cm.x();
0390     output(TpcTrackKalmanFitter::Y) = position_cm.y();
0391     output(TpcTrackKalmanFitter::Z) = position_cm.z();
0392     if (final_transverse > 1.0e-12 && std::isfinite(final_transverse))
0393     {
0394       output(TpcTrackKalmanFitter::Phi) =
0395           normalize_phi(std::atan2(direction.y(), direction.x()));
0396       output(TpcTrackKalmanFitter::TanLambda) = direction.z() / final_transverse;
0397       output(TpcTrackKalmanFitter::QOverPt) = -q_over_p / final_transverse;
0398       const double max_abs_qop_t = 1.0 / std::max(config.min_pt_gev, 1.0e-6);
0399       if (std::abs(output(TpcTrackKalmanFitter::QOverPt)) > max_abs_qop_t)
0400       {
0401         output(TpcTrackKalmanFitter::QOverPt) =
0402             std::copysign(max_abs_qop_t, output(TpcTrackKalmanFitter::QOverPt));
0403       }
0404     }
0405 
0406     output = apply_mean_energy_loss_path3d(output, signed_path3d_cm, config, mass_gev);
0407     record_seconds();
0408     return output;
0409   }
0410 
0411   StateVector propagate_uniform_bz(const StateVector &state,
0412                                    const double ds_cm,
0413                                    const double bfield_t,
0414                                    const TpcKalmanConfig &config,
0415                                    const double mass_gev)
0416   {
0417     StateVector output = state;
0418     const double phi = state(TpcTrackKalmanFitter::Phi);
0419     const double omega = kCurvaturePerCm * bfield_t *
0420                          state(TpcTrackKalmanFitter::QOverPt);
0421 
0422     if (std::abs(omega) < 1.0e-12)
0423     {
0424       output(TpcTrackKalmanFitter::X) += ds_cm * std::cos(phi);
0425       output(TpcTrackKalmanFitter::Y) += ds_cm * std::sin(phi);
0426     }
0427     else
0428     {
0429       const double phi2 = phi + omega * ds_cm;
0430       output(TpcTrackKalmanFitter::X) +=
0431           (std::sin(phi2) - std::sin(phi)) / omega;
0432       output(TpcTrackKalmanFitter::Y) +=
0433           -(std::cos(phi2) - std::cos(phi)) / omega;
0434       output(TpcTrackKalmanFitter::Phi) = normalize_phi(phi2);
0435     }
0436     output(TpcTrackKalmanFitter::Z) +=
0437         state(TpcTrackKalmanFitter::TanLambda) * ds_cm;
0438     return apply_mean_energy_loss(output, ds_cm, config, mass_gev);
0439   }
0440 
0441   StateMatrix transport_jacobian(const StateVector &state,
0442                                  const double ds_cm,
0443                                  const TpcKalmanConfig &config,
0444                                  const double mass_gev,
0445                                  RknDiagnostics *diagnostics)
0446   {
0447     StateMatrix jac = StateMatrix::Identity();
0448     const bool use_analytic_uniform =
0449         config.magnetic_field == nullptr && config.analytic_uniform_propagation;
0450     const bool use_fast_field_jacobian =
0451         config.magnetic_field != nullptr && config.rkn_fast_field_jacobian;
0452     double local_bz_t = config.bfield_t;
0453     if (use_fast_field_jacobian)
0454     {
0455       const Vector3 position_cm(state(TpcTrackKalmanFitter::X),
0456                                 state(TpcTrackKalmanFitter::Y),
0457                                 state(TpcTrackKalmanFitter::Z));
0458       local_bz_t = magnetic_field_tesla(position_cm, config).z();
0459       if (!std::isfinite(local_bz_t))
0460       {
0461         local_bz_t = config.bfield_t;
0462       }
0463     }
0464 
0465     const auto propagate_for_jacobian = [&](const StateVector &input)
0466     {
0467       if (use_analytic_uniform || use_fast_field_jacobian)
0468       {
0469         return propagate_uniform_bz(input, ds_cm, local_bz_t, config, mass_gev);
0470       }
0471       return propagate_rkn4(input, ds_cm, config, mass_gev, diagnostics);
0472     };
0473     const double scales[6] = {1.0e-4, 1.0e-4, 1.0e-4, 1.0e-5, 1.0e-6, 1.0e-6};
0474     for (int col = 0; col < 6; ++col)
0475     {
0476       const double step = scales[col] * std::max(1.0, std::abs(state(col)));
0477       StateVector plus = state;
0478       StateVector minus = state;
0479       plus(col) += step;
0480       minus(col) -= step;
0481       if (col == TpcTrackKalmanFitter::Phi)
0482       {
0483         plus(col) = normalize_phi(plus(col));
0484         minus(col) = normalize_phi(minus(col));
0485       }
0486 
0487       const StateVector f_plus = propagate_for_jacobian(plus);
0488       const StateVector f_minus = propagate_for_jacobian(minus);
0489       if (!f_plus.allFinite() || !f_minus.allFinite())
0490       {
0491         return StateMatrix::Constant(std::numeric_limits<double>::quiet_NaN());
0492       }
0493       jac.col(col) = residual(f_plus, f_minus) / (2.0 * step);
0494     }
0495     return jac;
0496   }
0497 
0498   double multiple_scattering_theta0(const StateVector &state,
0499                                     const double ds_cm,
0500                                     const TpcKalmanConfig &config,
0501                                     const double mass_gev)
0502   {
0503     if (config.material_x0_per_cm <= 0.0 || ds_cm == 0.0)
0504     {
0505       return 0.0;
0506     }
0507 
0508     const double qop_t = state(TpcTrackKalmanFitter::QOverPt);
0509     if (std::abs(qop_t) < 1.0e-12)
0510     {
0511       return 0.0;
0512     }
0513 
0514     const double tanl = state(TpcTrackKalmanFitter::TanLambda);
0515     const double path3d_cm = std::abs(ds_cm) * std::sqrt(1.0 + tanl * tanl);
0516     const double x_over_x0 = config.material_x0_per_cm * path3d_cm;
0517     if (x_over_x0 <= 0.0)
0518     {
0519       return 0.0;
0520     }
0521 
0522     const double pt = 1.0 / std::abs(qop_t);
0523     const double momentum = pt * std::sqrt(1.0 + tanl * tanl);
0524     const double energy = std::sqrt(momentum * momentum + mass_gev * mass_gev);
0525     const double beta = (energy > 0.0) ? momentum / energy : 0.0;
0526     if (beta <= 0.0 || momentum <= 0.0)
0527     {
0528       return 0.0;
0529     }
0530 
0531     const double log_term = 1.0 + 0.038 * std::log(x_over_x0);
0532     return config.multiple_scattering_scale * 0.0136 / (beta * momentum) *
0533            std::sqrt(x_over_x0) * log_term;
0534   }
0535 
0536   StateMatrix process_noise(const StateVector &state,
0537                             const double ds_cm,
0538                             const TpcKalmanConfig &config,
0539                             const double mass_gev)
0540   {
0541     const double scale = std::max(1.0, std::abs(ds_cm));
0542     StateMatrix noise = StateMatrix::Zero();
0543     noise(TpcTrackKalmanFitter::X, TpcTrackKalmanFitter::X) = square(config.process_sigma_pos_cm * scale);
0544     noise(TpcTrackKalmanFitter::Y, TpcTrackKalmanFitter::Y) = square(config.process_sigma_pos_cm * scale);
0545     noise(TpcTrackKalmanFitter::Z, TpcTrackKalmanFitter::Z) = square(config.process_sigma_pos_cm * scale);
0546     noise(TpcTrackKalmanFitter::Phi, TpcTrackKalmanFitter::Phi) = square(config.process_sigma_phi * scale);
0547     noise(TpcTrackKalmanFitter::QOverPt, TpcTrackKalmanFitter::QOverPt) = square(config.process_sigma_qop_t * scale);
0548     noise(TpcTrackKalmanFitter::TanLambda, TpcTrackKalmanFitter::TanLambda) = square(config.process_sigma_tanl * scale);
0549 
0550     const double theta0 = multiple_scattering_theta0(state, ds_cm, config, mass_gev);
0551     if (theta0 > 0.0)
0552     {
0553       const double tanl = state(TpcTrackKalmanFitter::TanLambda);
0554       const double sec2_lambda = 1.0 + square(tanl);
0555       noise(TpcTrackKalmanFitter::Phi, TpcTrackKalmanFitter::Phi) +=
0556           square(theta0 * std::sqrt(sec2_lambda));
0557       noise(TpcTrackKalmanFitter::TanLambda, TpcTrackKalmanFitter::TanLambda) +=
0558           square(theta0 * sec2_lambda);
0559     }
0560 
0561     if (config.energy_loss_sigma_fraction > 0.0 && config.energy_loss_gev_per_cm > 0.0)
0562     {
0563       const double tanl = state(TpcTrackKalmanFitter::TanLambda);
0564       const double path3d_cm = std::abs(ds_cm) * std::sqrt(1.0 + tanl * tanl);
0565       const double loss_sigma = config.energy_loss_sigma_fraction *
0566                                 config.energy_loss_gev_per_cm * path3d_cm;
0567       const double qop_t = state(TpcTrackKalmanFitter::QOverPt);
0568       if (std::abs(qop_t) > 1.0e-12)
0569       {
0570         const double pt = 1.0 / std::abs(qop_t);
0571         const double momentum = pt * std::sqrt(1.0 + tanl * tanl);
0572         if (momentum > 0.0)
0573         {
0574           noise(TpcTrackKalmanFitter::QOverPt, TpcTrackKalmanFitter::QOverPt) +=
0575               square(qop_t * loss_sigma / momentum);
0576         }
0577       }
0578     }
0579 
0580     return noise;
0581   }
0582 
0583   MeasurementCov measurement_covariance(const TpcTrackPoint &point,
0584                                         const double var_rphi,
0585                                         const double var_r,
0586                                         const double var_z)
0587   {
0588     const double radius = std::hypot(point.position.x, point.position.y);
0589     const double cos_phi = (radius > 0.0) ? point.position.x / radius : 1.0;
0590     const double sin_phi = (radius > 0.0) ? point.position.y / radius : 0.0;
0591 
0592     MeasurementCov cov = MeasurementCov::Zero();
0593     cov(0, 0) = var_r * square(cos_phi) + var_rphi * square(sin_phi);
0594     cov(1, 1) = var_r * square(sin_phi) + var_rphi * square(cos_phi);
0595     cov(0, 1) = (var_r - var_rphi) * sin_phi * cos_phi;
0596     cov(1, 0) = cov(0, 1);
0597     cov(2, 2) = var_z;
0598     return cov;
0599   }
0600 
0601   MeasurementCov measurement_local_rotation(const TpcTrackPoint &point)
0602   {
0603     const double radius = std::hypot(point.position.x, point.position.y);
0604     const double cos_phi = (radius > 0.0) ? point.position.x / radius : 1.0;
0605     const double sin_phi = (radius > 0.0) ? point.position.y / radius : 0.0;
0606 
0607     MeasurementCov rotation = MeasurementCov::Zero();
0608     rotation(0, 0) = cos_phi;
0609     rotation(0, 1) = sin_phi;
0610     rotation(1, 0) = -sin_phi;
0611     rotation(1, 1) = cos_phi;
0612     rotation(2, 2) = 1.0;
0613     return rotation;
0614   }
0615 
0616   double covariance_sigma(const MeasurementCov &covariance, const int index)
0617   {
0618     return std::sqrt(std::max(0.0, covariance(index, index)));
0619   }
0620 
0621   double covariance_correlation(const MeasurementCov &covariance,
0622                                 const int first,
0623                                 const int second,
0624                                 const double sigma_first,
0625                                 const double sigma_second)
0626   {
0627     const double denominator = sigma_first * sigma_second;
0628     if (!(denominator > 0.0) || !std::isfinite(denominator))
0629     {
0630       return std::numeric_limits<double>::quiet_NaN();
0631     }
0632     return std::clamp(covariance(first, second) / denominator, -1.0, 1.0);
0633   }
0634 
0635   MeasurementVector whiten_innovation(const MeasurementCov &innovation,
0636                                       const MeasurementVector &residual)
0637   {
0638     MeasurementVector whitened = MeasurementVector::Constant(
0639         std::numeric_limits<double>::quiet_NaN());
0640 
0641     const Eigen::LLT<MeasurementCov> llt(innovation);
0642     if (llt.info() == Eigen::Success)
0643     {
0644       whitened = llt.matrixL().solve(residual);
0645       return whitened;
0646     }
0647 
0648     const Eigen::SelfAdjointEigenSolver<MeasurementCov> eigensolver(innovation);
0649     if (eigensolver.info() != Eigen::Success ||
0650         eigensolver.eigenvalues().minCoeff() <= 0.0)
0651     {
0652       return whitened;
0653     }
0654 
0655     whitened = eigensolver.eigenvalues().cwiseSqrt().cwiseInverse().asDiagonal() *
0656                eigensolver.eigenvectors().transpose() * residual;
0657     return whitened;
0658   }
0659 
0660   bool make_seed(const std::vector<TpcTrackPoint> &points,
0661                  const TpcKalmanConfig &config,
0662                  TpcTrackHelix &seed,
0663                  std::vector<double> &theta_values,
0664                  std::vector<double> &path_s)
0665   {
0666     if (!TpcTrackHelixFitter::fit(points, 0, config.bfield_t, seed))
0667     {
0668       return false;
0669     }
0670 
0671     theta_values.clear();
0672     theta_values.reserve(points.size());
0673     for (const auto &point : points)
0674     {
0675       double theta = std::atan2(point.position.y - seed.cy, point.position.x - seed.cx);
0676       if (!theta_values.empty())
0677       {
0678         while (theta - theta_values.back() > kPi)
0679         {
0680           theta -= 2.0 * kPi;
0681         }
0682         while (theta - theta_values.back() < -kPi)
0683         {
0684           theta += 2.0 * kPi;
0685         }
0686       }
0687       theta_values.push_back(theta);
0688     }
0689 
0690     if (theta_values.size() < 2)
0691     {
0692       return false;
0693     }
0694 
0695     path_s.assign(theta_values.size(), 0.0);
0696     for (std::size_t i = 1; i < theta_values.size(); ++i)
0697     {
0698       path_s[i] = path_s[i - 1] + seed.radius * std::abs(theta_values[i] - theta_values[i - 1]);
0699     }
0700     return true;
0701   }
0702 
0703   bool initial_state(const std::vector<TpcTrackPoint> &points,
0704                      const TpcTrackHelix &seed,
0705                      const std::vector<double> &theta_values,
0706                      const TpcKalmanConfig &config,
0707                      StateVector &state)
0708   {
0709     if (points.empty() || theta_values.empty())
0710     {
0711       return false;
0712     }
0713 
0714     const double direction = seed.direction;
0715     const double theta0 = theta_values.front();
0716     const TpcTrackVec3 seed_position = TpcTrackHelixFitter::point(seed, theta0);
0717     if (!TpcTrackHelixFitter::finite(seed_position))
0718     {
0719       return false;
0720     }
0721 
0722     const double denom = kCurvaturePerCm * config.bfield_t * seed.radius;
0723     if (std::abs(denom) <= 0.0 || !std::isfinite(denom))
0724     {
0725       return false;
0726     }
0727 
0728     state.setZero();
0729     state(TpcTrackKalmanFitter::X) = seed_position.x;
0730     state(TpcTrackKalmanFitter::Y) = seed_position.y;
0731     state(TpcTrackKalmanFitter::Z) = seed_position.z;
0732     state(TpcTrackKalmanFitter::Phi) = normalize_phi(std::atan2(direction * std::cos(theta0),
0733                                                                 direction * -std::sin(theta0)));
0734     state(TpcTrackKalmanFitter::QOverPt) = direction / denom;
0735     state(TpcTrackKalmanFitter::TanLambda) = direction * seed.pitch / seed.radius;
0736 
0737     const double max_abs_qop_t = 1.0 / std::max(config.min_pt_gev, 1.0e-6);
0738     if (std::abs(state(TpcTrackKalmanFitter::QOverPt)) > max_abs_qop_t)
0739     {
0740       state(TpcTrackKalmanFitter::QOverPt) = std::copysign(max_abs_qop_t,
0741                                                            state(TpcTrackKalmanFitter::QOverPt));
0742     }
0743     return true;
0744   }
0745 }  // namespace
0746 
0747 bool TpcTrackKalmanFitter::fit(const std::vector<TpcTrackPoint> &input_points,
0748                                const int charge,
0749                                const TpcKalmanConfig &config,
0750                                TpcKalmanResult &result,
0751                                const double mass_gev)
0752 {
0753   result = TpcKalmanResult{};
0754   result.charge = charge;
0755   result.bfield_t = config.bfield_t;
0756   result.magnetic_field = config.magnetic_field;
0757   result.analytic_uniform_propagation = config.analytic_uniform_propagation;
0758   result.mass_gev = mass_gev;
0759 
0760   if (input_points.size() < 5)
0761   {
0762     result.message = "need at least five TPC points";
0763     return false;
0764   }
0765 
0766   std::vector<TpcTrackPoint> points = input_points;
0767   TpcTrackHelixFitter::order_points(points, config.point_order);
0768 
0769   std::vector<double> theta_values;
0770   if (!make_seed(points, config, result.seed, theta_values, result.path_s))
0771   {
0772     result.message = "helix seed failed";
0773     return false;
0774   }
0775 
0776   StateVector state;
0777   if (!initial_state(points, result.seed, theta_values, config, state))
0778   {
0779     result.message = "initial state failed";
0780     return false;
0781   }
0782 
0783   const std::size_t npoints = points.size();
0784   std::vector<StateVector> states_filtered(npoints);
0785   std::vector<StateVector> states_predicted(npoints);
0786   std::vector<StateMatrix> covs_filtered(npoints);
0787   std::vector<StateMatrix> covs_predicted(npoints);
0788   std::vector<StateMatrix> transport(npoints);
0789 
0790   const double min_meas = std::max(config.min_measurement_sigma_cm, 1.0e-12);
0791   const double sigma_rphi = std::max(config.meas_sigma_rphi_cm, min_meas);
0792   const double sigma_r = std::max(config.meas_sigma_r_cm, min_meas);
0793   const double sigma_z = std::max(config.meas_sigma_z_cm, min_meas);
0794   const double var_rphi = square(sigma_rphi);
0795   const double var_r = square(sigma_r);
0796   const double var_z = square(sigma_z);
0797 
0798   MeasurementMatrix hmat = MeasurementMatrix::Zero();
0799   hmat(0, X) = 1.0;
0800   hmat(1, Y) = 1.0;
0801   hmat(2, Z) = 1.0;
0802 
0803   StateMatrix cov = StateMatrix::Zero();
0804   cov(X, X) = square(config.initial_sigma_pos_cm);
0805   cov(Y, Y) = square(config.initial_sigma_pos_cm);
0806   cov(Z, Z) = square(config.initial_sigma_pos_cm);
0807   cov(Phi, Phi) = square(config.initial_sigma_phi);
0808   cov(QOverPt, QOverPt) = square(config.initial_sigma_qop_t);
0809   cov(TanLambda, TanLambda) = square(config.initial_sigma_tanl);
0810 
0811   double chi2 = 0.0;
0812   int ndof = 0;
0813   const StateMatrix eye = StateMatrix::Identity();
0814   RknDiagnostics rkn_diagnostics;
0815 
0816   result.measurement_chi2.reserve(npoints);
0817   result.measurement_used.reserve(npoints);
0818   if (config.collect_innovation_components)
0819   {
0820     result.measurement_in_seed.reserve(npoints);
0821     result.innovation_residual_r.reserve(npoints);
0822     result.innovation_residual_rphi.reserve(npoints);
0823     result.innovation_residual_z.reserve(npoints);
0824     result.prediction_sigma_r.reserve(npoints);
0825     result.prediction_sigma_rphi.reserve(npoints);
0826     result.prediction_sigma_z.reserve(npoints);
0827     result.innovation_sigma_r.reserve(npoints);
0828     result.innovation_sigma_rphi.reserve(npoints);
0829     result.innovation_sigma_z.reserve(npoints);
0830     result.innovation_rho_r_rphi.reserve(npoints);
0831     result.innovation_rho_r_z.reserve(npoints);
0832     result.innovation_rho_rphi_z.reserve(npoints);
0833     result.innovation_whitened_0.reserve(npoints);
0834     result.innovation_whitened_1.reserve(npoints);
0835     result.innovation_whitened_2.reserve(npoints);
0836   }
0837 
0838   const auto copy_rkn_diagnostics = [&]()
0839   {
0840     result.rkn_propagations = rkn_diagnostics.propagations;
0841     result.rkn_accepted_steps = rkn_diagnostics.accepted_steps;
0842     result.rkn_rejected_trials = rkn_diagnostics.rejected_trials;
0843     result.rkn_max_trial_accepts = rkn_diagnostics.max_trial_accepts;
0844     result.rkn_failures = rkn_diagnostics.failures;
0845     result.rkn_seconds = rkn_diagnostics.seconds;
0846   };
0847 
0848   for (std::size_t index = 0; index < npoints; ++index)
0849   {
0850     StateVector pred_state = state;
0851     StateMatrix pred_cov = cov;
0852     StateMatrix fmat = eye;
0853     if (index > 0)
0854     {
0855       const double ds = result.path_s[index] - result.path_s[index - 1];
0856       fmat = transport_jacobian(state, ds, config, mass_gev, &rkn_diagnostics);
0857       pred_state = (config.magnetic_field == nullptr && config.analytic_uniform_propagation)
0858                        ? propagate_uniform_bz(state, ds, config.bfield_t, config, mass_gev)
0859                        : propagate_rkn4(state, ds, config, mass_gev, &rkn_diagnostics);
0860       if (!fmat.allFinite() || !pred_state.allFinite())
0861       {
0862         copy_rkn_diagnostics();
0863         result.message = "RK propagation exceeded its step budget or became non-finite";
0864         return false;
0865       }
0866       pred_cov = fmat * cov * fmat.transpose() + process_noise(state, ds, config, mass_gev);
0867       pred_cov = 0.5 * (pred_cov + pred_cov.transpose()).eval();
0868     }
0869 
0870     MeasurementVector measurement;
0871     measurement << points[index].position.x, points[index].position.y, points[index].position.z;
0872     const MeasurementCov meas_cov = measurement_covariance(points[index], var_rphi, var_r, var_z);
0873     const MeasurementVector meas_residual = measurement - hmat * pred_state;
0874     const MeasurementCov innovation = hmat * pred_cov * hmat.transpose() + meas_cov;
0875     const MeasurementCov innovation_inv =
0876         innovation.completeOrthogonalDecomposition().solve(MeasurementCov::Identity());
0877     const double step_chi2 =
0878         (meas_residual.transpose() * innovation_inv * meas_residual)(0, 0);
0879 
0880     if (config.collect_innovation_components)
0881     {
0882       const MeasurementCov local_rotation = measurement_local_rotation(points[index]);
0883       const MeasurementVector local_residual = local_rotation * meas_residual;
0884       const MeasurementCov predicted_position_cov = hmat * pred_cov * hmat.transpose();
0885       const MeasurementCov local_prediction_cov =
0886           local_rotation * predicted_position_cov * local_rotation.transpose();
0887       const MeasurementCov local_innovation =
0888           local_rotation * innovation * local_rotation.transpose();
0889 
0890       const double prediction_sigma_r = covariance_sigma(local_prediction_cov, 0);
0891       const double prediction_sigma_rphi = covariance_sigma(local_prediction_cov, 1);
0892       const double prediction_sigma_z = covariance_sigma(local_prediction_cov, 2);
0893       const double innovation_sigma_r = covariance_sigma(local_innovation, 0);
0894       const double innovation_sigma_rphi = covariance_sigma(local_innovation, 1);
0895       const double innovation_sigma_z = covariance_sigma(local_innovation, 2);
0896       const MeasurementVector whitened = whiten_innovation(local_innovation, local_residual);
0897 
0898       // The current seed is a global helix fit, so every measurement contributes
0899       // to it. This marker will distinguish bootstrap seed points once the seed
0900       // is restricted to an initial consecutive subset.
0901       result.measurement_in_seed.push_back(1U);
0902       result.innovation_residual_r.push_back(local_residual(0));
0903       result.innovation_residual_rphi.push_back(local_residual(1));
0904       result.innovation_residual_z.push_back(local_residual(2));
0905       result.prediction_sigma_r.push_back(prediction_sigma_r);
0906       result.prediction_sigma_rphi.push_back(prediction_sigma_rphi);
0907       result.prediction_sigma_z.push_back(prediction_sigma_z);
0908       result.innovation_sigma_r.push_back(innovation_sigma_r);
0909       result.innovation_sigma_rphi.push_back(innovation_sigma_rphi);
0910       result.innovation_sigma_z.push_back(innovation_sigma_z);
0911       result.innovation_rho_r_rphi.push_back(
0912           covariance_correlation(local_innovation, 0, 1,
0913                                  innovation_sigma_r, innovation_sigma_rphi));
0914       result.innovation_rho_r_z.push_back(
0915           covariance_correlation(local_innovation, 0, 2,
0916                                  innovation_sigma_r, innovation_sigma_z));
0917       result.innovation_rho_rphi_z.push_back(
0918           covariance_correlation(local_innovation, 1, 2,
0919                                  innovation_sigma_rphi, innovation_sigma_z));
0920       result.innovation_whitened_0.push_back(whitened(0));
0921       result.innovation_whitened_1.push_back(whitened(1));
0922       result.innovation_whitened_2.push_back(whitened(2));
0923     }
0924 
0925     const Eigen::Matrix<double, 6, 3> gain = pred_cov * hmat.transpose() * innovation_inv;
0926     state = pred_state + gain * meas_residual;
0927     state(Phi) = normalize_phi(state(Phi));
0928     cov = (eye - gain * hmat) * pred_cov * (eye - gain * hmat).transpose() +
0929           gain * meas_cov * gain.transpose();
0930     cov = 0.5 * (cov + cov.transpose()).eval();
0931 
0932     result.measurement_chi2.push_back(step_chi2);
0933     result.measurement_used.push_back(1U);
0934     ++result.naccepted;
0935     chi2 += step_chi2;
0936     ndof += 3;
0937 
0938     states_predicted[index] = pred_state;
0939     covs_predicted[index] = pred_cov;
0940     transport[index] = fmat;
0941     states_filtered[index] = state;
0942     covs_filtered[index] = cov;
0943   }
0944 
0945   std::vector<StateVector> states_smoothed = states_filtered;
0946   std::vector<StateMatrix> covs_smoothed = covs_filtered;
0947   for (std::size_t next_index = npoints - 1; next_index > 0; --next_index)
0948   {
0949     const std::size_t index = next_index - 1;
0950     const StateMatrix pred_inv = covs_predicted[next_index]
0951                                      .completeOrthogonalDecomposition()
0952                                      .solve(StateMatrix::Identity());
0953     const StateMatrix smoother_gain =
0954         covs_filtered[index] *
0955         transport[next_index].transpose() *
0956         pred_inv;
0957     const StateVector smooth_residual = residual(states_smoothed[next_index],
0958                                                  states_predicted[next_index]);
0959     states_smoothed[index] = states_filtered[index] + smoother_gain * smooth_residual;
0960     states_smoothed[index](Phi) = normalize_phi(states_smoothed[index](Phi));
0961     covs_smoothed[index] =
0962         covs_filtered[index] +
0963         smoother_gain *
0964             (covs_smoothed[next_index] - covs_predicted[next_index]) *
0965             smoother_gain.transpose();
0966     covs_smoothed[index] =
0967         0.5 * (covs_smoothed[index] +
0968                covs_smoothed[index].transpose())
0969                   .eval();
0970   }
0971 
0972   result.states_filtered.reserve(npoints);
0973   result.covs_filtered.reserve(npoints);
0974   result.states_smoothed.reserve(npoints);
0975   result.covs_smoothed.reserve(npoints);
0976   for (std::size_t index = 0; index < npoints; ++index)
0977   {
0978     result.states_filtered.push_back(to_array(states_filtered[index]));
0979     result.covs_filtered.push_back(to_array(covs_filtered[index]));
0980     result.states_smoothed.push_back(to_array(states_smoothed[index]));
0981     result.covs_smoothed.push_back(to_array(covs_smoothed[index]));
0982   }
0983 
0984   result.chi2 = chi2;
0985   result.ndof = ndof - StateDim;
0986   copy_rkn_diagnostics();
0987   result.success = true;
0988   result.message = "ok";
0989   return true;
0990 }
0991 
0992 TpcTrackVec3 TpcTrackKalmanFitter::state_position(const std::array<double, StateDim> &state)
0993 {
0994   return {state[X], state[Y], state[Z]};
0995 }
0996 
0997 TpcTrackVec3 TpcTrackKalmanFitter::state_momentum(const std::array<double, StateDim> &state)
0998 {
0999   const double qop_t = state[QOverPt];
1000   const double pt = (std::abs(qop_t) < 1.0e-12) ? 1.0e12 : 1.0 / std::abs(qop_t);
1001   return {
1002       pt * std::cos(state[Phi]),
1003       pt * std::sin(state[Phi]),
1004       pt * state[TanLambda]};
1005 }
1006 
1007 TpcTrackVec3 TpcTrackKalmanFitter::state_tangent(const std::array<double, StateDim> &state)
1008 {
1009   return {
1010       std::cos(state[Phi]),
1011       std::sin(state[Phi]),
1012       state[TanLambda]};
1013 }
1014 
1015 std::array<double, TpcTrackKalmanFitter::StateDim> TpcTrackKalmanFitter::propagation_state(
1016     const TpcKalmanResult &fit,
1017     const TpcTrackVec3 & /*reference_vertex*/)
1018 {
1019   if (!fit.success || fit.states_smoothed.empty())
1020   {
1021     return {};
1022   }
1023 
1024   if (fit.charge == 0)
1025   {
1026     return fit.states_smoothed.front();
1027   }
1028 
1029   const double charge_sign = static_cast<double>((fit.charge > 0) ? 1 : -1);
1030   const double physical_qop_sign = -charge_sign;
1031   const double sequence_qop = fit.states_smoothed.front()[QOverPt];
1032   const bool sequence_runs_against_physical = sequence_qop * physical_qop_sign < 0.0;
1033 
1034   // The fitted states are assumed to be in a continuous along-track sequence.
1035   // Charge fixes only which end of that sequence is the physical initial point;
1036   // do not use the reference vertex or any transverse-distance heuristic here.
1037   auto state = sequence_runs_against_physical ? fit.states_smoothed.back()
1038                                               : fit.states_smoothed.front();
1039 
1040   const double qop_t = state[QOverPt];
1041   if (std::abs(qop_t) < 1.0e-12)
1042   {
1043     return state;
1044   }
1045 
1046   const double pt = 1.0 / std::abs(qop_t);
1047   const double fit_omega = kCurvaturePerCm * fit.bfield_t * qop_t;
1048   if (std::abs(fit_omega) < 1.0e-12)
1049   {
1050     return state;
1051   }
1052 
1053   const double fit_center_x = state[X] - std::sin(state[Phi]) / fit_omega;
1054   const double fit_center_y = state[Y] + std::cos(state[Phi]) / fit_omega;
1055 
1056   // Internal convention: omega = 0.003 * B * qop_t.  For a physical charge in
1057   // a solenoidal field this corresponds to qop_t = -charge / pT.  The Kalman
1058   // fit itself may have the opposite sign if the input point order was reversed,
1059   // so choose the tangent direction that preserves the fitted circle center.
1060   const double physical_qop_t = -charge_sign / pt;
1061   const double physical_omega = kCurvaturePerCm * fit.bfield_t * physical_qop_t;
1062   if (std::abs(physical_omega) < 1.0e-12)
1063   {
1064     return state;
1065   }
1066 
1067   auto center_distance2 = [&](const double phi)
1068   {
1069     const double cx = state[X] - std::sin(phi) / physical_omega;
1070     const double cy = state[Y] + std::cos(phi) / physical_omega;
1071     return square(cx - fit_center_x) + square(cy - fit_center_y);
1072   };
1073 
1074   const double phi_keep = normalize_phi(state[Phi]);
1075   const double phi_flip = normalize_phi(state[Phi] + kPi);
1076   if (center_distance2(phi_flip) < center_distance2(phi_keep))
1077   {
1078     state[Phi] = phi_flip;
1079     state[TanLambda] *= -1.0;
1080   }
1081   else
1082   {
1083     state[Phi] = phi_keep;
1084   }
1085   state[QOverPt] = physical_qop_t;
1086   return state;
1087 }
1088 
1089 std::array<double, TpcTrackKalmanFitter::StateDim> TpcTrackKalmanFitter::propagate_state(
1090     const std::array<double, StateDim> &state,
1091     const double ds_cm,
1092     const TpcKalmanConfig &config,
1093     const double mass_gev)
1094 {
1095   const StateVector input = to_eigen(state);
1096   if (config.magnetic_field == nullptr && config.analytic_uniform_propagation)
1097   {
1098     return to_array(propagate_uniform_bz(input, ds_cm, config.bfield_t, config, mass_gev));
1099   }
1100   return to_array(propagate_rkn4(input, ds_cm, config, mass_gev));
1101 }
1102 
1103 std::pair<double, double> TpcTrackKalmanFitter::dca_to_vertex(const TpcKalmanResult &fit,
1104                                                               const TpcTrackVec3 &vertex,
1105                                                               const TpcKalmanConfig *input_config)
1106 {
1107   if (!fit.success || fit.states_smoothed.empty())
1108   {
1109     return {std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN()};
1110   }
1111 
1112   const auto state_array = propagation_state(fit, vertex);
1113   const StateVector state = to_eigen(state_array);
1114   const double omega = omega_from_state(state, fit.bfield_t);
1115   if (std::abs(omega) < 1.0e-10)
1116   {
1117     return TpcTrackHelixFitter::line_dca_to_vertex(state_position(state_array),
1118                                                    state_momentum(state_array),
1119                                                    vertex);
1120   }
1121 
1122   const double radius = 1.0 / omega;
1123   const double center_x = state(X) - std::sin(state(Phi)) / omega;
1124   const double center_y = state(Y) + std::cos(state(Phi)) / omega;
1125   const double vx = vertex.x - center_x;
1126   const double vy = vertex.y - center_y;
1127   const double distance_to_center = std::sqrt(vx * vx + vy * vy);
1128   double theta_closest = std::atan2(state(Y) - center_y, state(X) - center_x);
1129   double dca_xy = std::abs(radius);
1130   if (distance_to_center > 0.0)
1131   {
1132     const double closest_x = center_x + std::abs(radius) * vx / distance_to_center;
1133     const double closest_y = center_y + std::abs(radius) * vy / distance_to_center;
1134     const double theta0 = std::atan2(state(Y) - center_y, state(X) - center_x);
1135     const double theta_raw = std::atan2(closest_y - center_y, closest_x - center_x);
1136     theta_closest = theta0 + normalize_phi(theta_raw - theta0);
1137     dca_xy = std::abs(distance_to_center - std::abs(radius));
1138   }
1139 
1140   const double theta0 = std::atan2(state(Y) - center_y, state(X) - center_x);
1141   const double dtheta = normalize_phi(theta_closest - theta0);
1142   const double s_cm = dtheta / omega;
1143   TpcKalmanConfig config = input_config != nullptr ? *input_config : TpcKalmanConfig{};
1144   config.bfield_t = fit.bfield_t;
1145   config.magnetic_field = fit.magnetic_field;
1146   config.analytic_uniform_propagation = fit.analytic_uniform_propagation;
1147   const auto closest = propagate_state(state_array, s_cm, config, fit.mass_gev);
1148   return {dca_xy, std::abs(closest[Z] - vertex.z)};
1149 }