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 }
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
0899
0900
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 & )
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
1035
1036
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
1057
1058
1059
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 }