From b1acf6e2c5c969cd7143e65c6a82a6bd69693c23 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Mon, 9 Jun 2025 15:17:47 -0400 Subject: [PATCH 01/75] OpenKF for our case is building and predicting Our prediction step matches the results of the OpenKF example and is passing that portion of the test. It is failing the update step, but this might not be a problem as we are not adding the measurement noise to the augmented state vector, as the OpenKF example does. Instead we are treating the measurement noise analytically. This might cause the small differences in the results, but I need to run through the math to confirm that. --- .../OpenKF/kalman_filter/AtTrackFitterUKF.h | 374 ++++++++++++++++++ .../kalman_filter/AtTrackFitterUKFTest.cxx | 206 ++++++++++ AtReconstruction/AtFitter/OpenKF/kf_util.h | 8 +- AtReconstruction/CMakeLists.txt | 2 + 4 files changed, 587 insertions(+), 3 deletions(-) create mode 100644 AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h create mode 100644 AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h new file mode 100644 index 000000000..70ae0d9dd --- /dev/null +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h @@ -0,0 +1,374 @@ +/// +/// Copyright 2022 Mohanad Youssef (Al-khwarizmi) +/// +/// Use of this source code is governed by an GPL-3.0 - style +/// license that can be found in the LICENSE file or at +/// https://opensource.org/licenses/GPL-3.0 +/// +/// @author Mohanad Youssef +/// @file unscented_kalman_filter.h +/// + +#ifndef UNSCENTED_KALMAN_FILTER_LIB_H +#define UNSCENTED_KALMAN_FILTER_LIB_H + +#include "kalman_filter.h" +#include "kf_util.h" +#include "unscented_kalman_filter.h" + +namespace kf { + +/// @brief Class for fitting tracks using the Unscented Kalman Filter (UKF) algorithm. +/// @tparam DIM_X Dimension of the state vector. +/// @tparam DIM_Z Dimension of the measurement vector. +/// @tparam DIM_V Dimension of the process noise vector. +/// @tparam DIM_N Dimension of the measurement noise vector. +template +class TrackFitterUKF : public KalmanFilter { +public: + // Augmented state vector is just the process noise and state vector. The measurement noise is not included as that + // is independent of the propagation and measurement model and just adds linearly. + static constexpr int32_t DIM_A{DIM_X + DIM_V}; ///< @brief Augmented state dimension + static constexpr int32_t SIGMA_DIM_A{2 * DIM_A + 1}; ///< @brief Sigma points dimension for augmented state + float32_t m_kappa{0}; ///< @brief Kappa parameter for finding sigma points + + // Add variables to track the covariances of the process and measurement noise. + Matrix m_matQ; // @brief Process noise covariance matrix + Matrix m_matR; // @brief Measurement noise covariance matrix + + Matrix m_matSigmaXa{Matrix::Zero()}; ///< @brief Sigma points matrix + + TrackFitterUKF() + : KalmanFilter(), m_kappa(3 - DIM_A), m_matQ(Matrix::Zero()), + m_matR(Matrix::Zero()) + { + // 1. calculate weights + updateWeights(); + } + + ~TrackFitterUKF() {} + + void setKappa(float32_t kappa) + { + m_kappa = kappa; // Set the kappa parameter for sigma point calculation + updateWeights(); // Update the weights based on the new kappa value + } + + // This code uses two different conventions for managing noise. + // The state vector noise is set in the updateAugmentedStateAndCovariance() method, while + // the noise vectors for the process and measurement models are set in the setCovarianceQ() and + // setCovarianceR() methods. This is an odd choice. We will be moving everything into a common + // structure where updateAugmentedStateAndCovariance() handle all covariance updates that are actually + // part of the augmented state vector. + + /// + /// @brief adding process noise covariance Q to the augmented state covariance + /// matPa in the middle element of the diagonal. + /// + void setCovarianceQ(const Matrix &matQ) + { + m_matQ = matQ; // Store the process noise covariance matrix + } + + /// + /// @brief adding measurement noise covariance R to the augmented state + /// covariance matPa in the third element of the diagonal. + /// + void setCovarianceR(const Matrix &matR) + { + m_matR = matR; // Store the measurement noise covariance matrix + } + + /// Add state vector (m_vecX) to the augment state vector (m_vecXa) and also + /// add covariance matrix (m_matP) to the augment covariance (m_matPa). + void updateAugWithState() + { + // Copy state vector to augmented state vector + for (int32_t i{0}; i < DIM_X; ++i) { + m_vecXa[i] = m_vecX[i]; + } + + // Copy state covariance matrix to augmented covariance matrix + for (int32_t i{0}; i < DIM_X; ++i) { + for (int32_t j{0}; j < DIM_X; ++j) { + m_matPa(i, j) = m_matP(i, j); + } + } + } + + std::array calculateProcessNoiseMean() + { + // Calculate the expectation value of the process noise using the current value of the state vector m_vecX + std::array processNoiseMean{0}; + + // TODO: Set the mean energy loss based on the momentum and particle type. Probably best to track stopping power? + return processNoiseMean; + } + + Matrix calculateProcessNoiseCovariance() + { + // Calculate the process noise covariance matrix + Matrix matQ{Matrix::Zero()}; + + // TODO: Set the process noise covariance for angular straggle and energy loss. + return matQ; + } + + void updateAugWithProcessNoise() + { + auto processNoiseMean = calculateProcessNoiseMean(); + m_matQ; // = calculateProcessNoiseCovariance(); + + // Add the mean process noise to the augmented state vector + for (int32_t i{0}; i < DIM_V; ++i) { + m_vecXa[DIM_X + i] = processNoiseMean[i]; + } + + // Add process noise covariance to the augmented covariance matrix + const int32_t S_IDX{DIM_X}; + const int32_t L_IDX{S_IDX + DIM_V}; + + for (int32_t i{S_IDX}; i < L_IDX; ++i) { + for (int32_t j{S_IDX}; j < L_IDX; ++j) { + m_matPa(i, j) = m_matQ(i - S_IDX, j - S_IDX); + } + } + } + + /// + /// @brief update the augmented state vector and covariance matrix + /// This functions fully updates the augmented state vector (m_vecXa) and covariance matrix (m_matPa) + /// by setting both the state vector and process noise components. + /// + void updateAugmentedStateAndCovariance() + { + updateAugWithState(); + updateAugWithProcessNoise(); + } + + /// + /// @brief state prediction step of the unscented Kalman filter (UKF). + /// @param predictionModelFunc callback to the prediction/process model + /// function + /// + template + void predictUKF(PredictionModelCallback predictionModelFunc) + { + setKappa(3 - DIM_A); // Set kappa for the augmented state vector and update the weights. + updateAugmentedStateAndCovariance(); + + // Calculate the sigma points for the augmented state vector and save in a matrix where each column is a sigma + // point. + m_matSigmaXa = calculateSigmaPoints(m_vecXa, m_matPa); + + // Pull out the sigma points for the state vector and process noise in two different matrices. + Matrix sigmaXx{m_matSigmaXa.block(0, 0, DIM_X, SIGMA_DIM_A)}; // Sigma points for state vector + Matrix sigmaXv{ + m_matSigmaXa.block(DIM_X, 0, DIM_V, SIGMA_DIM_A)}; // Sigma points for process noise + + // Get each sigma point, apply the prediction model function, and store the results + // back into the sigmaXx matrix (safe since each sigma point is independent). + for (int32_t i{0}; i < SIGMA_DIM_A; ++i) { + const Vector sigmaXxi{util::getColumnAt(i, sigmaXx)}; + const Vector sigmaXvi{util::getColumnAt(i, sigmaXv)}; + + const Vector Yi{predictionModelFunc(sigmaXxi, sigmaXvi)}; // y = f(x) + + // Copy the predicted state vector back into the sigmaXx matrix. + util::copyToColumn(i, sigmaXx, Yi); + // Copy the predicted state vector back into the augmented state vector (for use in future functions). + util::copyToColumn(i, m_matSigmaXa, Yi); + } + + // Calculate the weighted mean and covariance of the sigma points for the state vector. + // This will be the new state vector and covariance matrix. + calculateWeightedMeanAndCovariance(sigmaXx, m_vecX, m_matP); + } + + /// + /// @brief measurement correction step of the unscented Kalman filter (UKF). + /// @param measurementModelFunc callback to the measurement model function + /// @param vecZ actual measurement vector. + /// + template + void correctUKF(MeasurementModelCallback measurementModelFunc, const Vector &vecZ) + { + // The state vector used here is an unaugmented state vector (m_vecX) and the covariance matrix is + // an unaugmented covariance matrix (m_matP). This is because we are assuming the measurement noise + // is independent of the state vector and process noise, so we can just use the unaugmented state vector + // and covariance matrix for the measurement correction step, then add the measurement noise covariance. + + // Pull out the sigma points for the state vector after prediction. + Matrix sigmaXx{m_matSigmaXa.block(0, 0, DIM_X, SIGMA_DIM_A)}; // Sigma points for state vector + + // Get each sigma point, apply the prediction model function, and store the results + // in the sigmaZ matrix. + Matrix sigmaZ; + for (int32_t i{0}; i < SIGMA_DIM_A; ++i) { + const Vector sigmaXxi{util::getColumnAt(i, sigmaXx)}; + + const Vector Zi{measurementModelFunc(sigmaXxi)}; // z = h(x) + + util::copyToColumn(i, sigmaZ, Zi); + } + + // calculate the mean measurement vector and covariance matrix + // from the sigma points. + Vector vecZhat; + Matrix matPzz; + calculateWeightedMeanAndCovariance(sigmaZ, vecZhat, matPzz); + + // Add in the measurement noise covariance matrix to the measurement covariance matrix. + matPzz += m_matR; // Add measurement noise covariance + + // TODO: calculate cross correlation + const Matrix matPxz{calculateCrossCorrelation(sigmaXx, m_vecX, sigmaZ, vecZhat)}; + + // kalman gain + const Matrix matK{matPxz * matPzz.inverse()}; + + m_vecX += matK * (vecZ - vecZhat); + m_matP -= matK * matPzz * matK.transpose(); + } + +private: + using KalmanFilter::m_vecX; // from Base KalmanFilter class + using KalmanFilter::m_matP; // from Base KalmanFilter class + + float32_t m_weight0; /// @brief unscented transform weight 0 for mean + float32_t m_weighti; /// @brief unscented transform weight i for none mean samples + + Vector m_vecXa{Vector::Zero()}; /// @brief augmented state vector (incl. process + /// and measurement noise means) + Matrix m_matPa{Matrix::Zero()}; /// @brief augmented state covariance (incl. + /// process and measurement noise covariances) + + /// + /// @brief algorithm to calculate the weights used to draw the sigma points + /// + void updateWeights() + { + static_assert(DIM_A > 0, "DIM_A is Zero which leads to numerical issue."); + + const float32_t denoTerm{m_kappa + static_cast(DIM_A)}; + + m_weight0 = m_kappa / denoTerm; + m_weighti = 0.5F / denoTerm; + } + + /// + /// @brief algorithm to calculate the deterministic sigma points for + /// the unscented transformation + /// + /// @param vecX mean of the normally distributed state + /// @param matPxx covariance of the normally distributed state + /// @param STATE_DIM dimension of the vector used to calculate the sigma points + /// @param SIGMA_DIM number of sigma points required (default is 2 * STATE_DIM + 1) + /// @return matrix of sigma points where each column is a sigma point + /// + template + Matrix + calculateSigmaPoints(const Vector &vecXa, const Matrix &matPa) + { + setKappa(3 - STATE_DIM); // Set kappa for the sigma points calculation + const float32_t scalarMultiplier{std::sqrt(STATE_DIM + m_kappa)}; // sqrt(n + \kappa) + + // cholesky factorization to get matrix Pxx square-root + Eigen::LLT> lltOfPa(matPa); + Matrix matSa{lltOfPa.matrixL()}; // sqrt(P_{a}) + + matSa *= scalarMultiplier; // sqrt( (n + \kappa) * P_{a} ) + + Matrix sigmaXa; + + // X_0 = \bar{xa} + util::copyToColumn(0, sigmaXa, vecXa); + + for (int32_t i{0}; i < STATE_DIM; ++i) { + const int32_t IDX_1{i + 1}; + const int32_t IDX_2{i + STATE_DIM + 1}; + + util::copyToColumn(IDX_1, sigmaXa, vecXa); + util::copyToColumn(IDX_2, sigmaXa, vecXa); + + const Vector vecShiftTerm{util::getColumnAt(i, matSa)}; + + util::addColumnFrom(IDX_1, sigmaXa, vecShiftTerm); // X_i^a = \bar{xa} + sqrt( (n^a + + // \kappa) * P^{a} ) + util::subColumnFrom(IDX_2, sigmaXa, vecShiftTerm); // X_{i+n}^a = \bar{xa} - sqrt( (n^a + + // \kappa) * P^{a} ) + } + + return sigmaXa; + } + + /// + /// @brief calculate the weighted mean and covariance given a set of sigma + /// points + /// @param[in] sigmaX matrix of (probably posterior) sigma points where each column contain single + /// sigma point. + /// @param[out] vecX output weighted mean of the sigma points + /// @param[out] matPxx output weighted covariance of the sigma points + /// + template + void calculateWeightedMeanAndCovariance(const Matrix &sigmaX, Vector &vecX, + Matrix &matPxx) + { + // 1. calculate mean of the sigma points + vecX = m_weight0 * util::getColumnAt(0, sigmaX); + for (int32_t i{1}; i < SIGMA_DIM; ++i) { + vecX += m_weighti * util::getColumnAt(i, sigmaX); // y += W[0, i] Y[:, i] + } + + // 2. calculate covariance: P_{yy} = \sum_{i_0}^{2n} W[0, i] (Y[:, i] - + // \bar{y}) (Y[:, i] - \bar{y})^T + Vector devXi{util::getColumnAt(0, sigmaX) - vecX}; // Y[:, 0] - \bar{ y } + + matPxx = m_weight0 * devXi * devXi.transpose(); // P_0 = W[0, 0] (Y[:, 0] - \bar{y}) (Y[:, 0] - + // \bar{y})^T + + for (int32_t i{1}; i < SIGMA_DIM; ++i) { + devXi = util::getColumnAt(i, sigmaX) - vecX; // Y[:, i] - \bar{y} + + const Matrix Pi{m_weighti * devXi * devXi.transpose()}; // P_i = W[0, i] (Y[:, i] - + // \bar{y}) (Y[:, i] - \bar{y})^T + + matPxx += Pi; // y += W[0, i] (Y[:, i] - \bar{y}) (Y[:, i] - \bar{y})^T + } + } + + /// + /// @brief calculate the cross-correlation given two sets sigma points X and Y + /// and their means x and y + /// @param sigmaX first matrix of sigma points where each column contain + /// single sigma point + /// @param vecX mean of the first set of sigma points + /// @param sigmaY second matrix of sigma points where each column contain + /// single sigma point + /// @param vecY mean of the second set of sigma points + /// @return matPxy, the cross-correlation matrix + /// + template + Matrix calculateCrossCorrelation(const Matrix &sigmaX, const Vector &vecX, + const Matrix &sigmaY, const Vector &vecY) + { + Vector devXi{util::getColumnAt(0, sigmaX) - vecX}; // X[:, 0] - \bar{ x } + Vector devYi{util::getColumnAt(0, sigmaY) - vecY}; // Y[:, 0] - \bar{ y } + + // P_0 = W[0, 0] (X[:, 0] - \bar{x}) (Y[:, 0] - \bar{y})^T + Matrix matPxy{m_weight0 * (devXi * devYi.transpose())}; + + for (int32_t i{1}; i < SIGMA_DIM; ++i) { + devXi = util::getColumnAt(i, sigmaX) - vecX; // X[:, i] - \bar{x} + devYi = util::getColumnAt(i, sigmaY) - vecY; // Y[:, i] - \bar{y} + + matPxy += m_weighti * (devXi * devYi.transpose()); // y += W[0, i] (Y[:, i] - + // \bar{y}) (Y[:, i] - \bar{y})^T + } + + return matPxy; + } +}; +} // namespace kf + +#endif // UNSCENTED_KALMAN_FILTER_LIB_H diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx new file mode 100644 index 000000000..f4e018c16 --- /dev/null +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx @@ -0,0 +1,206 @@ +#include "kalman_filter/AtTrackFitterUKF.h" + +#include "gtest/gtest.h" + +class AtTrackFitterUKFTest : public testing::Test { +public: + virtual void SetUp() override {} + virtual void TearDown() override {} + + static constexpr float FLOAT_EPSILON{0.001F}; + + static constexpr size_t DIM_X{4}; + static constexpr size_t DIM_V{4}; + static constexpr size_t DIM_Z{2}; + static constexpr size_t DIM_N{2}; + + kf::TrackFitterUKF m_ukf; + + /// @brief to propagate the state vector using the process model + /// @param x state vector + /// @param v process noise vector + /// @return propagated (unaugmented) state vector + static kf::Vector funcF(const kf::Vector &x, const kf::Vector &v) + { + kf::Vector y; + y[0] = x[0] + x[2] + v[0]; + y[1] = x[1] + x[3] + v[1]; + y[2] = x[2] + v[2]; + y[3] = x[3] + v[3]; + return y; + } + + /// @brief to apply the measurement model to the state vector + /// @param x the state vector of the system + /// @return the measurement vector + static kf::Vector funcH(const kf::Vector &x) + { + kf::Vector y; + + kf::float32_t px{x[0]}; + kf::float32_t py{x[1]}; + + y[0] = std::sqrt((px * px) + (py * py)); + y[1] = std::atan(py / (px + std::numeric_limits::epsilon())); + return y; + } +}; + +TEST_F(AtTrackFitterUKFTest, test_UKF_Prediction) +{ + kf::Vector x; + x << 2.0F, 1.0F, 0.0F, 0.0F; + + kf::Matrix P; + P << 0.01F, 0.0F, 0.0F, 0.0F, 0.0F, 0.01F, 0.0F, 0.0F, 0.0F, 0.0F, 0.05F, 0.0F, 0.0F, 0.0F, 0.0F, 0.05F; + + kf::Matrix Q; + Q << 0.05F, 0.0F, 0.0F, 0.0F, 0.0F, 0.05F, 0.0F, 0.0F, 0.0F, 0.0F, 0.1F, 0.0F, 0.0F, 0.0F, 0.0F, 0.1F; + + kf::Matrix R; + R << 0.01F, 0.0F, 0.0F, 0.01F; + + kf::Vector z; + z << 2.5F, 0.05F; + + m_ukf.vecX() = x; + m_ukf.matP() = P; + + m_ukf.setCovarianceQ(Q); + m_ukf.setCovarianceR(R); + + m_ukf.predictUKF(funcF); + + // Expectation from the python results: + // ===================================== + // x = + // [2.0 1.0 0.0 0.0] + // P = + // [[0.11 0.00 0.05 0.00] + // [0.00 0.11 0.00 0.05] + // [0.05 0.00 0.15 0.00] + // [0.00 0.05 0.00 0.15]] + + ASSERT_NEAR(m_ukf.vecX()[0], 2.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[1], 1.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[2], 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[3], 0.0F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(0, 0), 0.11F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 1), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 2), 0.05F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 3), 0.0F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(1, 0), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 1), 0.11F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 2), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 3), 0.05F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(2, 0), 0.05F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 1), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 2), 0.15F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 3), 0.0F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(3, 0), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 1), 0.05F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 2), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 3), 0.15F, FLOAT_EPSILON); +} + +TEST_F(AtTrackFitterUKFTest, test_UKF_PredictionAndCorrection) +{ + kf::Vector x; + x << 2.0F, 1.0F, 0.0F, 0.0F; + + kf::Matrix P; + P << 0.01F, 0.0F, 0.0F, 0.0F, 0.0F, 0.01F, 0.0F, 0.0F, 0.0F, 0.0F, 0.05F, 0.0F, 0.0F, 0.0F, 0.0F, 0.05F; + + kf::Matrix Q; + Q << 0.05F, 0.0F, 0.0F, 0.0F, 0.0F, 0.05F, 0.0F, 0.0F, 0.0F, 0.0F, 0.1F, 0.0F, 0.0F, 0.0F, 0.0F, 0.1F; + + kf::Matrix R; + R << 0.01F, 0.0F, 0.0F, 0.01F; + + kf::Vector z; + z << 2.5F, 0.05F; + + m_ukf.vecX() = x; + m_ukf.matP() = P; + + m_ukf.setCovarianceQ(Q); + m_ukf.setCovarianceR(R); + + m_ukf.predictUKF(funcF); + + // Expectation from the python results: + // ===================================== + // x = + // [2.0 1.0 0.0 0.0] + // P = + // [[0.11 0.00 0.05 0.00] + // [0.00 0.11 0.00 0.05] + // [0.05 0.00 0.15 0.00] + // [0.00 0.05 0.00 0.15]] + + ASSERT_NEAR(m_ukf.vecX()[0], 2.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[1], 1.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[2], 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[3], 0.0F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(0, 0), 0.11F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 1), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 2), 0.05F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 3), 0.0F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(1, 0), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 1), 0.11F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 2), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 3), 0.05F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(2, 0), 0.05F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 1), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 2), 0.15F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 3), 0.0F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(3, 0), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 1), 0.05F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 2), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 3), 0.15F, FLOAT_EPSILON); + + m_ukf.correctUKF(funcH, z); + + // Expectations from the python results: + // ====================================== + // x = + // [ 2.554 0.356 0.252 -0.293] + // P = + // [[ 0.01 -0.001 0.005 -0. ] + // [-0.001 0.01 - 0. 0.005 ] + // [ 0.005 - 0. 0.129 - 0. ] + // [-0. 0.005 - 0. 0.129]] + + ASSERT_NEAR(m_ukf.vecX()[0], 2.554F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[1], 0.356F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[2], 0.252F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[3], -0.293F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(0, 0), 0.01F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 1), -0.001F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 2), 0.005F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 3), 0.0F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(1, 0), -0.001F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 1), 0.01F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 2), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 3), 0.005F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(2, 0), 0.005F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 1), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 2), 0.129F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 3), 0.0F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(3, 0), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 1), 0.005F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 2), 0.0F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 3), 0.129F, FLOAT_EPSILON); +} diff --git a/AtReconstruction/AtFitter/OpenKF/kf_util.h b/AtReconstruction/AtFitter/OpenKF/kf_util.h index 4a01d3c19..311ed25eb 100644 --- a/AtReconstruction/AtFitter/OpenKF/kf_util.h +++ b/AtReconstruction/AtFitter/OpenKF/kf_util.h @@ -18,10 +18,12 @@ namespace kf { namespace util { -template -void copyToColumn(const int32_t colIdx, Matrix &lhsSigmaX, const Vector &rhsVecX) +template +void copyToColumn(const int32_t colIdx, Matrix &lhsSigmaX, const Vector &rhsVecX) { - for (int32_t i{0}; i < ROWS; ++i) { // rows + assert(colIdx < COLS); // assert if colIdx is out of boundary + assert(ROWS_COPY <= ROWS); // assert if rhsVecX is larger than lhsSigmaX + for (int32_t i{0}; i < ROWS_COPY; ++i) { // rows lhsSigmaX(i, colIdx) = rhsVecX[i]; } } diff --git a/AtReconstruction/CMakeLists.txt b/AtReconstruction/CMakeLists.txt index 3b5eac823..bb3024824 100755 --- a/AtReconstruction/CMakeLists.txt +++ b/AtReconstruction/CMakeLists.txt @@ -120,6 +120,7 @@ if(TARGET Eigen3::Eigen) set(SRCS ${SRCS} AtPatternRecognition/triplclust/src/orthogonallsq.cxx AtFitter/AtOpenKFTest.cxx + ) message(STATUS "Current sources: ${SRCS}") @@ -138,6 +139,7 @@ if(TARGET Eigen3::Eigen) set(TEST_SRCS_OPENKF AtFitter/OpenKF/kalman_filter/kalman_filter_test.cxx AtFitter/OpenKF/kalman_filter/unscented_kalman_filter_test.cxx + AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx ) attpcroot_generate_tests(OpenKFTests From c507853decd72bc9206d579f4b7f8999c596b782 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Mon, 9 Jun 2025 16:33:09 -0400 Subject: [PATCH 02/75] Implement update portion of UKF test --- .../OpenKF/kalman_filter/AtTrackFitterUKF.h | 26 +++++++ .../kalman_filter/AtTrackFitterUKFTest.cxx | 69 +++++++++++-------- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h index 70ae0d9dd..010518a3d 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h @@ -16,6 +16,8 @@ #include "kf_util.h" #include "unscented_kalman_filter.h" +#include + namespace kf { /// @brief Class for fitting tracks using the Unscented Kalman Filter (UKF) algorithm. @@ -212,20 +214,44 @@ class TrackFitterUKF : public KalmanFilter { util::copyToColumn(i, sigmaZ, Zi); } + std::cout << "Sigma Points for Measurement:" << std::endl; + // Print sigmaZ in CSV format + for (int32_t row = 0; row < DIM_Z; ++row) { + for (int32_t col = 0; col < SIGMA_DIM_A; ++col) { + std::cout << sigmaZ(row, col); + if (col < SIGMA_DIM_A - 1) + std::cout << ","; + } + std::cout << std::endl; + } + // calculate the mean measurement vector and covariance matrix // from the sigma points. Vector vecZhat; Matrix matPzz; calculateWeightedMeanAndCovariance(sigmaZ, vecZhat, matPzz); + std::cout << "Mean Measurement Vector (vecZhat):" << std::endl; + std::cout << vecZhat.transpose() << std::endl; + std::cout << "Measurement Covariance Matrix (matPzz):" << std::endl; + std::cout << matPzz << std::endl; + // Add in the measurement noise covariance matrix to the measurement covariance matrix. matPzz += m_matR; // Add measurement noise covariance + std::cout << "S Matrix (matPzz):" << std::endl; + std::cout << matPzz << std::endl; + // TODO: calculate cross correlation const Matrix matPxz{calculateCrossCorrelation(sigmaXx, m_vecX, sigmaZ, vecZhat)}; + std::cout << "Cross Correlation Matrix (matPxz):" << std::endl; + std::cout << matPxz << std::endl; + // kalman gain const Matrix matK{matPxz * matPzz.inverse()}; + std::cout << "Kalman Gain (matK):" << std::endl; + std::cout << matK << std::endl; m_vecX += matK * (vecZ - vecZhat); m_matP -= matK * matPzz * matK.transpose(); diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx index f4e018c16..9a29320a2 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx @@ -167,40 +167,49 @@ TEST_F(AtTrackFitterUKFTest, test_UKF_PredictionAndCorrection) ASSERT_NEAR(m_ukf.matP()(3, 2), 0.0F, FLOAT_EPSILON); ASSERT_NEAR(m_ukf.matP()(3, 3), 0.15F, FLOAT_EPSILON); + std::cout << "Sigma points after prediction:\n"; + for (int32_t j{0}; j < kf::TrackFitterUKF::DIM_A; ++j) { + for (int32_t i{0}; i < kf::TrackFitterUKF::DIM_A * 2 + 1; ++i) { + + std::cout << m_ukf.m_matSigmaXa(j, i) << ","; + } + std::cout << std::endl; + } + m_ukf.correctUKF(funcH, z); // Expectations from the python results: // ====================================== // x = - // [ 2.554 0.356 0.252 -0.293] + // [ 2.4758845 0.53327217 0.21649734 -0.21214576] // P = - // [[ 0.01 -0.001 0.005 -0. ] - // [-0.001 0.01 - 0. 0.005 ] - // [ 0.005 - 0. 0.129 - 0. ] - // [-0. 0.005 - 0. 0.129]] - - ASSERT_NEAR(m_ukf.vecX()[0], 2.554F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.vecX()[1], 0.356F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.vecX()[2], 0.252F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.vecX()[3], -0.293F, FLOAT_EPSILON); - - ASSERT_NEAR(m_ukf.matP()(0, 0), 0.01F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(0, 1), -0.001F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(0, 2), 0.005F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(0, 3), 0.0F, FLOAT_EPSILON); - - ASSERT_NEAR(m_ukf.matP()(1, 0), -0.001F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(1, 1), 0.01F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(1, 2), 0.0F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(1, 3), 0.005F, FLOAT_EPSILON); - - ASSERT_NEAR(m_ukf.matP()(2, 0), 0.005F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(2, 1), 0.0F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(2, 2), 0.129F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(2, 3), 0.0F, FLOAT_EPSILON); - - ASSERT_NEAR(m_ukf.matP()(3, 0), 0.0F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(3, 1), 0.005F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(3, 2), 0.0F, FLOAT_EPSILON); - ASSERT_NEAR(m_ukf.matP()(3, 3), 0.129F, FLOAT_EPSILON); + // [[ 0.01433114 -0.01026142 0.00651178 -0.00465059] + // [-0.01026142 0.0295458 -0.0046378 0.01344241] + // [ 0.00651178 -0.0046378 0.13023154 -0.00210188] + // [-0.00465059 0.01344241 -0.00210188 0.1333886 ]] + + ASSERT_NEAR(m_ukf.vecX()[0], 2.4758845F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[1], 0.53327217F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[2], 0.21649734F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[3], -0.21214576F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(0, 0), 0.01433114F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 1), -0.01026142F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 2), 0.00651178F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(0, 3), -0.00465059F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(1, 0), -0.01026142F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 1), 0.0295458F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 2), -0.0046378F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(1, 3), 0.01344241F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(2, 0), 0.00651178F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 1), -0.0046378F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 2), 0.13023154F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(2, 3), -0.00210188F, FLOAT_EPSILON); + + ASSERT_NEAR(m_ukf.matP()(3, 0), -0.00465059F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 1), 0.01344241F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 2), -0.00210188F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.matP()(3, 3), 0.1333886F, FLOAT_EPSILON); } From f0f9ee273f0d84c109f0f89157fcc341d9bfbba3 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Mon, 9 Jun 2025 16:43:02 -0400 Subject: [PATCH 03/75] Remove debug statements --- .../OpenKF/kalman_filter/AtTrackFitterUKF.h | 30 ++----------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h index 010518a3d..9b3a8c0ae 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h @@ -16,8 +16,6 @@ #include "kf_util.h" #include "unscented_kalman_filter.h" -#include - namespace kf { /// @brief Class for fitting tracks using the Unscented Kalman Filter (UKF) algorithm. @@ -111,6 +109,7 @@ class TrackFitterUKF : public KalmanFilter { { // Calculate the process noise covariance matrix Matrix matQ{Matrix::Zero()}; + matQ = m_matQ; // Use the stored process noise covariance matrix // TODO: Set the process noise covariance for angular straggle and energy loss. return matQ; @@ -119,7 +118,7 @@ class TrackFitterUKF : public KalmanFilter { void updateAugWithProcessNoise() { auto processNoiseMean = calculateProcessNoiseMean(); - m_matQ; // = calculateProcessNoiseCovariance(); + m_matQ = calculateProcessNoiseCovariance(); // Add the mean process noise to the augmented state vector for (int32_t i{0}; i < DIM_V; ++i) { @@ -156,7 +155,6 @@ class TrackFitterUKF : public KalmanFilter { template void predictUKF(PredictionModelCallback predictionModelFunc) { - setKappa(3 - DIM_A); // Set kappa for the augmented state vector and update the weights. updateAugmentedStateAndCovariance(); // Calculate the sigma points for the augmented state vector and save in a matrix where each column is a sigma @@ -214,44 +212,20 @@ class TrackFitterUKF : public KalmanFilter { util::copyToColumn(i, sigmaZ, Zi); } - std::cout << "Sigma Points for Measurement:" << std::endl; - // Print sigmaZ in CSV format - for (int32_t row = 0; row < DIM_Z; ++row) { - for (int32_t col = 0; col < SIGMA_DIM_A; ++col) { - std::cout << sigmaZ(row, col); - if (col < SIGMA_DIM_A - 1) - std::cout << ","; - } - std::cout << std::endl; - } - // calculate the mean measurement vector and covariance matrix // from the sigma points. Vector vecZhat; Matrix matPzz; calculateWeightedMeanAndCovariance(sigmaZ, vecZhat, matPzz); - std::cout << "Mean Measurement Vector (vecZhat):" << std::endl; - std::cout << vecZhat.transpose() << std::endl; - std::cout << "Measurement Covariance Matrix (matPzz):" << std::endl; - std::cout << matPzz << std::endl; - // Add in the measurement noise covariance matrix to the measurement covariance matrix. matPzz += m_matR; // Add measurement noise covariance - std::cout << "S Matrix (matPzz):" << std::endl; - std::cout << matPzz << std::endl; - // TODO: calculate cross correlation const Matrix matPxz{calculateCrossCorrelation(sigmaXx, m_vecX, sigmaZ, vecZhat)}; - std::cout << "Cross Correlation Matrix (matPxz):" << std::endl; - std::cout << matPxz << std::endl; - // kalman gain const Matrix matK{matPxz * matPzz.inverse()}; - std::cout << "Kalman Gain (matK):" << std::endl; - std::cout << matK << std::endl; m_vecX += matK * (vecZ - vecZhat); m_matP -= matK * matPzz * matK.transpose(); From 090951930cbb3de3ff27c6d2deea80ee9a9b5226 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Mon, 9 Jun 2025 17:00:14 -0400 Subject: [PATCH 04/75] Add check for positive definite cov matrix Removed some unnecessary updates to kappa Fixed missleading comment --- .../AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h index 9b3a8c0ae..a29c1a51b 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h @@ -71,8 +71,7 @@ class TrackFitterUKF : public KalmanFilter { } /// - /// @brief adding measurement noise covariance R to the augmented state - /// covariance matPa in the third element of the diagonal. + /// @brief set the measurement noise covariance R to be used in the update step /// void setCovarianceR(const Matrix &matR) { @@ -270,11 +269,13 @@ class TrackFitterUKF : public KalmanFilter { Matrix calculateSigmaPoints(const Vector &vecXa, const Matrix &matPa) { - setKappa(3 - STATE_DIM); // Set kappa for the sigma points calculation const float32_t scalarMultiplier{std::sqrt(STATE_DIM + m_kappa)}; // sqrt(n + \kappa) // cholesky factorization to get matrix Pxx square-root Eigen::LLT> lltOfPa(matPa); + if (lltOfPa.info() != Eigen::Success) { + throw std::runtime_error("Cholesky decomposition failed, matrix is not positive definite."); + } Matrix matSa{lltOfPa.matrixL()}; // sqrt(P_{a}) matSa *= scalarMultiplier; // sqrt( (n + \kappa) * P_{a} ) From 87587b2f927711d630678372489455b490ba8059 Mon Sep 17 00:00:00 2001 From: anthoak13 Date: Mon, 9 Jun 2025 20:58:05 -0400 Subject: [PATCH 05/75] Add physics test cases to be filled --- .../{AtTrackFitterUKF.h => TrackFitterUKF.h} | 0 ...tterUKFTest.cxx => TrackFitterUKFTest.cxx} | 81 ++++++++++++++++--- AtReconstruction/CMakeLists.txt | 2 +- 3 files changed, 69 insertions(+), 14 deletions(-) rename AtReconstruction/AtFitter/OpenKF/kalman_filter/{AtTrackFitterUKF.h => TrackFitterUKF.h} (100%) rename AtReconstruction/AtFitter/OpenKF/kalman_filter/{AtTrackFitterUKFTest.cxx => TrackFitterUKFTest.cxx} (77%) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h similarity index 100% rename from AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKF.h rename to AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx similarity index 77% rename from AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx rename to AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index 9a29320a2..5767d80e0 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -1,8 +1,8 @@ -#include "kalman_filter/AtTrackFitterUKF.h" +#include "kalman_filter/TrackFitterUKF.h" #include "gtest/gtest.h" -class AtTrackFitterUKFTest : public testing::Test { +class TrackFitterUKFExampleTest : public testing::Test { public: virtual void SetUp() override {} virtual void TearDown() override {} @@ -46,7 +46,7 @@ class AtTrackFitterUKFTest : public testing::Test { } }; -TEST_F(AtTrackFitterUKFTest, test_UKF_Prediction) +TEST_F(TrackFitterUKFExampleTest, Prediction) { kf::Vector x; x << 2.0F, 1.0F, 0.0F, 0.0F; @@ -107,7 +107,7 @@ TEST_F(AtTrackFitterUKFTest, test_UKF_Prediction) ASSERT_NEAR(m_ukf.matP()(3, 3), 0.15F, FLOAT_EPSILON); } -TEST_F(AtTrackFitterUKFTest, test_UKF_PredictionAndCorrection) +TEST_F(TrackFitterUKFExampleTest, PredictionAndCorrection) { kf::Vector x; x << 2.0F, 1.0F, 0.0F, 0.0F; @@ -167,15 +167,6 @@ TEST_F(AtTrackFitterUKFTest, test_UKF_PredictionAndCorrection) ASSERT_NEAR(m_ukf.matP()(3, 2), 0.0F, FLOAT_EPSILON); ASSERT_NEAR(m_ukf.matP()(3, 3), 0.15F, FLOAT_EPSILON); - std::cout << "Sigma points after prediction:\n"; - for (int32_t j{0}; j < kf::TrackFitterUKF::DIM_A; ++j) { - for (int32_t i{0}; i < kf::TrackFitterUKF::DIM_A * 2 + 1; ++i) { - - std::cout << m_ukf.m_matSigmaXa(j, i) << ","; - } - std::cout << std::endl; - } - m_ukf.correctUKF(funcH, z); // Expectations from the python results: @@ -213,3 +204,67 @@ TEST_F(AtTrackFitterUKFTest, test_UKF_PredictionAndCorrection) ASSERT_NEAR(m_ukf.matP()(3, 2), -0.00210188F, FLOAT_EPSILON); ASSERT_NEAR(m_ukf.matP()(3, 3), 0.1333886F, FLOAT_EPSILON); } + +class TrackFitterUKFPhysicsTest : public testing::Test { +public: + virtual void SetUp() override {} + virtual void TearDown() override {} + + static constexpr float FLOAT_EPSILON{0.001F}; + + static constexpr size_t DIM_X{6}; + static constexpr size_t DIM_V{2}; + static constexpr size_t DIM_Z{3}; + static constexpr size_t DIM_N{3}; + + kf::TrackFitterUKF m_ukf; + + /// @brief to propagate the state vector using the process model + /// @param x state vector + /// @param v process noise vector + /// @return propagated (unaugmented) state vector + static kf::Vector funcF(const kf::Vector &x, const kf::Vector &v) + { + //TODO: This needs to be filled with an RK4 solver for the physics model + kf::Vector y{x}; + + // For now, we just return the state vector as is + return y; + } + + /// @brief to apply the measurement model to the state vector + /// @param x the state vector of the system + /// @return the measurement vector + static kf::Vector funcH(const kf::Vector &x) + { + kf::Vector y; + y[0] = x[0]; + y[1] = x[1]; + y[2] = x[2]; + + return y; + } +}; + +TEST_F(TrackFitterUKFPhysicsTest, PhysicsPrediction) +{ + // TODO: This needs to be filled with a proper physics test. + kf::Vector x; // Initial state vector + + kf::Matrix P; // Initial state vector covariance matrix + + // Note: process noise is defined in the UKF class, so we don't need to set it here. + + kf::Vector z; // Measurement vector to be used in the correction step + z << std::sqrt(5), std::atan2(1.f ,2.f); + + kf::Matrix R; // Covariance matrix for the measurement noise + + m_ukf.vecX() = x; + m_ukf.matP() = P; + + m_ukf.setCovarianceR(R); + m_ukf.predictUKF(funcF); + + +} diff --git a/AtReconstruction/CMakeLists.txt b/AtReconstruction/CMakeLists.txt index bb3024824..bc0de8a62 100755 --- a/AtReconstruction/CMakeLists.txt +++ b/AtReconstruction/CMakeLists.txt @@ -139,7 +139,7 @@ if(TARGET Eigen3::Eigen) set(TEST_SRCS_OPENKF AtFitter/OpenKF/kalman_filter/kalman_filter_test.cxx AtFitter/OpenKF/kalman_filter/unscented_kalman_filter_test.cxx - AtFitter/OpenKF/kalman_filter/AtTrackFitterUKFTest.cxx + AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx ) attpcroot_generate_tests(OpenKFTests From 6b8698bef83092a1a9fcfa0def11970d1d1edcb9 Mon Sep 17 00:00:00 2001 From: anthoak13 Date: Mon, 9 Jun 2025 21:00:40 -0400 Subject: [PATCH 06/75] Run clang format --- .../OpenKF/kalman_filter/TrackFitterUKFTest.cxx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index 5767d80e0..1751ea443 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -225,9 +225,9 @@ class TrackFitterUKFPhysicsTest : public testing::Test { /// @return propagated (unaugmented) state vector static kf::Vector funcF(const kf::Vector &x, const kf::Vector &v) { - //TODO: This needs to be filled with an RK4 solver for the physics model + // TODO: This needs to be filled with an RK4 solver for the physics model kf::Vector y{x}; - + // For now, we just return the state vector as is return y; } @@ -248,7 +248,7 @@ class TrackFitterUKFPhysicsTest : public testing::Test { TEST_F(TrackFitterUKFPhysicsTest, PhysicsPrediction) { - // TODO: This needs to be filled with a proper physics test. + // TODO: This needs to be filled with a proper physics test. kf::Vector x; // Initial state vector kf::Matrix P; // Initial state vector covariance matrix @@ -256,7 +256,7 @@ TEST_F(TrackFitterUKFPhysicsTest, PhysicsPrediction) // Note: process noise is defined in the UKF class, so we don't need to set it here. kf::Vector z; // Measurement vector to be used in the correction step - z << std::sqrt(5), std::atan2(1.f ,2.f); + z << std::sqrt(5), std::atan2(1.f, 2.f); kf::Matrix R; // Covariance matrix for the measurement noise @@ -265,6 +265,4 @@ TEST_F(TrackFitterUKFPhysicsTest, PhysicsPrediction) m_ukf.setCovarianceR(R); m_ukf.predictUKF(funcF); - - } From 9efdcfcdcb3e6936c1da551157ce6226c8f52bff Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 17 Jul 2025 15:55:37 +0200 Subject: [PATCH 07/75] Force is working and propogator with no fields is working --- .../AtFitter/OpenKF/kalman_filter/HinH.txt | 143 +++++++++ .../OpenKF/kalman_filter/TrackFitterUKF.h | 4 +- .../kalman_filter/TrackFitterUKFTest.cxx | 297 +++++++++++++++++- AtTools/AtELossTable.h | 2 + 4 files changed, 437 insertions(+), 9 deletions(-) create mode 100644 AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt b/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt new file mode 100644 index 000000000..b436b8c8c --- /dev/null +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt @@ -0,0 +1,143 @@ + ================================================================== + SRIM version ---> SRIM-2013.00 + Calc. date ---> julio 16. 2025 + ================================================================== + + Disk File Name = SRIM Outputs\Hydrogen in Hydrogen (gas).txt + + Ion = Hydrogen [1] . Mass = 1.008 amu + + Target Density = 4.1906E-05 g/cm3 = 2.5036E+19 atoms/cm3 + Target is a GAS + ======= Target Composition ======== + Atom Atom Atomic Mass + Name Numb Percent Percent + ---- ---- ------- ------- + H 1 100.00 100.00 + ==================================== + Bragg Correction = 0.00% + Stopping Units = MeV / (mg/cm2) + See bottom of Table for other Stopping units + + Ion dE/dx dE/dx Projected Longitudinal Lateral + Energy Elec. Nuclear Range Straggling Straggling + -------------- ---------- ---------- ---------- ---------- ---------- +999.999 eV 7.504E-01 1.692E-01 267.71 um 61.83 um 55.80 um + 1.10 keV 7.871E-01 1.600E-01 291.11 um 64.85 um 59.45 um + 1.20 keV 8.221E-01 1.519E-01 313.96 um 67.62 um 62.90 um + 1.30 keV 8.556E-01 1.447E-01 336.31 um 70.18 um 66.16 um + 1.40 keV 8.879E-01 1.383E-01 358.18 um 72.56 um 69.25 um + 1.50 keV 9.191E-01 1.325E-01 379.59 um 74.77 um 72.19 um + 1.60 keV 9.492E-01 1.272E-01 400.57 um 76.83 um 74.99 um + 1.70 keV 9.785E-01 1.224E-01 421.14 um 78.76 um 77.65 um + 1.80 keV 1.007E+00 1.180E-01 441.32 um 80.57 um 80.20 um + 2.00 keV 1.061E+00 1.101E-01 480.60 um 83.97 um 84.97 um + 2.25 keV 1.124E+00 1.019E-01 527.87 um 87.75 um 90.40 um + 2.50 keV 1.183E+00 9.491E-02 573.34 um 91.09 um 95.34 um + 2.75 keV 1.239E+00 8.894E-02 617.19 um 94.06 um 99.86 um + 3.00 keV 1.293E+00 8.377E-02 659.59 um 96.72 um 104.03 um + 3.25 keV 1.344E+00 7.923E-02 700.68 um 99.13 um 107.88 um + 3.50 keV 1.393E+00 7.521E-02 740.56 um 101.32 um 111.46 um + 3.75 keV 1.440E+00 7.162E-02 779.35 um 103.33 um 114.80 um + 4.00 keV 1.486E+00 6.840E-02 817.12 um 105.18 um 117.93 um + 4.50 keV 1.572E+00 6.285E-02 889.92 um 108.70 um 123.65 um + 5.00 keV 1.654E+00 5.821E-02 959.49 um 111.75 um 128.76 um + 5.50 keV 1.731E+00 5.428E-02 1.03 mm 114.44 um 133.36 um + 6.00 keV 1.804E+00 5.090E-02 1.09 mm 116.83 um 137.55 um + 6.50 keV 1.873E+00 4.795E-02 1.15 mm 118.97 um 141.38 um + 7.00 keV 1.939E+00 4.536E-02 1.21 mm 120.92 um 144.91 um + 8.00 keV 2.064E+00 4.101E-02 1.33 mm 124.82 um 151.22 um + 9.00 keV 2.178E+00 3.748E-02 1.44 mm 128.13 um 156.72 um + 10.00 keV 2.285E+00 3.457E-02 1.54 mm 131.01 um 161.59 um + 11.00 keV 2.384E+00 3.211E-02 1.64 mm 133.54 um 165.95 um + 12.00 keV 2.477E+00 3.001E-02 1.74 mm 135.79 um 169.89 um + 13.00 keV 2.564E+00 2.819E-02 1.83 mm 137.82 um 173.48 um + 14.00 keV 2.646E+00 2.660E-02 1.92 mm 139.67 um 176.77 um + 15.00 keV 2.723E+00 2.519E-02 2.01 mm 141.36 um 179.82 um + 16.00 keV 2.796E+00 2.393E-02 2.09 mm 142.92 um 182.65 um + 17.00 keV 2.864E+00 2.281E-02 2.17 mm 144.37 um 185.29 um + 18.00 keV 2.929E+00 2.179E-02 2.26 mm 145.72 um 187.77 um + 20.00 keV 3.048E+00 2.003E-02 2.41 mm 149.01 um 192.31 um + 22.50 keV 3.179E+00 1.822E-02 2.60 mm 153.11 um 197.34 um + 25.00 keV 3.292E+00 1.673E-02 2.79 mm 156.75 um 201.82 um + 27.50 keV 3.389E+00 1.549E-02 2.96 mm 160.04 um 205.87 um + 30.00 keV 3.473E+00 1.443E-02 3.14 mm 163.06 um 209.56 um + 32.50 keV 3.544E+00 1.351E-02 3.30 mm 165.87 um 212.98 um + 35.00 keV 3.604E+00 1.272E-02 3.47 mm 168.50 um 216.15 um + 37.50 keV 3.654E+00 1.201E-02 3.63 mm 170.98 um 219.13 um + 40.00 keV 3.695E+00 1.139E-02 3.79 mm 173.35 um 221.95 um + 45.00 keV 3.753E+00 1.033E-02 4.11 mm 180.64 um 227.18 um + 50.00 keV 3.784E+00 9.466E-03 4.43 mm 187.42 um 231.99 um + 55.00 keV 3.794E+00 8.743E-03 4.74 mm 193.85 um 236.49 um + 60.00 keV 3.785E+00 8.129E-03 5.06 mm 200.03 um 240.76 um + 65.00 keV 3.763E+00 7.601E-03 5.37 mm 206.04 um 244.84 um + 70.00 keV 3.730E+00 7.142E-03 5.69 mm 211.93 um 248.78 um + 80.00 keV 3.638E+00 6.382E-03 6.33 mm 232.69 um 256.39 um + 90.00 keV 3.526E+00 5.776E-03 7.00 mm 252.74 um 263.79 um + 100.00 keV 3.403E+00 5.282E-03 7.69 mm 272.49 um 271.10 um + 110.00 keV 3.276E+00 4.870E-03 8.40 mm 292.22 um 278.42 um + 120.00 keV 3.150E+00 4.522E-03 9.14 mm 312.12 um 285.84 um + 130.00 keV 3.026E+00 4.222E-03 9.91 mm 332.30 um 293.41 um + 140.00 keV 2.907E+00 3.963E-03 10.72 mm 352.84 um 301.18 um + 150.00 keV 2.794E+00 3.735E-03 11.55 mm 373.81 um 309.18 um + 160.00 keV 2.687E+00 3.533E-03 12.42 mm 395.24 um 317.44 um + 170.00 keV 2.587E+00 3.353E-03 13.33 mm 417.16 um 326.00 um + 180.00 keV 2.492E+00 3.192E-03 14.26 mm 439.58 um 334.87 um + 200.00 keV 2.320E+00 2.915E-03 16.25 mm 524.78 um 353.64 um + 225.00 keV 2.134E+00 2.632E-03 18.92 mm 651.26 um 379.16 um + 250.00 keV 1.975E+00 2.402E-03 21.83 mm 773.93 um 407.12 um + 275.00 keV 1.840E+00 2.211E-03 24.95 mm 895.52 um 437.62 um + 300.00 keV 1.722E+00 2.050E-03 28.30 mm 1.02 mm 470.70 um + 325.00 keV 1.620E+00 1.911E-03 31.87 mm 1.14 mm 506.38 um + 350.00 keV 1.530E+00 1.791E-03 35.65 mm 1.26 mm 544.65 um + 375.00 keV 1.451E+00 1.686E-03 39.65 mm 1.39 mm 585.49 um + 400.00 keV 1.381E+00 1.594E-03 43.86 mm 1.52 mm 628.85 um + 450.00 keV 1.260E+00 1.437E-03 52.90 mm 1.99 mm 723.02 um + 500.00 keV 1.162E+00 1.310E-03 62.75 mm 2.44 mm 826.75 um + 550.00 keV 1.079E+00 1.205E-03 73.40 mm 2.88 mm 939.68 um + 600.00 keV 1.008E+00 1.116E-03 84.83 mm 3.31 mm 1.06 mm + 650.00 keV 9.477E-01 1.039E-03 97.02 mm 3.75 mm 1.19 mm + 700.00 keV 8.947E-01 9.735E-04 109.96 mm 4.18 mm 1.33 mm + 800.00 keV 8.064E-01 8.650E-04 138.03 mm 5.79 mm 1.63 mm + 900.00 keV 7.355E-01 7.791E-04 169.00 mm 7.29 mm 1.96 mm + 1.00 MeV 6.772E-01 7.095E-04 202.78 mm 8.74 mm 2.32 mm + 1.10 MeV 6.322E-01 6.518E-04 239.22 mm 10.17 mm 2.71 mm + 1.20 MeV 5.879E-01 6.031E-04 278.33 mm 11.61 mm 3.13 mm + 1.30 MeV 5.513E-01 5.616E-04 320.21 mm 13.07 mm 3.57 mm + 1.40 MeV 5.197E-01 5.256E-04 364.75 mm 14.54 mm 4.05 mm + 1.50 MeV 4.918E-01 4.942E-04 411.91 mm 16.04 mm 4.54 mm + 1.60 MeV 4.670E-01 4.665E-04 461.66 mm 17.55 mm 5.07 mm + 1.70 MeV 4.448E-01 4.418E-04 513.97 mm 19.09 mm 5.62 mm + 1.80 MeV 4.247E-01 4.197E-04 568.83 mm 20.65 mm 6.19 mm + 2.00 MeV 3.899E-01 3.819E-04 686.03 mm 26.57 mm 7.41 mm + 2.25 MeV 3.542E-01 3.436E-04 846.45 mm 35.05 mm 9.09 mm + 2.50 MeV 3.250E-01 3.125E-04 1.02 m 43.08 mm 10.91 mm + 2.75 MeV 3.005E-01 2.868E-04 1.21 m 50.96 mm 12.88 mm + 3.00 MeV 2.797E-01 2.652E-04 1.42 m 58.81 mm 14.99 mm + 3.25 MeV 2.618E-01 2.467E-04 1.64 m 66.70 mm 17.25 mm + 3.50 MeV 2.462E-01 2.307E-04 1.87 m 74.66 mm 19.66 mm + 3.75 MeV 2.325E-01 2.168E-04 2.12 m 82.73 mm 22.20 mm + 4.00 MeV 2.203E-01 2.045E-04 2.39 m 90.91 mm 24.88 mm + 4.50 MeV 1.997E-01 1.838E-04 2.95 m 121.70 mm 30.66 mm + 5.00 MeV 1.829E-01 1.671E-04 3.58 m 150.69 mm 36.98 mm + 5.50 MeV 1.689E-01 1.533E-04 4.26 m 179.04 mm 43.83 mm + 6.00 MeV 1.570E-01 1.416E-04 4.99 m 207.27 mm 51.20 mm + 6.50 MeV 1.468E-01 1.317E-04 5.78 m 235.63 mm 59.08 mm + 7.00 MeV 1.379E-01 1.231E-04 6.61 m 264.26 mm 67.48 mm + 8.00 MeV 1.232E-01 1.090E-04 8.44 m 370.83 mm 85.77 mm + 9.00 MeV 1.116E-01 9.794E-05 10.48 m 470.37 mm 106.02 mm + 10.00 MeV 1.021E-01 8.897E-05 12.71 m 567.83 mm 128.19 mm + 11.00 MeV 9.416E-02 8.155E-05 15.15 m 665.14 mm 152.25 mm +----------------------------------------------------------- + Multiply Stopping by for Stopping Units + ------------------- ------------------ + 4.1905E-04 eV / Angstrom + 4.1905E-03 keV / micron + 4.1905E-03 MeV / mm + 1.0000E+00 keV / (ug/cm2) + 1.0000E+00 MeV / (mg/cm2) + 1.0000E+03 keV / (mg/cm2) + 1.6738E+00 eV / (1E15 atoms/cm2) + 5.5947E-01 L.S.S. reduced units + ================================================================== + (C) 1984.1989.1992.1998.2008 by J.P. Biersack and J.F. Ziegler diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index a29c1a51b..3481cb98f 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -152,7 +152,7 @@ class TrackFitterUKF : public KalmanFilter { /// function /// template - void predictUKF(PredictionModelCallback predictionModelFunc) + void predictUKF(PredictionModelCallback predictionModelFunc, const Vector &vecZ) { updateAugmentedStateAndCovariance(); @@ -171,7 +171,7 @@ class TrackFitterUKF : public KalmanFilter { const Vector sigmaXxi{util::getColumnAt(i, sigmaXx)}; const Vector sigmaXvi{util::getColumnAt(i, sigmaXv)}; - const Vector Yi{predictionModelFunc(sigmaXxi, sigmaXvi)}; // y = f(x) + const Vector Yi{predictionModelFunc(sigmaXxi, sigmaXvi, vecZ)}; // y = f(x) // Copy the predicted state vector back into the sigmaXx matrix. util::copyToColumn(i, sigmaXx, Yi); diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index 1751ea443..e4b635ad3 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -1,5 +1,9 @@ #include "kalman_filter/TrackFitterUKF.h" +#include "AtELossTable.h" +#include "AtKinematics.h" + +#include "Math/Vector3D.h" #include "gtest/gtest.h" class TrackFitterUKFExampleTest : public testing::Test { @@ -20,7 +24,7 @@ class TrackFitterUKFExampleTest : public testing::Test { /// @param x state vector /// @param v process noise vector /// @return propagated (unaugmented) state vector - static kf::Vector funcF(const kf::Vector &x, const kf::Vector &v) + static kf::Vector funcF(const kf::Vector &x, const kf::Vector &v, const kf::Vector &z) { kf::Vector y; y[0] = x[0] + x[2] + v[0]; @@ -69,7 +73,7 @@ TEST_F(TrackFitterUKFExampleTest, Prediction) m_ukf.setCovarianceQ(Q); m_ukf.setCovarianceR(R); - m_ukf.predictUKF(funcF); + m_ukf.predictUKF(funcF, z); // Expectation from the python results: // ===================================== @@ -130,7 +134,7 @@ TEST_F(TrackFitterUKFExampleTest, PredictionAndCorrection) m_ukf.setCovarianceQ(Q); m_ukf.setCovarianceR(R); - m_ukf.predictUKF(funcF); + m_ukf.predictUKF(funcF, z); // Expectation from the python results: // ===================================== @@ -207,6 +211,8 @@ TEST_F(TrackFitterUKFExampleTest, PredictionAndCorrection) class TrackFitterUKFPhysicsTest : public testing::Test { public: + using XYZVector = ROOT::Math::XYZVector; + virtual void SetUp() override {} virtual void TearDown() override {} @@ -217,18 +223,192 @@ class TrackFitterUKFPhysicsTest : public testing::Test { static constexpr size_t DIM_Z{3}; static constexpr size_t DIM_N{3}; + static XYZVector fBField; // B-field in tesla + static XYZVector fEField; // E-field in V/m + static constexpr double fC = 299792458; // m/s + kf::TrackFitterUKF m_ukf; + /** + * @param pos Position of particle in mm + * @param mom Momentum of particle in MeV/c + * @param charge charge of the particle in Coulombs + * @param mass mass of particle in MeV/c^2 + * @param dedx Stopping power in MeV/mm + * + * @returns Force in N + */ + static XYZVector Force(XYZVector pos, XYZVector mom, double charge, double mass, double dedx) + { + + // auto fourMom = AtTools::Kinematics::Get4Vector(mom, mass); + // auto v = mom / fourMom.E() * c; // m/s + auto v = GetVel(mom, mass); + + auto F_lorentz = charge * (fEField + v.Cross(fBField)); + // std::cout << "F_lorentz: " << F_lorentz << std::endl; + auto dedx_si = dedx * 1.60218e-10; // de_dx in SI units (J/m) + + auto drag = -dedx_si * mom.Unit(); + // std::cout << "drag: " << drag << " mom " << mom << " dedx " << dedx_si << std::endl; + + return F_lorentz + drag; + } + + static XYZVector GetVel(XYZVector mom, double mass) + { + auto fourMom = AtTools::Kinematics::Get4Vector(mom, mass); + const double c = 299792458; // m/s + return mom / fourMom.E() * c; // m/s + } + + static double dist(const XYZVector &x, const XYZVector &z) { return std::sqrt((x - z).Mag2()); } + /// @brief to propagate the state vector using the process model /// @param x state vector /// @param v process noise vector + /// @param vecZ The next measurement point used to stop the propagation. /// @return propagated (unaugmented) state vector - static kf::Vector funcF(const kf::Vector &x, const kf::Vector &v) + static kf::Vector funcF(const kf::Vector &x, const kf::Vector &v, const kf::Vector &z) { + std::cout << "Staring to run funcF" << std::endl; // TODO: This needs to be filled with an RK4 solver for the physics model kf::Vector y{x}; + XYZVector measurement(z[0], z[1], z[2]); // Measurement point in mm + + double charge = x[6]; + double eLoss = v[0]; + double mass = 938.272; // Mass in MeV/c^2 + + double mat_density = 0; // Density of the material in g/cm^3 + AtTools::AtELossTable dedxModel(mat_density); + dedxModel.LoadSrimTable( + "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); // Load the + // SRIM table + // for energy + // loss + double scalingFactor = 1.0; + int iterations = 0; + double calc_eLoss = 0; + + while (std::abs(calc_eLoss - eLoss) > 1e-3) { + std::cout << "Running iteration " << iterations << " with scaling factor: " << scalingFactor + << " and energy loss: " << calc_eLoss << std::endl; + + if (iterations > 100) { + // If we are not converging, we should probably throw an error. + throw std::runtime_error("Energy loss did not converge after 100 iterations."); + } + + // Variables needed in a single run of the RK4 solver. This section needs to be repeated + // until the energy loss converges to the correct value. + double h = 1e-10; // Timestep in s (100 ns to start) + double lastApproach = std::numeric_limits::max(); + bool approaching = true; + iterations++; + + XYZVector pos(x[0], x[1], x[2]); + XYZVector mom(x[3], x[4], x[5]); + double KE_initial = std::sqrt(mom.Mag2() + mass * mass) - mass; // Kinetic energy in MeV + + while (true) { + std::cout << "Position: " << pos.X() << ", " << pos.Y() << ", " << pos.Z() << std::endl; + std::cout << "Momentum: " << mom.X() << ", " << mom.Y() << ", " << mom.Z() << std::endl; + + // Using timestep, propagate state forward one step. + double KE = std::sqrt(mom.Mag2() + mass * mass) - mass; // Kinetic energy in MeV + auto dedx = scalingFactor * dedxModel.GetdEdx(KE); // Get the stopping power in MeV/mm + // std::cout << "KE: " << KE << " dedx: " << dedx << std::endl; + + auto spline = dedxModel.GetSpline(); + // std::cout << "Spline: " << spline.get_x_min() << " to " << spline.get_x_max() << std::endl; + // std::cout << "dxde " << spline(KE) << " dxde " << dedx << std::endl; + + auto x_k1 = GetVel(mom, mass); + auto p_k1 = Force(pos, mom, charge, mass, dedx); + // std::cout << "vel: " << x_k1 << " speed " << x_k1.R() << std::endl; + // std::cout << "Force: " << p_k1 << std::endl; + + auto x_k2 = GetVel(mom + p_k1 * h / 2, mass); + auto p_k2 = Force(pos + x_k1 * h / 2, mom + p_k1 * h / 2, charge, mass, dedx); + // std::cout << "vel: " << x_k2 << " speed " << x_k2.R() << std::endl; + // std::cout << "Force: " << p_k2 << std::endl; + + auto x_k3 = GetVel(mom + p_k2 * h / 2, mass); + auto p_k3 = Force(pos + x_k2 * h / 2, mom + p_k2 * h / 2, charge, mass, dedx); + // std::cout << "vel: " << x_k3 << " speed " << x_k3.R() << std::endl; + // std::cout << "Force: " << p_k3 << std::endl; + + auto x_k4 = GetVel(mom + p_k3 * h, mass); + auto p_k4 = Force(pos + x_k3 * h, mom + p_k3 * h, charge, mass, dedx); + // std::cout << "vel: " << x_k4 << " speed " << x_k4.R() << std::endl; + // std::cout << "Force: " << p_k4 << std::endl; + + auto mom_SItoMeV = 1.60218e-13 / 299792458; // Factor to convert momentum to MeV/c from kg*m/s + auto F_SI = (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) / 6; + + // Convert momentum to SI, update, and convert back to MeV/c + auto mom_SI = mom * mom_SItoMeV; + mom_SI += F_SI * h; + mom = mom_SI / mom_SItoMeV; // Convert back to MeV/c + + // Convert position to SI, update, and convert back to mm + auto pos_SI = pos / 1e3; // Convert mm to m + pos_SI += (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) * h / 6; + pos = pos_SI * 1e3; // Convert back to mm + + std::cout << "Average force: " << F_SI << " N" << std::endl; + std::cout << "Momentum: " << mom * mom_SItoMeV << " kg m/s" << std::endl; + std::cout << "Delta x: " << (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) / 6 << " m/s" << std::endl; + + auto approach = dist(pos, measurement); + std::cout << "pos: " << pos << " measurement" << measurement << std::endl; + std::cout << "Approach: " << approach << " last approach: " << lastApproach << std::endl; + if (approach < lastApproach) { + // We are still approaching the measurement point + approaching = true; + lastApproach = approach; + + continue; + } + + bool reachedMeasurementPoint = (approaching && approach > lastApproach); + bool particleStopped = std::sqrt(mom.Mag2() + mass * mass) - mass < 0.01; + if (reachedMeasurementPoint || particleStopped) { + // Last iteration we were still approaching the measurement point. Now we are further away + // then before. We have probably reached the measurement point if things are well behaved. + // I can think of cases where this will not be true. A better solution might be to run + // tracking the point of closest approach until the distance between the current state and + // the measurement point is larger than the distance between the last state and the measurement point. + + // Undo the last update, and break out of the integration loop + // pos = pos - (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) * h / 6; + // mom = mom - (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) * h / 6; + y[0] = pos.X(); + y[1] = pos.Y(); + y[2] = pos.Z(); + y[3] = mom.X(); + y[4] = mom.Y(); + y[5] = mom.Z(); + + // Update the scaling factor + double KE_final = std::sqrt(mom.Mag2() + mass * mass) - mass; + calc_eLoss = KE_initial - KE_final; // Energy loss in MeV + scalingFactor *= calc_eLoss / eLoss; + std::cout << "------- End of RK4 interation ---------" << std::endl; + std::cout << "Particle stopped: " << particleStopped << std::endl; + std::cout << "Reached measurement point: " << reachedMeasurementPoint << std::endl; + std::cout << "Last approach: " << lastApproach << " Current approach: " << approach << std::endl; + std::cout << "Desired energy loss: " << eLoss << " MeV" << std::endl; + std::cout << "Calculated energy loss: " << calc_eLoss << " MeV" << std::endl; + std::cout << "New scaling factor: " << scalingFactor << std::endl; + std::cout << "Final Position: " << pos.X() << ", " << pos.Y() << ", " << pos.Z() << std::endl; + std::cout << "Final Momentum: " << mom.X() << ", " << mom.Y() << ", " << mom.Z() << std::endl; + return y; + } + } // End of loop over RK4 integration + } // End loop over energy loss convergence - // For now, we just return the state vector as is return y; } @@ -244,8 +424,15 @@ class TrackFitterUKFPhysicsTest : public testing::Test { return y; } + + const double mass_p = 938.272; // Mass of proton in MeV/c^2 + const double charge_p = 1.602176634e-19; // Charge of proton }; +// Definition of static member variables +TrackFitterUKFPhysicsTest::XYZVector TrackFitterUKFPhysicsTest::fBField; +TrackFitterUKFPhysicsTest::XYZVector TrackFitterUKFPhysicsTest::fEField; + TEST_F(TrackFitterUKFPhysicsTest, PhysicsPrediction) { // TODO: This needs to be filled with a proper physics test. @@ -263,6 +450,102 @@ TEST_F(TrackFitterUKFPhysicsTest, PhysicsPrediction) m_ukf.vecX() = x; m_ukf.matP() = P; - m_ukf.setCovarianceR(R); - m_ukf.predictUKF(funcF); + // m_ukf.setCovarianceR(R); + // m_ukf.predictUKF(funcF, z); + ASSERT_EQ(true, true); +} + +TEST_F(TrackFitterUKFPhysicsTest, TestForceNoFields) +{ + XYZVector pos(0, 0, 0); // Position in mm + XYZVector mom(100, 0, 0); // Momentum in MeV/c + fBField = XYZVector(0, 0, 0); // B-field in tesla + fEField = XYZVector(0, 0, 0); // E-field + + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + double dedx = 1; // Stopping power in MeV/mm + + auto force = Force(pos, mom, charge, mass, dedx); + + ASSERT_NEAR(force.X(), -1.602e-10, FLOAT_EPSILON); + ASSERT_NEAR(force.Y(), 0, FLOAT_EPSILON); + ASSERT_NEAR(force.Z(), 0, FLOAT_EPSILON); + + mom = XYZVector(100, 0, 100); // Reset momentum + force = Force(pos, mom, charge, mass, dedx); + ASSERT_NEAR(force.X(), -1.602e-10 / std::sqrt(2), FLOAT_EPSILON); + ASSERT_NEAR(force.Y(), 0, FLOAT_EPSILON); + ASSERT_NEAR(force.Z(), -1.602e-10 / std::sqrt(2), FLOAT_EPSILON); +} + +TEST_F(TrackFitterUKFPhysicsTest, TestForceEField) +{ + XYZVector pos(0, 0, 0); // Position in mm + XYZVector mom(100, 0, 0); // Momentum in MeV/c + fBField = XYZVector(0, 0, 1); // B-field in tesla + fEField = XYZVector(0, 0, 0); // E-field in V/m + + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + double dedx = 0; // Stopping power in MeV/mm + + auto force = Force(pos, mom, charge, mass, dedx); + + ASSERT_NEAR(force.X(), 0, FLOAT_EPSILON); + ASSERT_NEAR(force.Y(), 0, FLOAT_EPSILON); + ASSERT_NEAR(force.Z(), 1.121e-14, FLOAT_EPSILON); +} + +TEST_F(TrackFitterUKFPhysicsTest, TestForceBField) +{ + XYZVector pos(0, 0, 0); // Position in mm + XYZVector mom(100, 0, 0); // Momentum in MeV/c + fBField = XYZVector(0, 0, 1); // B-field in tesla + fEField = XYZVector(0, 0, 0); // E-field + + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + double dedx = 0; // Stopping power in MeV/mm + + auto force = Force(pos, mom, charge, mass, dedx); + + ASSERT_NEAR(force.X(), 0, FLOAT_EPSILON); + ASSERT_NEAR(force.Y(), -5.09e-12, FLOAT_EPSILON); + ASSERT_NEAR(force.Z(), 0, FLOAT_EPSILON); +} + +TEST_F(TrackFitterUKFPhysicsTest, TestPropagatorNoField) +{ + + double KE = 1; // Kinetic energy in MeV + double E = KE + mass_p; + double p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c + fBField = XYZVector(0, 0, 0); // B-field in tesla + fEField = XYZVector(0, 0, 0); // E-field + + kf::Vector x; // Initial state vector + x[0] = 0; + x[1] = 0; + x[2] = 0; + x[3] = p; // p_x + x[4] = 0; + x[5] = 0; + + ASSERT_NEAR(x[3], 43.331, 1e-1); // Make sure momentum is calculated correctly + + kf::Vector v; // Process noise vector + v[0] = 1; // Energy loss in MeV + v[1] = 0.0; // No process noise in this example + + kf::Vector z; // Measurement vector + z[0] = 1e3; + z[1] = 0; + z[2] = 0; + + auto final = funcF(x, v, z); // Propagate the state vector using the process model + + // Check the final position is close to the stopping point from LISE + ASSERT_NEAR(final[3], 0, 0.1); // Final momentum in x-direction should be close to 0 + ASSERT_NEAR(final[0], 210, 10); // Final position in x-direction should be close to 210 mm } diff --git a/AtTools/AtELossTable.h b/AtTools/AtELossTable.h index de3c29657..683bdf57d 100644 --- a/AtTools/AtELossTable.h +++ b/AtTools/AtELossTable.h @@ -51,6 +51,8 @@ class AtELossTable : public AtELossModel { [[deprecated]] double GetEnergyOld(double energyIni, double distance) const; + const tk::spline &GetSpline() const { return fdXdE; } + private: void LoadTable(const std::vector &energy, const std::vector &dEdX); void LoadRangeVariance(const std::vector &energy, const std::vector &rangeVariance) From abae9bc06958a953abd1a79c5127426ff9a56ecd Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 17 Jul 2025 15:58:56 +0200 Subject: [PATCH 08/75] Add additional test --- .../OpenKF/kalman_filter/TrackFitterUKFTest.cxx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index e4b635ad3..a7dbb9c68 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -381,9 +381,6 @@ class TrackFitterUKFPhysicsTest : public testing::Test { // tracking the point of closest approach until the distance between the current state and // the measurement point is larger than the distance between the last state and the measurement point. - // Undo the last update, and break out of the integration loop - // pos = pos - (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) * h / 6; - // mom = mom - (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) * h / 6; y[0] = pos.X(); y[1] = pos.Y(); y[2] = pos.Z(); @@ -548,4 +545,13 @@ TEST_F(TrackFitterUKFPhysicsTest, TestPropagatorNoField) // Check the final position is close to the stopping point from LISE ASSERT_NEAR(final[3], 0, 0.1); // Final momentum in x-direction should be close to 0 ASSERT_NEAR(final[0], 210, 10); // Final position in x-direction should be close to 210 mm + + KE = 0.5; + E = KE + mass_p; + p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c + x[3] = p; // Reset momentum + final = funcF(x, v, z); // Propagate the state vector using the + + ASSERT_NEAR(final[3], 0, 0.1); // Final momentum in x-direction should be close to 0 + ASSERT_NEAR(final[0], 68.6, 5); // Final position in x } From a7036b6624dbf08efce476ea38c4505b6b41c8c1 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 17 Jul 2025 17:01:24 +0200 Subject: [PATCH 09/75] Add test for fixing energy loss --- .../kalman_filter/TrackFitterUKFTest.cxx | 84 +++++++++++++++---- 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index a7dbb9c68..903e5fb7d 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -291,7 +291,7 @@ class TrackFitterUKFPhysicsTest : public testing::Test { int iterations = 0; double calc_eLoss = 0; - while (std::abs(calc_eLoss - eLoss) > 1e-3) { + while (std::abs(calc_eLoss - eLoss) > 1e-4) { std::cout << "Running iteration " << iterations << " with scaling factor: " << scalingFactor << " and energy loss: " << calc_eLoss << std::endl; @@ -309,9 +309,12 @@ class TrackFitterUKFPhysicsTest : public testing::Test { XYZVector pos(x[0], x[1], x[2]); XYZVector mom(x[3], x[4], x[5]); + double KE_initial = std::sqrt(mom.Mag2() + mass * mass) - mass; // Kinetic energy in MeV while (true) { + XYZVector lastPos = pos; + XYZVector lastMom = mom; std::cout << "Position: " << pos.X() << ", " << pos.Y() << ", " << pos.Z() << std::endl; std::cout << "Momentum: " << mom.X() << ", " << mom.Y() << ", " << mom.Z() << std::endl; @@ -357,13 +360,13 @@ class TrackFitterUKFPhysicsTest : public testing::Test { pos_SI += (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) * h / 6; pos = pos_SI * 1e3; // Convert back to mm - std::cout << "Average force: " << F_SI << " N" << std::endl; - std::cout << "Momentum: " << mom * mom_SItoMeV << " kg m/s" << std::endl; - std::cout << "Delta x: " << (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) / 6 << " m/s" << std::endl; + // std::cout << "Average force: " << F_SI << " N" << std::endl; + // std::cout << "Momentum: " << mom * mom_SItoMeV << " kg m/s" << std::endl; + // std::cout << "Delta x: " << (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) / 6 << " m/s" << std::endl; auto approach = dist(pos, measurement); - std::cout << "pos: " << pos << " measurement" << measurement << std::endl; - std::cout << "Approach: " << approach << " last approach: " << lastApproach << std::endl; + // std::cout << "pos: " << pos << " measurement" << measurement << std::endl; + // std::cout << "Approach: " << approach << " last approach: " << lastApproach << std::endl; if (approach < lastApproach) { // We are still approaching the measurement point approaching = true; @@ -381,27 +384,30 @@ class TrackFitterUKFPhysicsTest : public testing::Test { // tracking the point of closest approach until the distance between the current state and // the measurement point is larger than the distance between the last state and the measurement point. - y[0] = pos.X(); - y[1] = pos.Y(); - y[2] = pos.Z(); - y[3] = mom.X(); - y[4] = mom.Y(); - y[5] = mom.Z(); + // Undo the last step since we were closer last time. + y[0] = lastPos.X(); + y[1] = lastPos.Y(); + y[2] = lastPos.Z(); + y[3] = lastMom.X(); + y[4] = lastMom.Y(); + y[5] = lastMom.Z(); // Update the scaling factor - double KE_final = std::sqrt(mom.Mag2() + mass * mass) - mass; + double KE_final = std::sqrt(lastMom.Mag2() + mass * mass) - mass; calc_eLoss = KE_initial - KE_final; // Energy loss in MeV - scalingFactor *= calc_eLoss / eLoss; - std::cout << "------- End of RK4 interation ---------" << std::endl; + scalingFactor *= eLoss / calc_eLoss; + std::cout << "------- End of RK4 interation " << iterations << " ---------" << std::endl; std::cout << "Particle stopped: " << particleStopped << std::endl; std::cout << "Reached measurement point: " << reachedMeasurementPoint << std::endl; std::cout << "Last approach: " << lastApproach << " Current approach: " << approach << std::endl; std::cout << "Desired energy loss: " << eLoss << " MeV" << std::endl; std::cout << "Calculated energy loss: " << calc_eLoss << " MeV" << std::endl; + std::cout << "Difference: " << calc_eLoss - eLoss << " MeV" << std::endl; std::cout << "New scaling factor: " << scalingFactor << std::endl; std::cout << "Final Position: " << pos.X() << ", " << pos.Y() << ", " << pos.Z() << std::endl; std::cout << "Final Momentum: " << mom.X() << ", " << mom.Y() << ", " << mom.Z() << std::endl; - return y; + break; + // return y; } } // End of loop over RK4 integration } // End loop over energy loss convergence @@ -512,7 +518,7 @@ TEST_F(TrackFitterUKFPhysicsTest, TestForceBField) ASSERT_NEAR(force.Z(), 0, FLOAT_EPSILON); } -TEST_F(TrackFitterUKFPhysicsTest, TestPropagatorNoField) +TEST_F(TrackFitterUKFPhysicsTest, TestPropagatorStoppingNoField) { double KE = 1; // Kinetic energy in MeV @@ -555,3 +561,47 @@ TEST_F(TrackFitterUKFPhysicsTest, TestPropagatorNoField) ASSERT_NEAR(final[3], 0, 0.1); // Final momentum in x-direction should be close to 0 ASSERT_NEAR(final[0], 68.6, 5); // Final position in x } + +TEST_F(TrackFitterUKFPhysicsTest, TestPropagatorNoField) +{ + double KE = 1; // Kinetic energy in MeV + double E = KE + mass_p; + double p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c + fBField = XYZVector(0, 0, 0); // B-field in tesla + fEField = XYZVector(0, 0, 0); // E-field + + kf::Vector x; // Initial state vector + x[0] = 0; + x[1] = 0; + x[2] = 0; + x[3] = p; // p_x + x[4] = 0; + x[5] = 0; + + ASSERT_NEAR(x[3], 43.331, 1e-1); // Make sure momentum is calculated correctly + + kf::Vector v; // Process noise vector + v[0] = 0.0285; // Energy loss in MeV + v[1] = 0.0; // No process noise in this example + + kf::Vector z; // Measurement vector + z[0] = 10; // Measure after 10 mm + z[1] = 0; + z[2] = 0; + + auto final = funcF(x, v, z); // Propagate the state vector using the process model + + // Check the final position is close to the stopping point from LISE + double E_fin = KE - v[0] + mass_p; + double p_fin = std::sqrt(E_fin * E_fin - mass_p * mass_p); + ASSERT_NEAR(final[3], p_fin, 0.1); // Final momentum in x-direction + ASSERT_NEAR(final[0], 10, .5); // Final position in x-direction should be close to 10 mm + + v[0] = 0.3237; // Energy loss in MeV + z[0] = 100; // Measure after 100 mm + final = funcF(x, v, z); // Propagate the state vector using the process model + E_fin = KE - v[0] + mass_p; + p_fin = std::sqrt(E_fin * E_fin - mass_p * mass_p); + ASSERT_NEAR(final[3], p_fin, 0.1); // Final momentum + ASSERT_NEAR(final[0], 100, .5); // Final position +} From 37fc28bfa376c76c5ac0f27e5e15f1154ea947d7 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 17 Jul 2025 18:13:37 +0200 Subject: [PATCH 10/75] Move some code from tests to AtTools --- AtTools/AtKinematics.cxx | 9 +++- AtTools/AtKinematics.h | 19 ++++++++ AtTools/AtPropagator.cxx | 20 ++++++++ AtTools/AtPropagator.h | 93 +++++++++++++++++++++++++++++++++++ AtTools/AtPropagatorTest.cxx | 95 ++++++++++++++++++++++++++++++++++++ AtTools/AtToolsLinkDef.h | 1 + AtTools/CMakeLists.txt | 2 + 7 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 AtTools/AtPropagator.cxx create mode 100644 AtTools/AtPropagator.h create mode 100644 AtTools/AtPropagatorTest.cxx diff --git a/AtTools/AtKinematics.cxx b/AtTools/AtKinematics.cxx index 2431d34f2..755baf91c 100644 --- a/AtTools/AtKinematics.cxx +++ b/AtTools/AtKinematics.cxx @@ -303,5 +303,12 @@ double EtoA(double mass) { return mass / 931.5; } - +ROOT::Math::XYZVector GetVel(ROOT::Math::XYZVector mom, double mass) +{ + return mom / Get4Vector(mom, mass).E() * fC; +} +double KE(ROOT::Math::XYZVector mom, double mass) +{ + return std::sqrt(mom.Mag2() + mass * mass) - mass; // Kinetic energy in MeV +} } // namespace AtTools::Kinematics diff --git a/AtTools/AtKinematics.h b/AtTools/AtKinematics.h index b0d343fd2..8be5c0111 100644 --- a/AtTools/AtKinematics.h +++ b/AtTools/AtKinematics.h @@ -7,6 +7,7 @@ #ifndef ATKINEMATICS_H #define ATKINEMATICS_H +#include #include #include // for PxPyPzEVector #include // for Double_t, THashConsistencyHolder, Int_t, ClassDef @@ -64,6 +65,8 @@ class AtKinematics : public TObject { namespace Kinematics { +static constexpr double fC = 299792458.0; // Speed of light in m/s + double GetGamma(double KE, double m1, double m2); double GetGamma(double beta); double GetVelocity(double gamma); @@ -74,6 +77,22 @@ double GetRelMom(double gamma, double mass); double AtoE(double Amu); double EtoA(double mass); +/** + * Calculate the kinetic energy of a particle given its momentum and mass. + * @param mom Momentum vector (in MeV/c) + * @param mass Mass of the particle (in MeV/c^2) + * @returns Kinetic energy in MeV + */ +double KE(ROOT::Math::XYZVector mom, double mass); + +/** + * Calculate the velocity vector from momentum and mass + * @param mom Momentum vector (in MeV/c) + * @param mass Mass of the particle (in MeV/c^2) + * @returns Velocity vector in m/s + */ +ROOT::Math::XYZVector GetVel(ROOT::Math::XYZVector mom, double mass); + template ROOT::Math::PxPyPzEVector Get4Vector(Vector mom, double m) { diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx new file mode 100644 index 000000000..c7a9b8858 --- /dev/null +++ b/AtTools/AtPropagator.cxx @@ -0,0 +1,20 @@ +#include "AtPropagator.h" + +#include "AtKinematics.h" +namespace AtTools { + +AtPropagator::XYZVector AtPropagator::Force(XYZVector pos, XYZVector mom) const +{ + auto v = Kinematics::GetVel(mom, fMass); + + auto F_lorentz = fQ * (fEField + v.Cross(fBField)); + // std::cout << "F_lorentz: " << F_lorentz << std::endl; + auto dedx = fScalingFactor * fELossModel->GetdEdx(Kinematics::KE(mom, fMass)); // Stopping power in MeV/mm + auto dedx_si = dedx * 1.60218e-10; // de_dx in SI units (J/m) + + auto drag = -dedx_si * mom.Unit(); + // std::cout << "drag: " << drag << " mom " << mom << " dedx " << dedx_si << std::endl; + + return F_lorentz + drag; +} +} // namespace AtTools diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h new file mode 100644 index 000000000..58a2df0d3 --- /dev/null +++ b/AtTools/AtPropagator.h @@ -0,0 +1,93 @@ +#ifndef ATPROPAGATOR_H +#define ATPROPAGATOR_H + +#include "AtELossModel.h" + +#include "Math/Vector3D.h" + +namespace AtTools { + +/** + * @brief Class for propagating particles through a medium. + * + * This class is responsible for simulating the propagation of particles + * through a medium, taking into account energy loss and other effects. + * Uses an AtELossModel to calculate the energy loss and propagates particles + * in the presence of electric and magnetic fields. + * + * Class is designed to be used with a single particle type. Create a new instance of the + * class if the material or particle type changes. + */ +class AtPropagator { +protected: + using XYZVector = ROOT::Math::XYZVector; + XYZVector fEField{0, 0, 0}; // Electric field vector + XYZVector fBField{0, 0, 0}; // Magnetic field vector + + const double fQ; // Charge of the particle in Coulombs + const double fMass; // Mass of the particle in MeV/c^2 + const std::unique_ptr fELossModel; // Energy loss model + + // Internal state variables for the propagator + double fH = 1e-10; /// Step size for propagation in s + double fETolerance = 1e-4; /// Energy tolerance for convergence when fixing energy loss + double fScalingFactor = 1.0; /// Scaling factor for energy loss + + XYZVector fPos; // Current position of the particle in mm + XYZVector fMom; // Current momentum of the particle in MeV/c + + static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) + +public: + AtPropagator(double charge, double mass, std::unique_ptr elossModel) + : fQ(charge), fMass(mass), fELossModel(std::move(elossModel)) + { + } + /** + * @brief Set the electric field (V/m) + */ + void SetEField(const XYZVector &eField) { fEField = eField; } + void SetEField(double ex, double ey, double ez) { fEField.SetXYZ(ex, ey, ez); } + /** + * @brief Set the magnetic field (T) + */ + void SetBField(const XYZVector &bField) { fBField = bField; } + void SetBField(double bx, double by, double bz) { fBField.SetXYZ(bx, by, bz); } + + /** + * @brief Set the state of the particle. + * + * @param pos Position of the particle in mm. + * @param mom Momentum of the particle in MeV/c. + */ + void SetState(const XYZVector &pos, const XYZVector &mom) + { + fPos = pos; + fMom = mom; + } + XYZVector GetPosition() const { return fPos; } + XYZVector GetMomentum() const { return fMom; } + + /** + * @brief Propagate the particle to the point of closest approach to the given point. + * + * Propagate to a given point in space, adjusting the magnitude of the stopping power + * to ensure that a specific about of energy is lost during the propagation. + * + * @param point The point to approach. + * @param eLoss If not 0, constrain the energy loss to this value (adjusting the stopping power). + */ + void PropagateToPoint(const XYZVector &point, double eLoss = 0); + + /** + * @brief Calculate the force acting on the particle. + * + * @param pos Position of the particle in mm. + * @param mom Momentum of the particle in MeV/c. + * @return The force acting on the particle in N. + */ + XYZVector Force(XYZVector pos, XYZVector mom) const; +}; + +} // namespace AtTools +#endif // #ifndef ATPROPAGATOR_H diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx new file mode 100644 index 000000000..56a6ba77f --- /dev/null +++ b/AtTools/AtPropagatorTest.cxx @@ -0,0 +1,95 @@ +#include "AtPropagator.h" + +#include "AtKinematics.h" + +#include + +#include +#include +using ROOT::Math::XYZVector; + +using namespace AtTools; + +const double mass_p = 938.272; // Mass of proton in MeV/c^2 +const double charge_p = 1.602176634e-19; // Charge of proton + +class DummyELossModel : public AtELossModel { +public: + double eLoss = 1; + DummyELossModel() : AtELossModel(0) {} + + double GetdEdx(double /*KE*/) const override { return eLoss; } + double GetRange(double /*energyIni*/, double /*energyFin = 0*/) const override { return 1.0; } + double GetEnergyLoss(double /*energyIni*/, double /*distance*/) const override { return 1.0; } + double GetEnergy(double /*energyIni*/, double /*distance*/) const override { return 1.0; } +}; + +TEST(AtPropagatorTest, ForceNoField) +{ + XYZVector pos(0, 0, 0); // Position in mm + XYZVector mom(100, 0, 0); // Momentum in MeV/c + + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + double dedx = 1; // Stopping power in MeV/mm + + // Create a dummy energy loss model + auto elossModel = std::make_unique(); + AtPropagator propagator(charge, mass, std::move(elossModel)); + propagator.SetEField({0, 0, 0}); + propagator.SetBField({0, 0, 0}); + + auto force = propagator.Force(pos, mom); + + ASSERT_NEAR(force.X(), -1.602e-10, 1e-12); + ASSERT_NEAR(force.Y(), 0, 1e-12); + ASSERT_NEAR(force.Z(), 0, 1e-12); + + mom = XYZVector(100, 0, 100); // Reset momentum + force = propagator.Force(pos, mom); + ASSERT_NEAR(force.X(), -1.602e-10 / std::sqrt(2), 1e-12); + ASSERT_NEAR(force.Y(), 0, 1e-12); + ASSERT_NEAR(force.Z(), -1.602e-10 / std::sqrt(2), 1e-12); +} + +TEST(AtPropagatorTest, ForceEField) +{ + XYZVector pos(0, 0, 0); // Position in mm + XYZVector mom(100, 0, 0); // Momentum in MeV/c + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + + // Create a dummy energy loss model + auto elossModel = std::make_unique(); + elossModel->eLoss = 0; // No energy loss for this test + AtPropagator propagator(charge, mass, std::move(elossModel)); + propagator.SetEField({0, 0, 70000}); + propagator.SetBField({0, 0, 0}); + + auto force = propagator.Force(pos, mom); + + ASSERT_NEAR(force.X(), 0, 1e-12); + ASSERT_NEAR(force.Y(), 0, 1e-12); + ASSERT_NEAR(force.Z(), 1.121e-14, 1e-15); +} + +TEST(AtPropagatorTest, ForceBField) +{ + XYZVector pos(0, 0, 0); // Position in mm + XYZVector mom(100, 0, 0); // Momentum in MeV/c + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + + // Create a dummy energy loss model + auto elossModel = std::make_unique(); + elossModel->eLoss = 0; // No energy loss for this test + AtPropagator propagator(charge, mass, std::move(elossModel)); + propagator.SetEField({0, 0, 0}); + propagator.SetBField({0, 0, 1}); + + auto force = propagator.Force(pos, mom); + + ASSERT_NEAR(force.X(), 0, 1e-12); + ASSERT_NEAR(force.Y(), -5.09e-12, 1e-13); + ASSERT_NEAR(force.Z(), 0, 1e-12); +} \ No newline at end of file diff --git a/AtTools/AtToolsLinkDef.h b/AtTools/AtToolsLinkDef.h index 729423c95..c8f178919 100644 --- a/AtTools/AtToolsLinkDef.h +++ b/AtTools/AtToolsLinkDef.h @@ -27,6 +27,7 @@ #pragma link C++ class AtEDistortionModel - !; #pragma link C++ class AtTools::AtKinematics + ; +#pragma link C++ class AtTools::AtPropagator - !; #pragma link C++ class AtTools::AtVirtualTerminal + ; #pragma link C++ class RandomSample::AtSample - !; diff --git a/AtTools/CMakeLists.txt b/AtTools/CMakeLists.txt index eae5925fc..82b04ad96 100644 --- a/AtTools/CMakeLists.txt +++ b/AtTools/CMakeLists.txt @@ -19,6 +19,7 @@ set(SRCS AtEDistortionModel.cxx AtVirtualTerminal.cxx AtKinematics.cxx + AtPropagator.cxx AtFormat.cxx AtSpline.cxx @@ -77,6 +78,7 @@ Set(INCLUDE_DIR set(TEST_SRCS DataCleaning/AtkNNTest.cxx AtELossTableTest.cxx + AtPropagatorTest.cxx ) if(CATIMA_FOUND) set(TEST_SRCS ${TEST_SRCS} From b0bed98062bc83f75fd3d3714ce38fa0b3665502 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Mon, 21 Jul 2025 11:42:12 +0200 Subject: [PATCH 11/75] Add Propagator class for charged particles in mat Also adds tests. --- .../kalman_filter/TrackFitterUKFTest.cxx | 27 ++--- AtTools/AtPropagator.cxx | 108 +++++++++++++++++- AtTools/AtPropagator.h | 12 +- AtTools/AtPropagatorTest.cxx | 84 ++++++++++++++ 4 files changed, 212 insertions(+), 19 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index 903e5fb7d..183e96c0c 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -243,7 +243,7 @@ class TrackFitterUKFPhysicsTest : public testing::Test { // auto fourMom = AtTools::Kinematics::Get4Vector(mom, mass); // auto v = mom / fourMom.E() * c; // m/s - auto v = GetVel(mom, mass); + auto v = AtTools::Kinematics::GetVel(mom, mass); auto F_lorentz = charge * (fEField + v.Cross(fBField)); // std::cout << "F_lorentz: " << F_lorentz << std::endl; @@ -255,13 +255,6 @@ class TrackFitterUKFPhysicsTest : public testing::Test { return F_lorentz + drag; } - static XYZVector GetVel(XYZVector mom, double mass) - { - auto fourMom = AtTools::Kinematics::Get4Vector(mom, mass); - const double c = 299792458; // m/s - return mom / fourMom.E() * c; // m/s - } - static double dist(const XYZVector &x, const XYZVector &z) { return std::sqrt((x - z).Mag2()); } /// @brief to propagate the state vector using the process model @@ -310,7 +303,8 @@ class TrackFitterUKFPhysicsTest : public testing::Test { XYZVector pos(x[0], x[1], x[2]); XYZVector mom(x[3], x[4], x[5]); - double KE_initial = std::sqrt(mom.Mag2() + mass * mass) - mass; // Kinetic energy in MeV + double KE_initial = + AtTools::Kinematics::KE(mom, mass); // std::sqrt(mom.Mag2() + mass * mass) - mass; // Kinetic energy in MeV while (true) { XYZVector lastPos = pos; @@ -319,30 +313,30 @@ class TrackFitterUKFPhysicsTest : public testing::Test { std::cout << "Momentum: " << mom.X() << ", " << mom.Y() << ", " << mom.Z() << std::endl; // Using timestep, propagate state forward one step. - double KE = std::sqrt(mom.Mag2() + mass * mass) - mass; // Kinetic energy in MeV - auto dedx = scalingFactor * dedxModel.GetdEdx(KE); // Get the stopping power in MeV/mm + double KE = AtTools::Kinematics::KE(mom, mass); // Kinetic energy in MeV + auto dedx = scalingFactor * dedxModel.GetdEdx(KE); // Get the stopping power in MeV/mm // std::cout << "KE: " << KE << " dedx: " << dedx << std::endl; auto spline = dedxModel.GetSpline(); // std::cout << "Spline: " << spline.get_x_min() << " to " << spline.get_x_max() << std::endl; // std::cout << "dxde " << spline(KE) << " dxde " << dedx << std::endl; - auto x_k1 = GetVel(mom, mass); + auto x_k1 = AtTools::Kinematics::GetVel(mom, mass); auto p_k1 = Force(pos, mom, charge, mass, dedx); // std::cout << "vel: " << x_k1 << " speed " << x_k1.R() << std::endl; // std::cout << "Force: " << p_k1 << std::endl; - auto x_k2 = GetVel(mom + p_k1 * h / 2, mass); + auto x_k2 = AtTools::Kinematics::GetVel(mom + p_k1 * h / 2, mass); auto p_k2 = Force(pos + x_k1 * h / 2, mom + p_k1 * h / 2, charge, mass, dedx); // std::cout << "vel: " << x_k2 << " speed " << x_k2.R() << std::endl; // std::cout << "Force: " << p_k2 << std::endl; - auto x_k3 = GetVel(mom + p_k2 * h / 2, mass); + auto x_k3 = AtTools::Kinematics::GetVel(mom + p_k2 * h / 2, mass); auto p_k3 = Force(pos + x_k2 * h / 2, mom + p_k2 * h / 2, charge, mass, dedx); // std::cout << "vel: " << x_k3 << " speed " << x_k3.R() << std::endl; // std::cout << "Force: " << p_k3 << std::endl; - auto x_k4 = GetVel(mom + p_k3 * h, mass); + auto x_k4 = AtTools::Kinematics::GetVel(mom + p_k3 * h, mass); auto p_k4 = Force(pos + x_k3 * h, mom + p_k3 * h, charge, mass, dedx); // std::cout << "vel: " << x_k4 << " speed " << x_k4.R() << std::endl; // std::cout << "Force: " << p_k4 << std::endl; @@ -457,6 +451,7 @@ TEST_F(TrackFitterUKFPhysicsTest, PhysicsPrediction) // m_ukf.predictUKF(funcF, z); ASSERT_EQ(true, true); } +/* TEST_F(TrackFitterUKFPhysicsTest, TestForceNoFields) { @@ -553,6 +548,7 @@ TEST_F(TrackFitterUKFPhysicsTest, TestPropagatorStoppingNoField) ASSERT_NEAR(final[0], 210, 10); // Final position in x-direction should be close to 210 mm KE = 0.5; + v[0] = 0.5; // Energy loss in MeV E = KE + mass_p; p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c x[3] = p; // Reset momentum @@ -561,6 +557,7 @@ TEST_F(TrackFitterUKFPhysicsTest, TestPropagatorStoppingNoField) ASSERT_NEAR(final[3], 0, 0.1); // Final momentum in x-direction should be close to 0 ASSERT_NEAR(final[0], 68.6, 5); // Final position in x } +*/ TEST_F(TrackFitterUKFPhysicsTest, TestPropagatorNoField) { diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index c7a9b8858..3728335af 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -1,6 +1,8 @@ #include "AtPropagator.h" #include "AtKinematics.h" + +#include namespace AtTools { AtPropagator::XYZVector AtPropagator::Force(XYZVector pos, XYZVector mom) const @@ -8,13 +10,113 @@ AtPropagator::XYZVector AtPropagator::Force(XYZVector pos, XYZVector mom) const auto v = Kinematics::GetVel(mom, fMass); auto F_lorentz = fQ * (fEField + v.Cross(fBField)); - // std::cout << "F_lorentz: " << F_lorentz << std::endl; + LOG(debug) << "F_lorentz: " << F_lorentz; auto dedx = fScalingFactor * fELossModel->GetdEdx(Kinematics::KE(mom, fMass)); // Stopping power in MeV/mm auto dedx_si = dedx * 1.60218e-10; // de_dx in SI units (J/m) auto drag = -dedx_si * mom.Unit(); - // std::cout << "drag: " << drag << " mom " << mom << " dedx " << dedx_si << std::endl; + LOG(debug) << "drag: " << drag << " mom " << mom << " dedx " << dedx_si; + + return F_lorentz + drag; // Force in N +} + +void AtPropagator::RK4Step() +{ + double h = fH; // Step size in seconds + + auto x_k1 = Kinematics::GetVel(fMom, fMass); + auto p_k1 = Force(fPos, fMom); + + auto x_k2 = Kinematics::GetVel(fMom + p_k1 * h / 2, fMass); + auto p_k2 = Force(fPos + x_k1 * h / 2, fMom + p_k1 * h / 2); + + auto x_k3 = Kinematics::GetVel(fMom + p_k2 * h / 2, fMass); + auto p_k3 = Force(fPos + x_k2 * h / 2, fMom + p_k2 * h / 2); + + auto x_k4 = Kinematics::GetVel(fMom + p_k3 * h, fMass); + auto p_k4 = Force(fPos + x_k3 * h, fMom + p_k3 * h); + + auto F_SI = (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) / 6; // Force in SI units (N) + + auto mom_SI = fReltoSImom * fMom; + mom_SI += F_SI * h; // Update momentum in SI units (kg m/s) + fMom = mom_SI / fReltoSImom; // Convert back to + + auto pos_SI = fPos * 1e-3; // Convert position to SI units (m) + pos_SI += (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) * h / 6; // Update position in SI units (m + fPos = pos_SI * 1e3; // Convert back to mm +} + +void AtPropagator::PropagateToPoint(const XYZVector &point, double eLoss) +{ + LOG(info) << "Propagating to point: " << point << " with eLoss: " << eLoss; + + double scalingFactor = 1.0; + int iterations = 0; + double calc_eLoss = 0; + + while (std::abs(calc_eLoss - eLoss) > 1e-4 || eLoss == 0) { + LOG(debug) << "Running iteration " << iterations << " with scaling factor: " << scalingFactor + << " and energy loss: " << calc_eLoss; + + if (iterations > 100) { + // If we are not converging, we should probably throw an error. + throw std::runtime_error("Energy loss did not converge after 100 iterations."); + } + + double lastApproach = std::numeric_limits::max(); + bool approaching = true; + iterations++; + auto KE_initial = Kinematics::KE(fMom, fMass); + + while (true) { + XYZVector lastPos = fPos; + XYZVector lastMom = fMom; + LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + + RK4Step(); + + auto approach = (fPos - point).R(); + if (approach < lastApproach) { + // We are still approaching the measurement point + approaching = true; + lastApproach = approach; + continue; + } + + bool reachedMeasurementPoint = (approaching && approach > lastApproach); + bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; + if (reachedMeasurementPoint || particleStopped) { + // Last iteration we were still approaching the measurement point. Now we are further away + // then before. We have probably reached the measurement point if things are well behaved. + // I can think of cases where this will not be true. A better solution might be to run + // tracking the point of closest approach until the distance between the current state and + // the measurement point is larger than the distance between the last state and the measurement point. + + // Undo the last step since we were closer last time. + fPos = lastPos; + fMom = lastMom; + + double KE_final = Kinematics::KE(fMom, fMass); + calc_eLoss = KE_initial - KE_final; // Energy loss in MeV + scalingFactor *= eLoss / calc_eLoss; + LOG(info) << "------- End of RK4 interation " << iterations << " ---------"; + LOG(info) << "Particle stopped: " << particleStopped; + LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; + LOG(info) << "Last approach: " << lastApproach << " Current approach: " << approach; + LOG(info) << "Desired energy loss: " << eLoss << " MeV"; + LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; + LOG(info) << "Difference: " << calc_eLoss - eLoss << " MeV"; + LOG(info) << "New scaling factor: " << scalingFactor; + LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - return F_lorentz + drag; + if (eLoss == 0) + return; // If no energy loss is specified, we are done. + break; // Else rerun with adjusted scaling factor + } + } // End of loop over RK4 integration + } // End loop over energy loss convergence } } // namespace AtTools diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 58a2df0d3..066d894f4 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -30,7 +30,8 @@ class AtPropagator { // Internal state variables for the propagator double fH = 1e-10; /// Step size for propagation in s - double fETolerance = 1e-4; /// Energy tolerance for convergence when fixing energy loss + double fETol = 1e-4; /// Energy tolerance for convergence when fixing energy loss + double fStopTol = 0.01; /// Maximum kinetic energy to consider the particle stopped double fScalingFactor = 1.0; /// Scaling factor for energy loss XYZVector fPos; // Current position of the particle in mm @@ -87,6 +88,15 @@ class AtPropagator { * @return The force acting on the particle in N. */ XYZVector Force(XYZVector pos, XYZVector mom) const; + +protected: + /** + * @brief Perform a single RK4 step for propagation. + * + * This method performs a single Runge-Kutta 4th order step to propagate the particle's state. + * Updates fPos and fMom. + */ + void RK4Step(); }; } // namespace AtTools diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 56a6ba77f..f3c130187 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -1,10 +1,12 @@ #include "AtPropagator.h" +#include "AtELossTable.h" #include "AtKinematics.h" #include #include +#include #include using ROOT::Math::XYZVector; @@ -92,4 +94,86 @@ TEST(AtPropagatorTest, ForceBField) ASSERT_NEAR(force.X(), 0, 1e-12); ASSERT_NEAR(force.Y(), -5.09e-12, 1e-13); ASSERT_NEAR(force.Z(), 0, 1e-12); +} + +TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) +{ + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + auto elossModel = std::make_unique(0); + elossModel->LoadSrimTable( + "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + AtPropagator propagator(charge, mass, std::move(elossModel)); + + double KE = 1; // Kinetic energy in MeV + double E = KE + mass_p; + double p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c + XYZVector startPos(0, 0, 0); // Start position in mm + XYZVector startMom(p, 0, 0); // Start momentum in MeV/c + + propagator.SetState(startPos, startMom); + propagator.SetEField({0, 0, 0}); // No electric field + propagator.SetBField({0, 0, 0}); // No magnetic field + + ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); + + XYZVector targetPoint(1e3, 0, 0); // Target point to propagate to (1 m) + propagator.PropagateToPoint(targetPoint, KE); + + auto finalPos = propagator.GetPosition(); + auto finalMom = propagator.GetMomentum(); + + ASSERT_NEAR(finalPos.X(), 210, 10); // Final position in x-direction should be close to 210 mm + ASSERT_NEAR(finalMom.X(), 0, 0.1); + + KE = 0.5; + E = KE + mass_p; + p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c + startMom.SetXYZ(p, 0, 0); // Reset momentum + propagator.SetState(startPos, startMom); + + propagator.PropagateToPoint(targetPoint, KE); // Propagate to range + finalPos = propagator.GetPosition(); + finalMom = propagator.GetMomentum(); + ASSERT_NEAR(finalPos.X(), 68.6, 5); // Final position in x-direction should be close to 68.6 mm + ASSERT_NEAR(finalMom.X(), 0, 0.1); // Final momentum in x-direction should be close to 0 +} + +TEST(AtPropagatorTest, PropagateToPoint_NoField) +{ + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + auto elossModel = std::make_unique(0); + elossModel->LoadSrimTable( + "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + AtPropagator propagator(charge, mass, std::move(elossModel)); + + double KE = 1; // Kinetic energy in MeV + double E = KE + mass_p; + double p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c + XYZVector startPos(0, 0, 0); // Start position in mm + XYZVector startMom(p, 0, 0); // Start momentum in MeV/c + + double eLoss = 0.0285; // Expected energy loss in MeV (LISE) + double E_fin = KE - eLoss + mass_p; // Expected final energy after loss + double p_fin = std::sqrt(E_fin * E_fin - mass_p * mass_p); // Expected final momentum in MeV/c + + propagator.SetState(startPos, startMom); + propagator.SetEField({0, 0, 0}); // No electric field + propagator.SetBField({0, 0, 0}); // No magnetic field + + ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); + + std::cout << "STARTING PROPAGATION " << std::endl << std::endl; + + XYZVector targetPoint(10, 0, 0); // Target point to propagate to 10 mm + propagator.PropagateToPoint(targetPoint); + + std::cout << "FINISHING PROPAGATION " << std::endl << std::endl; + + auto finalPos = propagator.GetPosition(); + auto finalMom = propagator.GetMomentum(); + + ASSERT_NEAR(finalPos.X(), 10, 1); // Final position in x-direction should be close to 10 mm + ASSERT_NEAR(finalMom.X(), p_fin, 0.1); } \ No newline at end of file From 72e80879771cebe28fa8ff7ac51c4e1ddd5dcedc Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Mon, 21 Jul 2025 12:50:00 +0200 Subject: [PATCH 12/75] Update to using XYZPoint over XYZVector --- AtTools/AtPropagator.cxx | 10 ++++++---- AtTools/AtPropagator.h | 27 ++++++++++++++++++++++----- AtTools/AtPropagatorTest.cxx | 16 +++++++++------- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 3728335af..1d5f639e2 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -5,7 +5,7 @@ #include namespace AtTools { -AtPropagator::XYZVector AtPropagator::Force(XYZVector pos, XYZVector mom) const +AtPropagator::XYZVector AtPropagator::Force(XYZPoint pos, XYZVector mom) const { auto v = Kinematics::GetVel(mom, fMass); @@ -47,7 +47,9 @@ void AtPropagator::RK4Step() fPos = pos_SI * 1e3; // Convert back to mm } -void AtPropagator::PropagateToPoint(const XYZVector &point, double eLoss) +void AtPropagator::PropagateTo() {} + +void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss) { LOG(info) << "Propagating to point: " << point << " with eLoss: " << eLoss; @@ -70,8 +72,8 @@ void AtPropagator::PropagateToPoint(const XYZVector &point, double eLoss) auto KE_initial = Kinematics::KE(fMom, fMass); while (true) { - XYZVector lastPos = fPos; - XYZVector lastMom = fMom; + auto lastPos = fPos; + auto lastMom = fMom; LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 066d894f4..be30a50cd 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -3,6 +3,10 @@ #include "AtELossModel.h" +#include + +#include "Math/Plane3D.h" +#include "Math/Point3D.h" #include "Math/Vector3D.h" namespace AtTools { @@ -21,6 +25,9 @@ namespace AtTools { class AtPropagator { protected: using XYZVector = ROOT::Math::XYZVector; + using XYZPoint = ROOT::Math::XYZPoint; + using Plane3D = ROOT::Math::Plane3D; + using DistanceFunc = std::function; XYZVector fEField{0, 0, 0}; // Electric field vector XYZVector fBField{0, 0, 0}; // Magnetic field vector @@ -34,7 +41,7 @@ class AtPropagator { double fStopTol = 0.01; /// Maximum kinetic energy to consider the particle stopped double fScalingFactor = 1.0; /// Scaling factor for energy loss - XYZVector fPos; // Current position of the particle in mm + XYZPoint fPos; // Current position of the particle in mm XYZVector fMom; // Current momentum of the particle in MeV/c static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) @@ -61,12 +68,12 @@ class AtPropagator { * @param pos Position of the particle in mm. * @param mom Momentum of the particle in MeV/c. */ - void SetState(const XYZVector &pos, const XYZVector &mom) + void SetState(const XYZPoint &pos, const XYZVector &mom) { fPos = pos; fMom = mom; } - XYZVector GetPosition() const { return fPos; } + XYZPoint GetPosition() const { return fPos; } XYZVector GetMomentum() const { return fMom; } /** @@ -78,7 +85,15 @@ class AtPropagator { * @param point The point to approach. * @param eLoss If not 0, constrain the energy loss to this value (adjusting the stopping power). */ - void PropagateToPoint(const XYZVector &point, double eLoss = 0); + void PropagateToPoint(const XYZPoint &point, double eLoss = 0); + + /** + * @brief Propagate the particle to the given plane. + * + * Propagate the particle until it reaches the specified plane, adjusting the magnitude + * of the stopping power to ensure that a specific amount of energy is lost during the propagation. + */ + void PropagateToPlane(const Plane3D &plane, double eLoss = 0); /** * @brief Calculate the force acting on the particle. @@ -87,7 +102,7 @@ class AtPropagator { * @param mom Momentum of the particle in MeV/c. * @return The force acting on the particle in N. */ - XYZVector Force(XYZVector pos, XYZVector mom) const; + XYZVector Force(XYZPoint pos, XYZVector mom) const; protected: /** @@ -97,6 +112,8 @@ class AtPropagator { * Updates fPos and fMom. */ void RK4Step(); + + void PropagateTo(); }; } // namespace AtTools diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index f3c130187..6a48aedd8 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -8,6 +8,8 @@ #include #include #include +using ROOT::Math::Plane3D; +using ROOT::Math::XYZPoint; using ROOT::Math::XYZVector; using namespace AtTools; @@ -28,7 +30,7 @@ class DummyELossModel : public AtELossModel { TEST(AtPropagatorTest, ForceNoField) { - XYZVector pos(0, 0, 0); // Position in mm + XYZPoint pos(0, 0, 0); // Position in mm XYZVector mom(100, 0, 0); // Momentum in MeV/c double charge = charge_p; // Charge in Coulombs @@ -56,7 +58,7 @@ TEST(AtPropagatorTest, ForceNoField) TEST(AtPropagatorTest, ForceEField) { - XYZVector pos(0, 0, 0); // Position in mm + XYZPoint pos(0, 0, 0); // Position in mm XYZVector mom(100, 0, 0); // Momentum in MeV/c double charge = charge_p; // Charge in Coulombs double mass = mass_p; // Mass in MeV/c^2 @@ -77,7 +79,7 @@ TEST(AtPropagatorTest, ForceEField) TEST(AtPropagatorTest, ForceBField) { - XYZVector pos(0, 0, 0); // Position in mm + XYZPoint pos(0, 0, 0); // Position in mm XYZVector mom(100, 0, 0); // Momentum in MeV/c double charge = charge_p; // Charge in Coulombs double mass = mass_p; // Mass in MeV/c^2 @@ -108,7 +110,7 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) double KE = 1; // Kinetic energy in MeV double E = KE + mass_p; double p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c - XYZVector startPos(0, 0, 0); // Start position in mm + XYZPoint startPos(0, 0, 0); // Start position in mm XYZVector startMom(p, 0, 0); // Start momentum in MeV/c propagator.SetState(startPos, startMom); @@ -117,7 +119,7 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); - XYZVector targetPoint(1e3, 0, 0); // Target point to propagate to (1 m) + XYZPoint targetPoint(1e3, 0, 0); // Target point to propagate to (1 m) propagator.PropagateToPoint(targetPoint, KE); auto finalPos = propagator.GetPosition(); @@ -151,7 +153,7 @@ TEST(AtPropagatorTest, PropagateToPoint_NoField) double KE = 1; // Kinetic energy in MeV double E = KE + mass_p; double p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c - XYZVector startPos(0, 0, 0); // Start position in mm + XYZPoint startPos(0, 0, 0); // Start position in mm XYZVector startMom(p, 0, 0); // Start momentum in MeV/c double eLoss = 0.0285; // Expected energy loss in MeV (LISE) @@ -166,7 +168,7 @@ TEST(AtPropagatorTest, PropagateToPoint_NoField) std::cout << "STARTING PROPAGATION " << std::endl << std::endl; - XYZVector targetPoint(10, 0, 0); // Target point to propagate to 10 mm + XYZPoint targetPoint(10, 0, 0); // Target point to propagate to 10 mm propagator.PropagateToPoint(targetPoint); std::cout << "FINISHING PROPAGATION " << std::endl << std::endl; From 27ca18468a803fa5e881ef055c8d88372859643e Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Mon, 21 Jul 2025 12:53:41 +0200 Subject: [PATCH 13/75] Use internal scaling factor --- AtTools/AtPropagator.cxx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 1d5f639e2..0d9190f8e 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -53,12 +53,11 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss) { LOG(info) << "Propagating to point: " << point << " with eLoss: " << eLoss; - double scalingFactor = 1.0; int iterations = 0; double calc_eLoss = 0; while (std::abs(calc_eLoss - eLoss) > 1e-4 || eLoss == 0) { - LOG(debug) << "Running iteration " << iterations << " with scaling factor: " << scalingFactor + LOG(debug) << "Running iteration " << iterations << " with scaling factor: " << fScalingFactor << " and energy loss: " << calc_eLoss; if (iterations > 100) { @@ -102,7 +101,7 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss) double KE_final = Kinematics::KE(fMom, fMass); calc_eLoss = KE_initial - KE_final; // Energy loss in MeV - scalingFactor *= eLoss / calc_eLoss; + fScalingFactor *= eLoss / calc_eLoss; LOG(info) << "------- End of RK4 interation " << iterations << " ---------"; LOG(info) << "Particle stopped: " << particleStopped; LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; @@ -110,15 +109,19 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss) LOG(info) << "Desired energy loss: " << eLoss << " MeV"; LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; LOG(info) << "Difference: " << calc_eLoss - eLoss << " MeV"; - LOG(info) << "New scaling factor: " << scalingFactor; + LOG(info) << "New scaling factor: " << fScalingFactor; LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - if (eLoss == 0) - return; // If no energy loss is specified, we are done. - break; // Else rerun with adjusted scaling factor + if (eLoss == 0) { + fScalingFactor = 1; // Reset scaling factor after convergence + return; // If no energy loss is specified, we are done. + } + break; // Else rerun with adjusted scaling factor } } // End of loop over RK4 integration } // End loop over energy loss convergence + + fScalingFactor = 1; // Reset scaling factor after convergence } } // namespace AtTools From ef1dec0b342fd1e3e4f7574a0668720355d197b0 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 00:13:31 +0200 Subject: [PATCH 14/75] Fixed step propagator stopping in correct place --- AtTools/AtKinematics.cxx | 6 + AtTools/AtKinematics.h | 1 + AtTools/AtPropagator.cxx | 498 ++++++++++++++++++++++++++++++----- AtTools/AtPropagator.h | 104 +++++++- AtTools/AtPropagatorTest.cxx | 84 +++++- 5 files changed, 609 insertions(+), 84 deletions(-) diff --git a/AtTools/AtKinematics.cxx b/AtTools/AtKinematics.cxx index 755baf91c..3f9d416f7 100644 --- a/AtTools/AtKinematics.cxx +++ b/AtTools/AtKinematics.cxx @@ -303,6 +303,12 @@ double EtoA(double mass) { return mass / 931.5; } +double GetSpeed(double p, double mass) +{ + // Calculate the speed of a particle given its momentum and mass in m/s + double beta = GetBeta(p, mass); + return beta * fC; // Speed in m/s +} ROOT::Math::XYZVector GetVel(ROOT::Math::XYZVector mom, double mass) { return mom / Get4Vector(mom, mass).E() * fC; diff --git a/AtTools/AtKinematics.h b/AtTools/AtKinematics.h index 8be5c0111..622f347d9 100644 --- a/AtTools/AtKinematics.h +++ b/AtTools/AtKinematics.h @@ -76,6 +76,7 @@ double GetBeta(double p, double mass); double GetRelMom(double gamma, double mass); double AtoE(double Amu); double EtoA(double mass); +double GetSpeed(double p, double mass); /// Calculate the speed of a particle given its momentum and mass in m/s /** * Calculate the kinetic energy of a particle given its momentum and mass. diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 0d9190f8e..0d6bef446 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -3,6 +3,29 @@ #include "AtKinematics.h" #include + +// Butcher tableau (c, a_ij) and the two b vectors for Dormand–Prince 5(4) +// c1 = 0 +// Butcher tableau coefficients for Dormand–Prince 5(4) method + +static constexpr double c[7] = {0.0, 1.0 / 5.0, 3.0 / 10.0, 4.0 / 5.0, 8.0 / 9.0, 1.0, 1.0}; + +static constexpr double a[7][6] = { + {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + {1.0 / 5.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + {3.0 / 40.0, 9.0 / 40.0, 0.0, 0.0, 0.0, 0.0}, + {44.0 / 45.0, -56.0 / 15.0, 32.0 / 9.0, 0.0, 0.0, 0.0}, + {19372.0 / 6561.0, -25360.0 / 2187.0, 64448.0 / 6561.0, -212.0 / 729.0, 0.0, 0.0}, + {9017.0 / 3168.0, -355.0 / 33.0, 46732.0 / 5247.0, 49.0 / 176.0, -5103.0 / 18656.0, 0.0}, + {35.0 / 384.0, 0.0, 500.0 / 1113.0, 125.0 / 192.0, -2187.0 / 6784.0, 11.0 / 84.0}}; + +// b (5th-order) +static constexpr double b[7] = {35.0 / 384.0, 0.0, 500.0 / 1113.0, 125.0 / 192.0, -2187.0 / 6784.0, 11.0 / 84.0, 0.0}; + +// b* (4th-order, “star”) +static constexpr double bs[7] = {5179.0 / 57600.0, 0.0, 7571.0 / 16695.0, 393.0 / 640.0, -92097.0 / 339200.0, + 187.0 / 2100.0, 1.0 / 40.0}; + namespace AtTools { AtPropagator::XYZVector AtPropagator::Force(XYZPoint pos, XYZVector mom) const @@ -20,26 +43,241 @@ AtPropagator::XYZVector AtPropagator::Force(XYZPoint pos, XYZVector mom) const return F_lorentz + drag; // Force in N } -void AtPropagator::RK4Step() +AtPropagator::XYZVector AtPropagator::dpds(const XYZPoint &pos, const XYZVector &mom) const +{ + // Calculate the force acting on the particle at the given position and momentum + auto speed = Kinematics::GetSpeed(mom.R(), fMass); // Speed in m/s + return Force(pos, mom) / speed; +} +AtPropagator::XYZVector AtPropagator::d2xds2(const XYZPoint &pos, const XYZVector &mom) const +{ + auto phat = mom.Unit(); // Unit vector in the direction of momentum + auto p = mom.R(); // Magnitude of the momentum + auto dpds_vec = dpds(pos, mom); // Derivative of momentum w.r.t. arc length + + return 1 / p * (dpds_vec - phat * (phat.Dot(dpds_vec))); // Second derivative of position w.r.t. arc length +} + +void AtPropagator::PropagateToPointAdaptive(const XYZPoint &point) { - double h = fH; // Step size in seconds + LOG(info) << "Propagating to point: " << point; + + auto KE_initial = Kinematics::KE(fMom, fMass); + while (true) { + fLastPos = fPos; + fLastMom = fMom; + LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + + auto acceptedStep = RK4StepAdaptive(fH); + if (!acceptedStep) { + LOG(error) << "RK4 step failed, aborting propagation."; + return; // Abort propagation if step failed + } + + if (!acceptedStep || !ReachedPOCA(point)) + continue; + + bool reachedMeasurementPoint = ReachedPOCA(point); + bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; + if (reachedMeasurementPoint || particleStopped) { + // Last iteration we were still approaching the measurement point. Now we are further away + // then before. We have probably reached the measurement point if things are well behaved. + // I can think of cases where this will not be true. A better solution might be to run + // tracking the point of closest approach until the distance between the current state and + // the measurement point is larger than the distance between the last state and the measurement point. + + // Undo the last step since we were closer last time. + double lastApproach = (fLastPos - point).R(); + double approach = (fPos - point).R(); + fPos = fLastPos; + fMom = fLastMom; + + double KE_final = Kinematics::KE(fMom, fMass); + auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV + LOG(info) << "------- End of RK4 interation ---------"; + LOG(info) << "Particle stopped: " << particleStopped; + LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; + LOG(info) << "Last approach: " << lastApproach << " Current approach: " << approach; + LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; + LOG(info) << "Scaling factor: " << fScalingFactor; + LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + return; + } + } // End of loop over RK4 integration +} + +bool AtPropagator::RK4StepAdaptive(double &h) +{ + // Take h to be the step size in m. + // Use DP5(4) method for adaptive step size control. + + double atol_pos = 1e-2; // Absolute tolerance for position (mm) + double atol_mom = 1e-2; // Absolute tolerance for momentum (MeV/c) + double rtol = 1e-4; // Relative tolerance for both position and momentum - auto x_k1 = Kinematics::GetVel(fMom, fMass); - auto p_k1 = Force(fPos, fMom); + auto x0_mm = fPos; + auto p0 = fMom; + LOG(info) << "Starting RK4 step with initial position: " << x0_mm.X() << ", " << x0_mm.Y() << ", " << x0_mm.Z(); + LOG(info) << "Initial momentum: " << p0.X() << ", " << p0.Y() << ", " << p0.Z(); - auto x_k2 = Kinematics::GetVel(fMom + p_k1 * h / 2, fMass); - auto p_k2 = Force(fPos + x_k1 * h / 2, fMom + p_k1 * h / 2); + while (true) { + auto x_SI = fPos * 1e-3; // Convert position to SI units (m) + auto p_SI = fReltoSImom * fMom; // Convert momentum to SI units (kg m/s) + XYZVector kx[7]; // kx[i] will hold the position derivatives (unitless) + XYZVector kp[7]; // kp[i] will hold the momentum derivatives (SI units) - auto x_k3 = Kinematics::GetVel(fMom + p_k2 * h / 2, fMass); - auto p_k3 = Force(fPos + x_k2 * h / 2, fMom + p_k2 * h / 2); + // anonymous lambda to calculate and store the kx and kp values. Input is SI units. + auto calc_k = [&](const XYZPoint &x, const XYZVector &p, int i) { + kx[i] = p.Unit(); // The derivative of the position is then just the unit vector of the momentum. + kp[i] = dpds(x * 1e-3, p / fReltoSImom); // The derivative of the momentum is dpds. + }; - auto x_k4 = Kinematics::GetVel(fMom + p_k3 * h, fMass); - auto p_k4 = Force(fPos + x_k3 * h, fMom + p_k3 * h); + // anonymous lambda to calculate the position and momentum at the i-th stage + auto calc_xp = [&](int i) { + XYZVector dx(0, 0, 0); + XYZVector dp(0, 0, 0); + for (int j = 0; j < i; ++j) { + dx = dx + kx[j] * a[i][j]; + dp = dp + kp[j] * a[i][j]; + } + XYZPoint x = x_SI + dx * h; + XYZVector p = p_SI + dp * h; + return std::make_pair(x, p); + }; + + // Calculate kx and kp for each stage + // build stage 0 + calc_k(x_SI, p0, 0); + + // build stage 1 + auto [x1, p1] = calc_xp(1); + calc_k(x1, p1, 1); // k1 + + // build stage 2 + auto [x2, p2] = calc_xp(2); + calc_k(x2, p2, 2); // k2 + + // build stage 3 + auto [x3, p3] = calc_xp(3); + calc_k(x3, p3, 3); // k3 + + // build stage 4 + auto [x4, p4] = calc_xp(4); + calc_k(x4, p4, 4); // k4 + + // build stage 5 + auto [x5, p5] = calc_xp(5); + calc_k(x5, p5, 5); // k5 + + // build stage 6 + auto [x6, p6] = calc_xp(6); + calc_k(x6, p6, 6); // k6 + + // Calculate the new position and momentum using the 5th-order method + XYZVector dx(0, 0, 0); + XYZVector dp(0, 0, 0); + for (int i = 0; i < 7; ++i) { + dx = dx + kx[i] * b[i]; + dp = dp + kp[i] * b[i]; + } + XYZPoint x_new_5 = x_SI + dx * h; // New position in SI units (m) + XYZVector p_new_5 = p_SI + dp * h; // New momentum in SI units (kg m/s) + + // Calculate the new position and momentum using the 4th-order method + dx = XYZVector(0, 0, 0); + dp = XYZVector(0, 0, 0); + for (int i = 0; i < 7; ++i) { + dx = dx + kx[i] * bs[i]; + dp = dp + kp[i] * bs[i]; + } + XYZPoint x_new_4 = x_SI + dx * h; // New position in SI units (m) + XYZVector p_new_4 = p_SI + dp * h; // New momentum in SI units (kg m/s) - auto F_SI = (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) / 6; // Force in SI units (N) + auto x_4_mm = x_new_4 * 1e3; // Convert back to mm + auto p_4_MeV = p_new_4 / fReltoSImom; // Convert back to MeV/c + auto x_5_mm = x_new_5 * 1e3; // Convert back to mm + auto p_5_MeV = p_new_5 / fReltoSImom; // Convert back to MeV/c + LOG(info) << "New position (5th order): " << x_5_mm.X() << ", " << x_5_mm.Y() << ", " << x_5_mm.Z(); + LOG(info) << "New momentum (5th order): " << p_5_MeV.X() << ", " << p_5_MeV.Y() << ", " << p_5_MeV.Z(); + LOG(info) << "New position (4th order): " << x_4_mm.X() << ", " << x_4_mm.Y() << ", " << x_4_mm.Z(); + LOG(info) << "New momentum (4th order): " << p_4_MeV.X() << ", " << p_4_MeV.Y() << ", " << p_4_MeV.Z(); + + // Convert back to mm and MeV/c + XYZVector x_err = (x_5_mm - x_4_mm); // Error in position (mm) + XYZVector p_err = (p_5_MeV - p_4_MeV); // Error in momentum (MeV/c) + + // Calculate the overall error + double ex = x_err.X() / (atol_pos + rtol * std::abs(x_5_mm.X())); + double ey = x_err.Y() / (atol_pos + rtol * std::abs(x_5_mm.Y())); + double ez = x_err.Z() / (atol_pos + rtol * std::abs(x_5_mm.Z())); + + double ep_x = p_err.X() / (atol_mom + rtol * std::abs(p_5_MeV.X())); + double ep_y = p_err.Y() / (atol_mom + rtol * std::abs(p_5_MeV.Y())); + double ep_z = p_err.Z() / (atol_mom + rtol * std::abs(p_5_MeV.Z())); + + // Combine errors (norm) + double err = std::sqrt(ex * ex + ey * ey + ez * ez + ep_x * ep_x + ep_y * ep_y + ep_z * ep_z); + + double factor = std::pow(err, -1.0 / 5.0); // Adjust step size based on error + factor = std::clamp(factor, 0.25, 4.0); // Clamp factor to reasonable limits + double hNew = h * factor; + // We now know the local error at this point. Now we need to decide to accept the point or not. + if (err <= 1.0) { + // Accept the step + fPos = x_5_mm; // Update position in mm + fMom = p_5_MeV; // Update momentum in MeV/c + LOG(info) << "Accepted step with error: " << err; + LOG(info) << "Step size: " << h << " m"; + LOG(info) << "New step size: " << hNew << " m"; + LOG(info) << "New Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "New Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + + // Adjust the step size for the next iteration + h = hNew; + return true; // Step accepted + } else { + // Reject the step and reduce the step size + LOG(info) << "Rejected step with error: " << err; + LOG(info) << "Step size: " << h << " m"; + LOG(info) << "Reducing step size to: " << hNew << " m"; + + h = hNew; // Reduce step size + if (h < 1e-6) { + LOG(error) << "Step size too small, aborting propagation."; + return false; // Abort propagation if step size is too small + } + } + } +} + +void AtPropagator::RK4Step(double h) +{ + // Take h to be the step size in m. + + auto x_k1 = fMom.Unit(); // The derivative of the position is then just the unit vector of the momentum. + auto p_k1 = dpds(fPos, fMom); // The derivative of the momentum is dpds. + + auto x_2 = fPos + x_k1 * h / 2; // Position at the midpoint + auto p_2 = fMom + p_k1 * h / 2; // Momentum at the midpoint + auto x_k2 = p_2.Unit(); + auto p_k2 = dpds(x_2, p_2); + + auto x_3 = fPos + x_k2 * h / 2; // Position at the second midpoint + auto p_3 = fMom + p_k2 * h / 2; // Momentum at the second midpoint + auto x_k3 = p_3.Unit(); + auto p_k3 = dpds(x_3, p_3); + + auto x_4 = fPos + x_k3 * h; // Position at the end of the step + auto p_4 = fMom + p_k3 * h; // Momentum at the end of the step + auto x_k4 = p_4.Unit(); + auto p_k4 = dpds(x_4, p_4); + + auto dpds_SI = (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) / 6; // "Force" in SI units (N) auto mom_SI = fReltoSImom * fMom; - mom_SI += F_SI * h; // Update momentum in SI units (kg m/s) + mom_SI += dpds_SI * h; // Update momentum in SI units (kg m/s) fMom = mom_SI / fReltoSImom; // Convert back to auto pos_SI = fPos * 1e-3; // Convert position to SI units (m) @@ -47,16 +285,181 @@ void AtPropagator::RK4Step() fPos = pos_SI * 1e3; // Convert back to mm } -void AtPropagator::PropagateTo() {} +bool AtPropagator::ReachedPOCA(const XYZPoint &point) +{ + // Here we need to check if we are getting closer or further away from the POCA. + // We may walk right past it so need to look for a change in the sign of the derivative or + // something like that. + auto lastDeriv = (fLastPos - point).Dot(fLastMom.Unit()); // proportional missing constants + auto currDeriv = (fPos - point).Dot(fMom.Unit()); + LOG(debug) << "Last Derivative: " << lastDeriv << ", Current Derivative: " << currDeriv; + return lastDeriv * currDeriv < 0; + + auto lastApproach = (fLastPos - point).R(); + auto approach = (fPos - point).R(); + return (approach > lastApproach); +} + +bool AtPropagator::IntersectedPlane(const Plane3D &plane) +{ + // Check if the particle has crossed the plane this step. + auto prevSign = plane.Distance(fLastPos) > 0 ? 1 : -1; + auto currSign = plane.Distance(fPos) > 0 ? 1 : -1; + return (prevSign != currSign); +} + +void AtPropagator::PropagateToPlane(const Plane3D &plane) +{ + LOG(info) << "Propagating to plane: " << plane; + + auto KE_initial = Kinematics::KE(fMom, fMass); + while (true) { + fLastPos = fPos; + fLastMom = fMom; + LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + + RK4Step(fH); + + if (!IntersectedPlane(plane)) + continue; + + bool reachedMeasurementPoint = IntersectedPlane(plane); + bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; + if (reachedMeasurementPoint || particleStopped) { + // Last iteration we were still approaching the measurement point. Now we are further away + // then before. We have probably reached the measurement point if things are well behaved. + // I can think of cases where this will not be true. A better solution might be to run + // tracking the point of closest approach until the distance between the current state and + // the measurement point is larger than the distance between the last state and the measurement point. + + // Undo the last step since we were closer last time. + double lastApproach = plane.Distance(fLastPos); + double approach = plane.Distance(fPos); + fPos = fLastPos; + fMom = fLastMom; + + double KE_final = Kinematics::KE(fMom, fMass); + auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV + LOG(info) << "------- End of RK4 interation ---------"; + LOG(info) << "Particle stopped: " << particleStopped; + LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; + LOG(info) << "Last approach: " << lastApproach << " Current approach: " << approach; + LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; + LOG(info) << "Scaling factor: " << fScalingFactor; + LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + return; + } + } // End of loop over RK4 integration +} + +void AtPropagator::PropagateToPoint(const XYZPoint &point) +{ + LOG(info) << "Propagating to point: " << point; + + auto KE_initial = Kinematics::KE(fMom, fMass); + while (true) { + fLastPos = fPos; + fLastMom = fMom; + LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + + RK4Step(fH); + + if (!ReachedPOCA(point)) + continue; + + bool reachedMeasurementPoint = ReachedPOCA(point); + bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; + bool momentumReversed = (fLastMom.Dot(fMom) < 0); + + // If we stopped, then we should figure out about where we stopped assuming linear de/dx over this last step + if (particleStopped || momentumReversed) { + LOG(info) << "------ Particle stopped ------"; + + double finalH = (fPos - fLastPos).R(); // Distance traveled in the last step + double E_loss = + Kinematics::KE(fLastMom, fMass) + Kinematics::KE(fMom, fMass); // Energy loss in MeV in the last step + double dedx = E_loss / finalH; // Stopping power in MeV/mm + + LOG(info) << "Particle stopped with final step size: " << finalH << " mm"; + LOG(info) << "Energy loss in last step: " << E_loss << " MeV"; + LOG(info) << "Stopping power (dE/dx): " << dedx << " MeV/mm"; + LOG(info) << "Energy before stopping: " << Kinematics::KE(fLastMom, fMass) << " MeV"; + finalH = Kinematics::KE(fLastMom, fMass) / dedx; // Distance to stop in mm + LOG(info) << "Estimated distance to stop: " << finalH << " mm"; + + fPos = fLastPos; + fMom = fLastMom; // Reset to last position and momentum + RK4Step(finalH); // Propagate to the point where we stopped + fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped + fLastMom = fMom; + + LOG(info) << "Final Position after stopping: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Final Momentum after stopping: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + } + + if (reachedMeasurementPoint) { + // We reached the measurement point, so we should figure out how far we are from the measurement point + // and update that remaining amount + LOG(info) << "------ Reached measurement point------"; + double finalH = (fLastPos - fPos).R(); // Distance traveled in the last step + double approach = (fLastPos - point).R(); // Distance to the measurement point + LOG(info) << "Distance to measurement point: " << approach << " mm"; + LOG(info) << "Final step size: " << finalH << " mm"; + + finalH = approach * 1e-3; // Convert to meters for the RK4 step + fPos = fLastPos; + fMom = fLastMom; + RK4Step(finalH); // Propagate to the measurement point + } + + if (reachedMeasurementPoint || particleStopped) { + // Undo the last step since we were closer last time. + double lastApproach = (fLastPos - point).R(); + double approach = (fPos - point).R(); + + double KE_final = Kinematics::KE(fMom, fMass); + auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV + LOG(info) << "------- End of RK4 interation ---------"; + LOG(info) << "Particle stopped: " << particleStopped; + LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; + LOG(info) << "Last approach: " << lastApproach << " Current approach: " << approach; + LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; + LOG(info) << "Scaling factor: " << fScalingFactor; + LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + LOG(info) << "Last Position: " << fLastPos.X() << ", " << fLastPos.Y() << ", " << fLastPos.Z(); + LOG(info) << "Last Momentum: " << fLastMom.X() << ", " << fLastMom.Y() << ", " << fLastMom.Z(); + + // fPos = fLastPos; + // fMom = fLastMom; + return; + } + } // End of loop over RK4 integration +} void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss) { LOG(info) << "Propagating to point: " << point << " with eLoss: " << eLoss; + if (eLoss == 0) { + LOG(warn) << "No energy loss specified, propagating without energy loss adjustment."; + PropagateToPoint(point); + return; + } + int iterations = 0; double calc_eLoss = 0; + double KE_initial = Kinematics::KE(fMom, fMass); + auto initialMom = fMom; // Save initial momentum for energy loss calculation + auto initialPos = fPos; // Save initial position for energy loss calculation + + while (std::abs(calc_eLoss - eLoss) > 1e-4) { + fMom = initialMom; // Reset position and momentum to initial values for the next iteration + fPos = initialPos; - while (std::abs(calc_eLoss - eLoss) > 1e-4 || eLoss == 0) { LOG(debug) << "Running iteration " << iterations << " with scaling factor: " << fScalingFactor << " and energy loss: " << calc_eLoss; @@ -65,62 +468,21 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss) throw std::runtime_error("Energy loss did not converge after 100 iterations."); } - double lastApproach = std::numeric_limits::max(); - bool approaching = true; iterations++; - auto KE_initial = Kinematics::KE(fMom, fMass); - - while (true) { - auto lastPos = fPos; - auto lastMom = fMom; - LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - - RK4Step(); - - auto approach = (fPos - point).R(); - if (approach < lastApproach) { - // We are still approaching the measurement point - approaching = true; - lastApproach = approach; - continue; - } + PropagateToPoint(point); // Propagate without energy loss adjustment - bool reachedMeasurementPoint = (approaching && approach > lastApproach); - bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; - if (reachedMeasurementPoint || particleStopped) { - // Last iteration we were still approaching the measurement point. Now we are further away - // then before. We have probably reached the measurement point if things are well behaved. - // I can think of cases where this will not be true. A better solution might be to run - // tracking the point of closest approach until the distance between the current state and - // the measurement point is larger than the distance between the last state and the measurement point. - - // Undo the last step since we were closer last time. - fPos = lastPos; - fMom = lastMom; - - double KE_final = Kinematics::KE(fMom, fMass); - calc_eLoss = KE_initial - KE_final; // Energy loss in MeV - fScalingFactor *= eLoss / calc_eLoss; - LOG(info) << "------- End of RK4 interation " << iterations << " ---------"; - LOG(info) << "Particle stopped: " << particleStopped; - LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; - LOG(info) << "Last approach: " << lastApproach << " Current approach: " << approach; - LOG(info) << "Desired energy loss: " << eLoss << " MeV"; - LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; - LOG(info) << "Difference: " << calc_eLoss - eLoss << " MeV"; - LOG(info) << "New scaling factor: " << fScalingFactor; - LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - - if (eLoss == 0) { - fScalingFactor = 1; // Reset scaling factor after convergence - return; // If no energy loss is specified, we are done. - } - break; // Else rerun with adjusted scaling factor - } - } // End of loop over RK4 integration - } // End loop over energy loss convergence + double KE_final = Kinematics::KE(fMom, fMass); + calc_eLoss = KE_initial - KE_final; // Energy loss in MeV + fScalingFactor *= eLoss / calc_eLoss; + LOG(info) << "Desired energy loss: " << eLoss << " MeV"; + LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; + LOG(info) << "Difference: " << calc_eLoss - eLoss << " MeV"; + LOG(info) << "New scaling factor: " << fScalingFactor; + LOG(info) << "Condition: " << (std::abs(calc_eLoss - eLoss) > 1e-4); + + } // End loop over energy loss convergence + + LOG(info) << "Energy loss converged after " << iterations << " iterations."; fScalingFactor = 1; // Reset scaling factor after convergence } diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index be30a50cd..01b916fa1 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -36,14 +36,19 @@ class AtPropagator { const std::unique_ptr fELossModel; // Energy loss model // Internal state variables for the propagator - double fH = 1e-10; /// Step size for propagation in s + double fH = 1e-4; /// Step size for propagation in m + double fDelta = 1e-3; /// Relative error tolerance for adaptive step size. 10^-3 means each 1m of propagation + /// introduces at most 1mm of error. double fETol = 1e-4; /// Energy tolerance for convergence when fixing energy loss - double fStopTol = 0.01; /// Maximum kinetic energy to consider the particle stopped + double fStopTol = 0.01; /// Maximum kinetic energy to consider the particle stopped (MeV) double fScalingFactor = 1.0; /// Scaling factor for energy loss XYZPoint fPos; // Current position of the particle in mm XYZVector fMom; // Current momentum of the particle in MeV/c + XYZPoint fLastPos; + XYZVector fLastMom; + static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) public: @@ -56,6 +61,7 @@ class AtPropagator { */ void SetEField(const XYZVector &eField) { fEField = eField; } void SetEField(double ex, double ey, double ez) { fEField.SetXYZ(ex, ey, ez); } + /** * @brief Set the magnetic field (T) */ @@ -73,6 +79,10 @@ class AtPropagator { fPos = pos; fMom = mom; } + + void SetDelta(double delta) { fDelta = delta; } + void SetH(double h) { fH = h; } + XYZPoint GetPosition() const { return fPos; } XYZVector GetMomentum() const { return fMom; } @@ -83,17 +93,24 @@ class AtPropagator { * to ensure that a specific about of energy is lost during the propagation. * * @param point The point to approach. - * @param eLoss If not 0, constrain the energy loss to this value (adjusting the stopping power). + * @param eLoss If not 0, constrain the energy loss to this value by adjusting fScalingFactor. + */ + void PropagateToPoint(const XYZPoint &point, double eLoss); + + /** + * @brief Propagate the particle to the point of closest approach to the given point. + * + * @param point The point to approach. */ - void PropagateToPoint(const XYZPoint &point, double eLoss = 0); + void PropagateToPoint(const XYZPoint &point); + void PropagateToPointAdaptive(const XYZPoint &point); /** * @brief Propagate the particle to the given plane. * - * Propagate the particle until it reaches the specified plane, adjusting the magnitude - * of the stopping power to ensure that a specific amount of energy is lost during the propagation. + * @param plane The plane to approach. */ - void PropagateToPlane(const Plane3D &plane, double eLoss = 0); + void PropagateToPlane(const Plane3D &plane); /** * @brief Calculate the force acting on the particle. @@ -104,17 +121,86 @@ class AtPropagator { */ XYZVector Force(XYZPoint pos, XYZVector mom) const; + /** + * @brief Calculate the derivate of the momentum w.r.t. arc length. + * + * @param pos Position of the particle in mm. + * @param mom Momentum of the particle in MeV/c. + * @return The derivative of the momentum w.r.t. arc length in N/m. + */ + XYZVector dpds(const XYZPoint &pos, const XYZVector &mom) const; + + /** + * @brief Calculate the second derivative of the position w.r.t. arc length. + * + * \frac{d^2\vec{x}}{ds^2} = \frac{1}{p} \left( \frac{d\vec{p}}{ds} - \hat{p} (\hat{p} \cdot \frac{d\vec{p}}{ds}) + * \right) + * + * @param pos Position of the particle in mm. + * @param mom Momentum of the particle in MeV/c. + * @return The second derivative of the position w.r.t. arc length in m/m^2. + */ + XYZVector d2xds2(const XYZPoint &pos, const XYZVector &mom) const; + + XYZVector dxds(const XYZPoint &pos, const XYZVector &mom) const + { + return mom.Unit(); // The derivative of the position is just the unit vector of the momentum. + } + protected: /** * @brief Perform a single RK4 step for propagation. * * This method performs a single Runge-Kutta 4th order step to propagate the particle's state. * Updates fPos and fMom. + * @param h Step size for the RK4 step in meters. + */ + void RK4Step(double h); + + /** + * @brief Perform a single RK4 step using the Nystrom method. + * This method performs a single Runge-Kutta 4th order step using the Nystrom method + * to propagate the particle's state. Updates fPos and fMom. + * @param h Step size for the RK4 step in meters. */ - void RK4Step(); + void RK4StepNystrom(double h); - void PropagateTo(); + /** + * @brief Perform an adaptive RK4 step for propagation. + * This method performs an adaptive Runge-Kutta 4th order step to propagate the particle's state. + * Updates fPos and fMom. + * + * The error is based on the difference in the positions at the end of the step. i.e: + * \eps = \sqrt{\eps_x^2 + \eps_y^2 + \eps_z^2}, where + * \eps_x = 1/30*|x_1 - x_2|, where x_1 is using h/2 and x_2 is using h. + * + * Step size is adjust to ensure the local error is less than fDelta. + * + * @param h Step size for the RK4 step in seconds. Modified in place to reflect the new step size. + * @return True if the step was accepted, false otherwise. + */ + bool RK4StepAdaptive(double &h); + + void PropagateTo(DistanceFunc distanceFunc); + + bool ReachedPOCA(const XYZPoint &point); + bool IntersectedPlane(const Plane3D &plane); }; +static constexpr double a21 = 1.0 / 5.0; +static constexpr double a31 = 3.0 / 40.0, a32 = 9.0 / 40.0; +static constexpr double a41 = 44.0 / 45.0, a42 = -56.0 / 15.0, a43 = 32.0 / 9.0; +static constexpr double a51 = 19372.0 / 6561.0, a52 = -25360.0 / 2187.0, a53 = 64448.0 / 6561.0, a54 = -212.0 / 729.0; +static constexpr double a61 = 9017.0 / 3168.0, a62 = -355.0 / 33.0, a63 = 46732.0 / 5247.0, a64 = 49.0 / 176.0, + a65 = -5103.0 / 18656.0; +static constexpr double a71 = 35.0 / 384.0, a72 = 0.0, a73 = 500.0 / 1113.0, a74 = 125.0 / 192.0, + a75 = -2187.0 / 6784.0, a76 = 11.0 / 84.0; +// b (5th-order) +static constexpr double b1 = 35.0 / 384.0, b3 = 500.0 / 1113.0, b4 = 125.0 / 192.0, b5 = -2187.0 / 6784.0, + b6 = 11.0 / 84.0; +// b* (4th-order, “star”) +static constexpr double bs1 = 5179.0 / 57600.0, bs3 = 7571.0 / 16695.0, bs4 = 393.0 / 640.0, bs5 = -92097.0 / 339200.0, + bs6 = 187.0 / 2100.0, bs7 = 1.0 / 40.0; + } // namespace AtTools #endif // #ifndef ATPROPAGATOR_H diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 6a48aedd8..2e831836c 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -120,7 +120,7 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); XYZPoint targetPoint(1e3, 0, 0); // Target point to propagate to (1 m) - propagator.PropagateToPoint(targetPoint, KE); + propagator.PropagateToPoint(targetPoint); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); @@ -128,16 +128,16 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) ASSERT_NEAR(finalPos.X(), 210, 10); // Final position in x-direction should be close to 210 mm ASSERT_NEAR(finalMom.X(), 0, 0.1); - KE = 0.5; + KE = 0.75; E = KE + mass_p; p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c startMom.SetXYZ(p, 0, 0); // Reset momentum propagator.SetState(startPos, startMom); - propagator.PropagateToPoint(targetPoint, KE); // Propagate to range + propagator.PropagateToPoint(targetPoint); // Propagate to range finalPos = propagator.GetPosition(); finalMom = propagator.GetMomentum(); - ASSERT_NEAR(finalPos.X(), 68.6, 5); // Final position in x-direction should be close to 68.6 mm + ASSERT_NEAR(finalPos.X(), 130, 10); // Final position in x-direction should be close to 130 mm ASSERT_NEAR(finalMom.X(), 0, 0.1); // Final momentum in x-direction should be close to 0 } @@ -166,16 +166,86 @@ TEST(AtPropagatorTest, PropagateToPoint_NoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); - std::cout << "STARTING PROPAGATION " << std::endl << std::endl; - XYZPoint targetPoint(10, 0, 0); // Target point to propagate to 10 mm propagator.PropagateToPoint(targetPoint); - std::cout << "FINISHING PROPAGATION " << std::endl << std::endl; + auto finalPos = propagator.GetPosition(); + auto finalMom = propagator.GetMomentum(); + + ASSERT_NEAR(finalPos.X(), 10, 1); // Final position in x-direction should be close to 10 mm + ASSERT_NEAR(finalMom.X(), p_fin, 0.01); +} + +TEST(AtPropagatorTest, PropagateToPlane_NoField) +{ + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + auto elossModel = std::make_unique(0); + elossModel->LoadSrimTable( + "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + AtPropagator propagator(charge, mass, std::move(elossModel)); + + double KE = 1; // Kinetic energy in MeV + double E = KE + mass_p; + double p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c + XYZPoint startPos(0, 0, 0); // Start position in mm + XYZVector startMom(p, 0, 0); // Start momentum in MeV/c + + double eLoss = 0.0285; // Expected energy loss in MeV in 10 mm (LISE) + double E_fin = KE - eLoss + mass_p; // Expected final energy after loss + double p_fin = std::sqrt(E_fin * E_fin - mass_p * mass_p); // Expected final momentum in MeV/c + + propagator.SetState(startPos, startMom); + propagator.SetEField({0, 0, 0}); // No electric field + propagator.SetBField({0, 0, 0}); // No magnetic field + + ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); + + XYZPoint planePoint(10, 10, 10); // Target point to propagate to 10 mm + XYZVector planeNormal(1, 0, 0); // Normal vector of the plane in x-direction + Plane3D plane(planeNormal, planePoint); // Create the plane + propagator.PropagateToPlane(plane); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); ASSERT_NEAR(finalPos.X(), 10, 1); // Final position in x-direction should be close to 10 mm ASSERT_NEAR(finalMom.X(), p_fin, 0.1); +} + +TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) +{ + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + auto elossModel = std::make_unique(0); + elossModel->LoadSrimTable( + "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + AtPropagator propagator(charge, mass, std::move(elossModel)); + + double KE = 1; // Kinetic energy in MeV + double E = KE + mass_p; + double p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c + XYZPoint startPos(0, 0, 0); // Start position in mm + XYZVector startMom(p, 0, 0); // Start momentum in MeV/c + + double eLoss = 0.0285; // Expected energy loss in MeV in 10 mm (LISE) + double E_fin = KE - eLoss + mass_p; // Expected final energy after loss + double p_fin = std::sqrt(E_fin * E_fin - mass_p * mass_p); // Expected final momentum in MeV/c + + propagator.SetState(startPos, startMom); + propagator.SetEField({0, 0, 0}); // No electric field + propagator.SetBField({0, 0, 0}); // No magnetic field + propagator.SetDelta(1e-3); // Set relative error tolerance. Traveling 10 mm means at most 10 microns of error. + propagator.SetH(1); // Set initial step size to 1 s + + ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); + + XYZPoint targetPoint(10, 0, 0); // Target point to propagate to (10 mm) + propagator.PropagateToPointAdaptive(targetPoint); + + auto finalPos = propagator.GetPosition(); + auto finalMom = propagator.GetMomentum(); + + ASSERT_NEAR(finalPos.X(), 10, 10 * 1e-3); // Final position in x-direction should be close to 10 mm + ASSERT_NEAR(finalMom.X(), p_fin, 0.1); } \ No newline at end of file From 57ea952cbe149b33874c32511ffa136a337118af Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 01:22:10 +0200 Subject: [PATCH 15/75] Non-Adaptive tests working --- AtTools/AtPropagator.cxx | 107 +++++++++++++++++++++++++---------- AtTools/AtPropagator.h | 1 + AtTools/AtPropagatorTest.cxx | 33 +++++++++++ 3 files changed, 110 insertions(+), 31 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 0d6bef446..376588409 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -316,38 +316,83 @@ void AtPropagator::PropagateToPlane(const Plane3D &plane) while (true) { fLastPos = fPos; fLastMom = fMom; - LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + LOG(info) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); RK4Step(fH); - if (!IntersectedPlane(plane)) - continue; - bool reachedMeasurementPoint = IntersectedPlane(plane); bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; - if (reachedMeasurementPoint || particleStopped) { - // Last iteration we were still approaching the measurement point. Now we are further away - // then before. We have probably reached the measurement point if things are well behaved. - // I can think of cases where this will not be true. A better solution might be to run - // tracking the point of closest approach until the distance between the current state and - // the measurement point is larger than the distance between the last state and the measurement point. + bool momentumReversed = (fLastMom.Dot(fMom) < 0); - // Undo the last step since we were closer last time. - double lastApproach = plane.Distance(fLastPos); - double approach = plane.Distance(fPos); + if (reachedMeasurementPoint && !particleStopped && !momentumReversed) { + // We reached the measurement point, so we should figure out how far we are from the measurement point + LOG(info) << "------ Reached measurement point ------"; + double finalH = (fLastPos - fPos).R(); // Distance traveled in the last step + double approach = std::abs(plane.Distance(fLastPos)); + + LOG(info) << "Distance to plane: " << approach << " mm"; + LOG(info) << "Final step size: " << finalH << " mm"; + + finalH = approach * 1e-3; // Convert to meters for the RK4 step fPos = fLastPos; fMom = fLastMom; + RK4Step(finalH); // Propagate to the measurement point + } + + if (particleStopped || momentumReversed) { + // In this case the particle stopped before hitting the plane + // we should throw a warning to let the user know that there wasn't + // enough energy to reach the plane. + LOG(warning) << "------ Particle stopped before intersecting plane ------"; + + // Calculate how far to travel before stopping + double KE_last = Kinematics::KE(fLastMom, fMass); + double deltaE = KE_last - fStopTol; + deltaE = std::max(deltaE, 0.0); // Ensure we don't have negative energy loss + + LOG(info) << "Last KE: " << KE_last << " MeV"; + LOG(info) << "Energy to loose to stop: " << deltaE << " MeV"; + + double h_Stop = deltaE / fELossModel->GetdEdx(KE_last); // Distance to stop in mm + RK4Step(h_Stop); + LOG(info) << "Propagated to stopping point: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Energy after stopping: " << Kinematics::KE(fMom, fMass) << " MeV"; + + while (!IntersectedPlane(plane)) { + fScalingFactor = 0; // Turn off enregy loss. + + // If we still haven't intersected the plane, we need to adjust the step size + double h = std::abs(plane.Distance(fPos)); // Reduce step size so we hit the plane + if (h <= fDistTol) + break; + LOG(info) << "Propagating to plane after stopping with step size: " << h << " mm"; + RK4Step(h * 1e-3); // Convert mm to m for RK4 step + LOG(info) << "New position after adjusting step size: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + } + fLastMom = fMom; + fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped + reachedMeasurementPoint = true; + } + + if (reachedMeasurementPoint || particleStopped || momentumReversed) { + double distanceToPlane = std::abs(plane.Distance(fPos)); double KE_final = Kinematics::KE(fMom, fMass); auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV LOG(info) << "------- End of RK4 interation ---------"; LOG(info) << "Particle stopped: " << particleStopped; LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; - LOG(info) << "Last approach: " << lastApproach << " Current approach: " << approach; + LOG(info) << "Distance to plane: " << distanceToPlane << " mm"; LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; LOG(info) << "Scaling factor: " << fScalingFactor; LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + + // Project the position onto the plane. Cannot use ProjectOnPlane since it is templated in such + // a way that it can't separate our XYZPoint and its internal XYZPoint. + double d = plane.Distance(fPos); // Distance from the point to the plane + fPos = XYZPoint(fPos.X() - plane.A() * d, fPos.Y() - plane.B() * d, fPos.Z() - plane.C() * d); + LOG(info) << "Projected Position on plane: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); return; } @@ -374,6 +419,21 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point) bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; bool momentumReversed = (fLastMom.Dot(fMom) < 0); + if (reachedMeasurementPoint) { + // We reached the measurement point, so we should figure out how far we are from the measurement point + // and update that remaining amount + LOG(info) << "------ Reached measurement point------"; + double finalH = (fLastPos - fPos).R(); // Distance traveled in the last step + double approach = (fLastPos - point).R(); // Distance to the measurement point + LOG(info) << "Distance to measurement point: " << approach << " mm"; + LOG(info) << "Final step size: " << finalH << " mm"; + + finalH = approach * 1e-3; // Convert to meters for the RK4 step + fPos = fLastPos; + fMom = fLastMom; + RK4Step(finalH); // Propagate to the measurement point + } + // If we stopped, then we should figure out about where we stopped assuming linear de/dx over this last step if (particleStopped || momentumReversed) { LOG(info) << "------ Particle stopped ------"; @@ -400,22 +460,7 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point) LOG(info) << "Final Momentum after stopping: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); } - if (reachedMeasurementPoint) { - // We reached the measurement point, so we should figure out how far we are from the measurement point - // and update that remaining amount - LOG(info) << "------ Reached measurement point------"; - double finalH = (fLastPos - fPos).R(); // Distance traveled in the last step - double approach = (fLastPos - point).R(); // Distance to the measurement point - LOG(info) << "Distance to measurement point: " << approach << " mm"; - LOG(info) << "Final step size: " << finalH << " mm"; - - finalH = approach * 1e-3; // Convert to meters for the RK4 step - fPos = fLastPos; - fMom = fLastMom; - RK4Step(finalH); // Propagate to the measurement point - } - - if (reachedMeasurementPoint || particleStopped) { + if (reachedMeasurementPoint || particleStopped || momentumReversed) { // Undo the last step since we were closer last time. double lastApproach = (fLastPos - point).R(); double approach = (fPos - point).R(); diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 01b916fa1..0f38e8f75 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -41,6 +41,7 @@ class AtPropagator { /// introduces at most 1mm of error. double fETol = 1e-4; /// Energy tolerance for convergence when fixing energy loss double fStopTol = 0.01; /// Maximum kinetic energy to consider the particle stopped (MeV) + double fDistTol = 1e-2; /// Distance tolerance when considering positions equal. (mm) double fScalingFactor = 1.0; /// Scaling factor for energy loss XYZPoint fPos; // Current position of the particle in mm diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 2e831836c..10311e97a 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -213,6 +213,39 @@ TEST(AtPropagatorTest, PropagateToPlane_NoField) ASSERT_NEAR(finalMom.X(), p_fin, 0.1); } +TEST(AtPropagatorTest, PropagateToPlane_StoppingNoField) +{ + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + auto elossModel = std::make_unique(0); + elossModel->LoadSrimTable( + "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + AtPropagator propagator(charge, mass, std::move(elossModel)); + + double KE = 1; // Kinetic energy in MeV + double E = KE + mass_p; + double p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c + XYZPoint startPos(0, 0, 0); // Start position in mm + XYZVector startMom(p, 0, 0); // Start momentum in MeV/c + + propagator.SetState(startPos, startMom); + propagator.SetEField({0, 0, 0}); // No electric field + propagator.SetBField({0, 0, 0}); // No magnetic field + + ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); + + XYZPoint planePoint(220, 0, 0); // Target point to propagate to 215 mm + XYZVector planeNormal(1, 0, 0); // Normal vector of the plane in x-direction + Plane3D plane(planeNormal, planePoint); // Create the plane + propagator.PropagateToPlane(plane); + + auto finalPos = propagator.GetPosition(); + auto finalMom = propagator.GetMomentum(); + + ASSERT_NEAR(finalPos.X(), 220, 1); // Final position in x-direction should be close to 215 mm + ASSERT_NEAR(finalMom.X(), 0, 0.1); +} + TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) { double charge = charge_p; // Charge in Coulombs From fdee72ac5055506b3231548fe4b2ce025bbb34e6 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 01:31:02 +0200 Subject: [PATCH 16/75] Adaptive working --- AtTools/AtPropagator.cxx | 58 +++++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 376588409..78e795a13 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -80,18 +80,53 @@ void AtPropagator::PropagateToPointAdaptive(const XYZPoint &point) bool reachedMeasurementPoint = ReachedPOCA(point); bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; - if (reachedMeasurementPoint || particleStopped) { - // Last iteration we were still approaching the measurement point. Now we are further away - // then before. We have probably reached the measurement point if things are well behaved. - // I can think of cases where this will not be true. A better solution might be to run - // tracking the point of closest approach until the distance between the current state and - // the measurement point is larger than the distance between the last state and the measurement point. + bool momentumReversed = (fLastMom.Dot(fMom) < 0); + if (reachedMeasurementPoint) { + // We reached the measurement point, so we should figure out how far we are from the measurement point + // and update that remaining amount + LOG(info) << "------ Reached measurement point------"; + double finalH = (fLastPos - fPos).R(); // Distance traveled in the last step + double approach = (fLastPos - point).R(); // Distance to the measurement point + LOG(info) << "Distance to measurement point: " << approach << " mm"; + LOG(info) << "Final step size: " << finalH << " mm"; + + finalH = approach * 1e-3; // Convert to meters for the RK4 step + fPos = fLastPos; + fMom = fLastMom; + RK4StepAdaptive(finalH); // Propagate to the measurement point + } + + // If we stopped, then we should figure out about where we stopped assuming linear de/dx over this last step + if (particleStopped || momentumReversed) { + LOG(info) << "------ Particle stopped ------"; + + double finalH = (fPos - fLastPos).R(); // Distance traveled in the last step + double E_loss = + Kinematics::KE(fLastMom, fMass) + Kinematics::KE(fMom, fMass); // Energy loss in MeV in the last step + double dedx = E_loss / finalH; // Stopping power in MeV/mm + + LOG(info) << "Particle stopped with final step size: " << finalH << " mm"; + LOG(info) << "Energy loss in last step: " << E_loss << " MeV"; + LOG(info) << "Stopping power (dE/dx): " << dedx << " MeV/mm"; + LOG(info) << "Energy before stopping: " << Kinematics::KE(fLastMom, fMass) << " MeV"; + finalH = Kinematics::KE(fLastMom, fMass) / dedx; // Distance to stop in mm + LOG(info) << "Estimated distance to stop: " << finalH << " mm"; + + fPos = fLastPos; + fMom = fLastMom; // Reset to last position and momentum + RK4Step(finalH); // Propagate to the point where we stopped + fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped + fLastMom = fMom; + + LOG(info) << "Final Position after stopping: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Final Momentum after stopping: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + } + + if (reachedMeasurementPoint || particleStopped || momentumReversed) { // Undo the last step since we were closer last time. double lastApproach = (fLastPos - point).R(); double approach = (fPos - point).R(); - fPos = fLastPos; - fMom = fLastMom; double KE_final = Kinematics::KE(fMom, fMass); auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV @@ -103,6 +138,11 @@ void AtPropagator::PropagateToPointAdaptive(const XYZPoint &point) LOG(info) << "Scaling factor: " << fScalingFactor; LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + LOG(info) << "Last Position: " << fLastPos.X() << ", " << fLastPos.Y() << ", " << fLastPos.Z(); + LOG(info) << "Last Momentum: " << fLastMom.X() << ", " << fLastMom.Y() << ", " << fLastMom.Z(); + + // fPos = fLastPos; + // fMom = fLastMom; return; } } // End of loop over RK4 integration @@ -115,7 +155,7 @@ bool AtPropagator::RK4StepAdaptive(double &h) double atol_pos = 1e-2; // Absolute tolerance for position (mm) double atol_mom = 1e-2; // Absolute tolerance for momentum (MeV/c) - double rtol = 1e-4; // Relative tolerance for both position and momentum + double rtol = 1e-6; // Relative tolerance for both position and momentum auto x0_mm = fPos; auto p0 = fMom; From 8b38a50a1a2fe96f7085f69b145002511bd04f79 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 01:34:52 +0200 Subject: [PATCH 17/75] Add test for small initial step size --- AtTools/AtPropagatorTest.cxx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 10311e97a..813426116 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -268,7 +268,6 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) propagator.SetState(startPos, startMom); propagator.SetEField({0, 0, 0}); // No electric field propagator.SetBField({0, 0, 0}); // No magnetic field - propagator.SetDelta(1e-3); // Set relative error tolerance. Traveling 10 mm means at most 10 microns of error. propagator.SetH(1); // Set initial step size to 1 s ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); @@ -281,4 +280,19 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) ASSERT_NEAR(finalPos.X(), 10, 10 * 1e-3); // Final position in x-direction should be close to 10 mm ASSERT_NEAR(finalMom.X(), p_fin, 0.1); + + propagator.SetState(startPos, startMom); + propagator.SetEField({0, 0, 0}); // No electric field + propagator.SetBField({0, 0, 0}); // No magnetic field + propagator.SetH(1e-6); // Set initial step size to 1e-6 m + + ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); + + propagator.PropagateToPointAdaptive(targetPoint); + + finalPos = propagator.GetPosition(); + finalMom = propagator.GetMomentum(); + + ASSERT_NEAR(finalPos.X(), 10, 10 * 1e-3); // Final position in x-direction should be close to 10 mm + ASSERT_NEAR(finalMom.X(), p_fin, 0.1); } \ No newline at end of file From 6cda9957ad0b05bef18a2ca56f8a0b985737b688 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 11:21:42 +0200 Subject: [PATCH 18/75] Fix unit issue in stopping plane calculation. --- AtTools/AtPropagator.cxx | 28 ++++++++++++++------ AtTools/AtPropagator.h | 55 ++++++++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 78e795a13..bf4b3b431 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -26,6 +26,9 @@ static constexpr double b[7] = {35.0 / 384.0, 0.0, 500.0 / 1113.0, 125.0 / 192.0 static constexpr double bs[7] = {5179.0 / 57600.0, 0.0, 7571.0 / 16695.0, 393.0 / 640.0, -92097.0 / 339200.0, 187.0 / 2100.0, 1.0 / 40.0}; +using ROOT::Math::Plane3D; +using ROOT::Math::XYZPoint; +using ROOT::Math::XYZVector; namespace AtTools { AtPropagator::XYZVector AtPropagator::Force(XYZPoint pos, XYZVector mom) const @@ -299,18 +302,18 @@ void AtPropagator::RK4Step(double h) auto x_k1 = fMom.Unit(); // The derivative of the position is then just the unit vector of the momentum. auto p_k1 = dpds(fPos, fMom); // The derivative of the momentum is dpds. - auto x_2 = fPos + x_k1 * h / 2; // Position at the midpoint - auto p_2 = fMom + p_k1 * h / 2; // Momentum at the midpoint + auto x_2 = fPos + x_k1 * h / 2; // Position at the midpoint + auto p_2 = fMom + p_k1 * h / 2 / fReltoSImom; // Momentum at the midpoint auto x_k2 = p_2.Unit(); auto p_k2 = dpds(x_2, p_2); - auto x_3 = fPos + x_k2 * h / 2; // Position at the second midpoint - auto p_3 = fMom + p_k2 * h / 2; // Momentum at the second midpoint + auto x_3 = fPos + x_k2 * h / 2; // Position at the second midpoint + auto p_3 = fMom + p_k2 * h / 2 / fReltoSImom; // Momentum at the second midpoint auto x_k3 = p_3.Unit(); auto p_k3 = dpds(x_3, p_3); - auto x_4 = fPos + x_k3 * h; // Position at the end of the step - auto p_4 = fMom + p_k3 * h; // Momentum at the end of the step + auto x_4 = fPos + x_k3 * h; // Position at the end of the step + auto p_4 = fMom + p_k3 * h / fReltoSImom; // Momentum at the end of the step auto x_k4 = p_4.Unit(); auto p_k4 = dpds(x_4, p_4); @@ -393,9 +396,10 @@ void AtPropagator::PropagateToPlane(const Plane3D &plane) LOG(info) << "Last KE: " << KE_last << " MeV"; LOG(info) << "Energy to loose to stop: " << deltaE << " MeV"; - double h_Stop = deltaE / fELossModel->GetdEdx(KE_last); // Distance to stop in mm - RK4Step(h_Stop); + LOG(info) << "Estimated distance to stop: " << h_Stop << " mm"; + + RK4Step(h_Stop * 1e-3); LOG(info) << "Propagated to stopping point: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); LOG(info) << "Energy after stopping: " << Kinematics::KE(fMom, fMass) << " MeV"; @@ -571,4 +575,12 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss) fScalingFactor = 1; // Reset scaling factor after convergence } + +AtStepper::StepResult +AtRK4Stepper::Step(double h, const XYZPoint &fPos, const XYZVector &fMom, DerivFunc derivFunc) const +{ + // Take h to be the step size in m. + + return {}; +} } // namespace AtTools diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 0f38e8f75..1b1a29247 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -188,20 +188,47 @@ class AtPropagator { bool IntersectedPlane(const Plane3D &plane); }; -static constexpr double a21 = 1.0 / 5.0; -static constexpr double a31 = 3.0 / 40.0, a32 = 9.0 / 40.0; -static constexpr double a41 = 44.0 / 45.0, a42 = -56.0 / 15.0, a43 = 32.0 / 9.0; -static constexpr double a51 = 19372.0 / 6561.0, a52 = -25360.0 / 2187.0, a53 = 64448.0 / 6561.0, a54 = -212.0 / 729.0; -static constexpr double a61 = 9017.0 / 3168.0, a62 = -355.0 / 33.0, a63 = 46732.0 / 5247.0, a64 = 49.0 / 176.0, - a65 = -5103.0 / 18656.0; -static constexpr double a71 = 35.0 / 384.0, a72 = 0.0, a73 = 500.0 / 1113.0, a74 = 125.0 / 192.0, - a75 = -2187.0 / 6784.0, a76 = 11.0 / 84.0; -// b (5th-order) -static constexpr double b1 = 35.0 / 384.0, b3 = 500.0 / 1113.0, b4 = 125.0 / 192.0, b5 = -2187.0 / 6784.0, - b6 = 11.0 / 84.0; -// b* (4th-order, “star”) -static constexpr double bs1 = 5179.0 / 57600.0, bs3 = 7571.0 / 16695.0, bs4 = 393.0 / 640.0, bs5 = -92097.0 / 339200.0, - bs6 = 187.0 / 2100.0, bs7 = 1.0 / 40.0; +class AtStepper { +public: + struct StepResult { + ROOT::Math::XYZPoint pos; // Position of the particle in mm + ROOT::Math::XYZVector mom; // Momentum of the particle in MeV/c + ROOT::Math::XYZPoint lastPos; // Last position of the particle in mm + ROOT::Math::XYZVector lastMom; // Last momentum of the particle in MeV/c + double h; // Step size for the step in m + bool success; // Whether the step was successful + }; + /** + * @brief Function type defining the derivative of the position and momentum w.r.t. distance. + * + * This function takes the current position and momentum and returns the derivate of the position and momentum. + * + * @param pos Current position of the particle in mm. + * @param mom Current momentum of the particle in MeV/c. + * @return A pair containing the derivatives of the position and momentum in SI units (m and kg m/s). + * The first element is the derivative of the position, and the second element is the derivative + * of the momentum. + */ + using DerivFunc = std::function( + const ROOT::Math::XYZPoint &, const ROOT::Math::XYZVector &)>; + + virtual StepResult + Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom, DerivFunc derivFunc) const = 0; + +protected: + static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) +}; + +class AtRK4Stepper : public AtStepper { +public: + StepResult Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom, + DerivFunc derivFunc) const override; +}; +class AtRK4AdaptiveStepper : public AtStepper { +public: + StepResult Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom, + DerivFunc derivFunc) const override; +}; } // namespace AtTools #endif // #ifndef ATPROPAGATOR_H From 511e04fb6070249223a86bae1176162f63f93d2d Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 11:50:47 +0200 Subject: [PATCH 19/75] PropagateToPoint uses AtStepper interface --- AtTools/AtPropagator.cxx | 86 ++++++++++++++++++++++++++++++++----- AtTools/AtPropagator.h | 92 +++++++++++++++++++++++++--------------- 2 files changed, 132 insertions(+), 46 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index bf4b3b431..b4c04a483 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -448,13 +448,19 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point) LOG(info) << "Propagating to point: " << point; auto KE_initial = Kinematics::KE(fMom, fMass); + AtRK4Stepper stepper; + stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; + while (true) { - fLastPos = fPos; - fLastMom = fMom; LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - RK4Step(fH); + auto result = stepper.Step(fH, fPos, fMom); + if (!result.success) { + LOG(error) << "Integration step failed, aborting propagation."; + return; // Abort propagation if step failed + } + CopyFromState(result); // Copy the new state from the stepper if (!ReachedPOCA(point)) continue; @@ -463,7 +469,7 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point) bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; bool momentumReversed = (fLastMom.Dot(fMom) < 0); - if (reachedMeasurementPoint) { + if (reachedMeasurementPoint && !particleStopped && !momentumReversed) { // We reached the measurement point, so we should figure out how far we are from the measurement point // and update that remaining amount LOG(info) << "------ Reached measurement point------"; @@ -473,15 +479,42 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point) LOG(info) << "Final step size: " << finalH << " mm"; finalH = approach * 1e-3; // Convert to meters for the RK4 step - fPos = fLastPos; - fMom = fLastMom; - RK4Step(finalH); // Propagate to the measurement point + result = stepper.Step(finalH, fLastPos, fLastMom); + if (!result.success) { + LOG(error) << "Failed to propagate to measurement point, aborting."; + return; // Abort propagation if step failed + } + auto origH = fH; // Save original step size + CopyFromState(result); // Update position and momentum to the new state + fH = origH; // Restore original step size } // If we stopped, then we should figure out about where we stopped assuming linear de/dx over this last step if (particleStopped || momentumReversed) { - LOG(info) << "------ Particle stopped ------"; + LOG(info) << "------ Particle stopped before measurement point/surface------"; + + // Calculate how far to travel before stopping + double KE_last = Kinematics::KE(fLastMom, fMass); + double deltaE = KE_last - fStopTol; + deltaE = std::max(deltaE, 0.0); // Ensure we don't have negative energy loss + double h_Stop = deltaE / fELossModel->GetdEdx(KE_last); // Distance to stop in mm + + LOG(info) << "KE at last point: " << KE_last << " MeV"; + LOG(info) << "Energy to lose to stop: " << deltaE << " MeV"; + LOG(info) << "Estimated distance to stop: " << h_Stop << " mm"; + LOG(info) << "dE/dx at last point: " << fELossModel->GetdEdx(KE_last) << " MeV/mm"; + result = stepper.Step(h_Stop * 1e-3, fLastPos, fLastMom); + if (!result.success) { + LOG(error) << "Failed to propagate to stopping point, aborting."; + return; // Abort propagation if step failed + } + auto origH = fH; // Save original step size + CopyFromState(result); // Update position and momentum to the new state + fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped + fLastMom = fMom; // Update last momentum to zero + fH = origH; // Restore original step size + /* double finalH = (fPos - fLastPos).R(); // Distance traveled in the last step double E_loss = Kinematics::KE(fLastMom, fMass) + Kinematics::KE(fMom, fMass); // Energy loss in MeV in the last step @@ -499,6 +532,7 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point) RK4Step(finalH); // Propagate to the point where we stopped fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped fLastMom = fMom; + */ LOG(info) << "Final Position after stopping: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); LOG(info) << "Final Momentum after stopping: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); @@ -576,11 +610,41 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss) fScalingFactor = 1; // Reset scaling factor after convergence } -AtStepper::StepResult -AtRK4Stepper::Step(double h, const XYZPoint &fPos, const XYZVector &fMom, DerivFunc derivFunc) const +AtStepper::StepResult AtRK4Stepper::Step(double h, const XYZPoint &fPos, const XYZVector &fMom) const { // Take h to be the step size in m. + StepResult result; + result.lastPos = fPos; + result.lastMom = fMom; + result.h = h; + result.success = true; + + auto [x_k1, p_k1] = + fDeriv(fPos, fMom); // The derivative of the position is then just the unit vector of the momentum. + + auto x_2 = fPos + x_k1 * h / 2; // Position at the midpoint + auto p_2 = fMom + p_k1 * h / 2 / fReltoSImom; // Momentum at the midpoint + + auto [x_k2, p_k2] = fDeriv(x_2, p_2); // Derivative at the midpoint + + auto x_3 = fPos + x_k2 * h / 2; // Position at the second midpoint + auto p_3 = fMom + p_k2 * h / 2 / fReltoSImom; // Momentum at the second midpoint + auto [x_k3, p_k3] = fDeriv(x_3, p_3); + + auto x_4 = fPos + x_k3 * h; // Position at the end of the step + auto p_4 = fMom + p_k3 * h / fReltoSImom; // Momentum at the end of the step + auto [x_k4, p_k4] = fDeriv(x_4, p_4); + + auto dpds_SI = (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) / 6; // "Force" in SI units (N) + + auto mom_SI = fReltoSImom * fMom; + mom_SI += dpds_SI * h; // Update momentum in SI units (kg m/s) + result.mom = mom_SI / fReltoSImom; // Convert back to + + auto pos_SI = fPos * 1e-3; // Convert position to SI units (m) + pos_SI += (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) * h / 6; // Update position in SI units (m + result.pos = pos_SI * 1e3; // Convert back to mm - return {}; + return result; } } // namespace AtTools diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 1b1a29247..0cdbea65e 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -11,6 +11,38 @@ namespace AtTools { +class AtStepper { +public: + struct StepResult { + ROOT::Math::XYZPoint pos; // Position of the particle in mm + ROOT::Math::XYZVector mom; // Momentum of the particle in MeV/c + ROOT::Math::XYZPoint lastPos; // Last position of the particle in mm + ROOT::Math::XYZVector lastMom; // Last momentum of the particle in MeV/c + double h; // Step size for the step in m + bool success; // Whether the step was successful + }; + /** + * @brief Function type defining the derivative of the position and momentum w.r.t. distance. + * + * This function takes the current position and momentum and returns the derivate of the position and momentum. + * + * @param pos Current position of the particle in mm. + * @param mom Current momentum of the particle in MeV/c. + * @return A pair containing the derivatives of the position and momentum in SI units (m and kg m/s). + * The first element is the derivative of the position, and the second element is the derivative + * of the momentum. + */ + using DerivFunc = std::function( + const ROOT::Math::XYZPoint &, const ROOT::Math::XYZVector &)>; + + DerivFunc fDeriv; + + virtual StepResult Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const = 0; + +protected: + static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) +}; + /** * @brief Class for propagating particles through a medium. * @@ -148,6 +180,12 @@ class AtPropagator { return mom.Unit(); // The derivative of the position is just the unit vector of the momentum. } + std::pair + Derivatives(const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const + { + return {dxds(pos, mom), dpds(pos, mom)}; + } + protected: /** * @brief Perform a single RK4 step for propagation. @@ -186,48 +224,32 @@ class AtPropagator { bool ReachedPOCA(const XYZPoint &point); bool IntersectedPlane(const Plane3D &plane); -}; - -class AtStepper { -public: - struct StepResult { - ROOT::Math::XYZPoint pos; // Position of the particle in mm - ROOT::Math::XYZVector mom; // Momentum of the particle in MeV/c - ROOT::Math::XYZPoint lastPos; // Last position of the particle in mm - ROOT::Math::XYZVector lastMom; // Last momentum of the particle in MeV/c - double h; // Step size for the step in m - bool success; // Whether the step was successful - }; - /** - * @brief Function type defining the derivative of the position and momentum w.r.t. distance. - * - * This function takes the current position and momentum and returns the derivate of the position and momentum. - * - * @param pos Current position of the particle in mm. - * @param mom Current momentum of the particle in MeV/c. - * @return A pair containing the derivatives of the position and momentum in SI units (m and kg m/s). - * The first element is the derivative of the position, and the second element is the derivative - * of the momentum. - */ - using DerivFunc = std::function( - const ROOT::Math::XYZPoint &, const ROOT::Math::XYZVector &)>; - - virtual StepResult - Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom, DerivFunc derivFunc) const = 0; - -protected: - static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) + void CopyFromState(const AtStepper::StepResult &result) + { + fPos = result.pos; + fMom = result.mom; + fLastPos = result.lastPos; + fLastMom = result.lastMom; + fH = result.h; + } + AtStepper::StepResult CopyToState() const + { + AtStepper::StepResult result; + result.pos = fPos; + result.mom = fMom; + result.lastPos = fLastPos; + result.lastMom = fLastMom; + return result; + } }; class AtRK4Stepper : public AtStepper { public: - StepResult Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom, - DerivFunc derivFunc) const override; + StepResult Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const override; }; class AtRK4AdaptiveStepper : public AtStepper { public: - StepResult Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom, - DerivFunc derivFunc) const override; + StepResult Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const override; }; } // namespace AtTools From a848c2b6c3b24468622a6cf235093e3c8f4197cd Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 12:07:41 +0200 Subject: [PATCH 20/75] All RK4 to point tests use stepper interface --- AtTools/AtPropagator.cxx | 182 ++++++++++++++++++++++++++++++----- AtTools/AtPropagator.h | 5 +- AtTools/AtPropagatorTest.cxx | 8 +- 3 files changed, 166 insertions(+), 29 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index b4c04a483..4a21f0919 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -443,12 +443,11 @@ void AtPropagator::PropagateToPlane(const Plane3D &plane) } // End of loop over RK4 integration } -void AtPropagator::PropagateToPoint(const XYZPoint &point) +void AtPropagator::PropagateToPoint(const XYZPoint &point, AtStepper &stepper) { LOG(info) << "Propagating to point: " << point; auto KE_initial = Kinematics::KE(fMom, fMass); - AtRK4Stepper stepper; stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; while (true) { @@ -514,25 +513,6 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point) fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped fLastMom = fMom; // Update last momentum to zero fH = origH; // Restore original step size - /* - double finalH = (fPos - fLastPos).R(); // Distance traveled in the last step - double E_loss = - Kinematics::KE(fLastMom, fMass) + Kinematics::KE(fMom, fMass); // Energy loss in MeV in the last step - double dedx = E_loss / finalH; // Stopping power in MeV/mm - - LOG(info) << "Particle stopped with final step size: " << finalH << " mm"; - LOG(info) << "Energy loss in last step: " << E_loss << " MeV"; - LOG(info) << "Stopping power (dE/dx): " << dedx << " MeV/mm"; - LOG(info) << "Energy before stopping: " << Kinematics::KE(fLastMom, fMass) << " MeV"; - finalH = Kinematics::KE(fLastMom, fMass) / dedx; // Distance to stop in mm - LOG(info) << "Estimated distance to stop: " << finalH << " mm"; - - fPos = fLastPos; - fMom = fLastMom; // Reset to last position and momentum - RK4Step(finalH); // Propagate to the point where we stopped - fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped - fLastMom = fMom; - */ LOG(info) << "Final Position after stopping: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); LOG(info) << "Final Momentum after stopping: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); @@ -563,13 +543,13 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point) } // End of loop over RK4 integration } -void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss) +void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss, AtStepper &stepper) { LOG(info) << "Propagating to point: " << point << " with eLoss: " << eLoss; if (eLoss == 0) { LOG(warn) << "No energy loss specified, propagating without energy loss adjustment."; - PropagateToPoint(point); + PropagateToPoint(point, stepper); return; } @@ -592,7 +572,7 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss) } iterations++; - PropagateToPoint(point); // Propagate without energy loss adjustment + PropagateToPoint(point, stepper); // Propagate without energy loss adjustment double KE_final = Kinematics::KE(fMom, fMass); calc_eLoss = KE_initial - KE_final; // Energy loss in MeV @@ -647,4 +627,158 @@ AtStepper::StepResult AtRK4Stepper::Step(double h, const XYZPoint &fPos, const X return result; } + +AtStepper::StepResult AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, const XYZVector &fMom) const +{ + // Take h to be the step size in m. + StepResult result; + result.lastPos = fPos; + result.lastMom = fMom; + result.h = h; + result.success = true; + + // Take h to be the step size in m. + // Use DP5(4) method for adaptive step size control. + + double atol_pos = 1e-2; // Absolute tolerance for position (mm) + double atol_mom = 1e-2; // Absolute tolerance for momentum (MeV/c) + double rtol = 1e-6; // Relative tolerance for both position and momentum + + auto x0_mm = fPos; + auto p0 = fMom; + LOG(info) << "Starting RK4 step with initial position: " << x0_mm.X() << ", " << x0_mm.Y() << ", " << x0_mm.Z(); + LOG(info) << "Initial momentum: " << p0.X() << ", " << p0.Y() << ", " << p0.Z(); + + while (true) { + auto x_SI = fPos * 1e-3; // Convert position to SI units (m) + auto p_SI = fReltoSImom * fMom; // Convert momentum to SI units (kg m/s) + XYZVector kx[7]; // kx[i] will hold the position derivatives (unitless) + XYZVector kp[7]; // kp[i] will hold the momentum derivatives (SI units) + + // anonymous lambda to calculate and store the kx and kp values. Input is SI units. + auto calc_k = [&](const XYZPoint &x, const XYZVector &p, int i) { + auto [k_x, k_p] = fDeriv(x * 1e-3, p / fReltoSImom); + kx[i] = k_x; // Store the position derivative (unitless) + kp[i] = k_p; // Store the momentum derivative (SI units) + }; + + // anonymous lambda to calculate the position and momentum at the i-th stage + auto calc_xp = [&](int i) { + XYZVector dx(0, 0, 0); + XYZVector dp(0, 0, 0); + for (int j = 0; j < i; ++j) { + dx = dx + kx[j] * a[i][j]; + dp = dp + kp[j] * a[i][j]; + } + XYZPoint x = x_SI + dx * h; + XYZVector p = p_SI + dp * h; + return std::make_pair(x, p); + }; + + // Calculate kx and kp for each stage + // build stage 0 + calc_k(x_SI, p0, 0); + + // build stage 1 + auto [x1, p1] = calc_xp(1); + calc_k(x1, p1, 1); // k1 + + // build stage 2 + auto [x2, p2] = calc_xp(2); + calc_k(x2, p2, 2); // k2 + + // build stage 3 + auto [x3, p3] = calc_xp(3); + calc_k(x3, p3, 3); // k3 + + // build stage 4 + auto [x4, p4] = calc_xp(4); + calc_k(x4, p4, 4); // k4 + + // build stage 5 + auto [x5, p5] = calc_xp(5); + calc_k(x5, p5, 5); // k5 + + // build stage 6 + auto [x6, p6] = calc_xp(6); + calc_k(x6, p6, 6); // k6 + + // Calculate the new position and momentum using the 5th-order method + XYZVector dx(0, 0, 0); + XYZVector dp(0, 0, 0); + for (int i = 0; i < 7; ++i) { + dx = dx + kx[i] * b[i]; + dp = dp + kp[i] * b[i]; + } + XYZPoint x_new_5 = x_SI + dx * h; // New position in SI units (m) + XYZVector p_new_5 = p_SI + dp * h; // New momentum in SI units (kg m/s) + + // Calculate the new position and momentum using the 4th-order method + dx = XYZVector(0, 0, 0); + dp = XYZVector(0, 0, 0); + for (int i = 0; i < 7; ++i) { + dx = dx + kx[i] * bs[i]; + dp = dp + kp[i] * bs[i]; + } + XYZPoint x_new_4 = x_SI + dx * h; // New position in SI units (m) + XYZVector p_new_4 = p_SI + dp * h; // New momentum in SI units (kg m/s) + + auto x_4_mm = x_new_4 * 1e3; // Convert back to mm + auto p_4_MeV = p_new_4 / fReltoSImom; // Convert back to MeV/c + auto x_5_mm = x_new_5 * 1e3; // Convert back to mm + auto p_5_MeV = p_new_5 / fReltoSImom; // Convert back to MeV/c + LOG(info) << "New position (5th order): " << x_5_mm.X() << ", " << x_5_mm.Y() << ", " << x_5_mm.Z(); + LOG(info) << "New momentum (5th order): " << p_5_MeV.X() << ", " << p_5_MeV.Y() << ", " << p_5_MeV.Z(); + LOG(info) << "New position (4th order): " << x_4_mm.X() << ", " << x_4_mm.Y() << ", " << x_4_mm.Z(); + LOG(info) << "New momentum (4th order): " << p_4_MeV.X() << ", " << p_4_MeV.Y() << ", " << p_4_MeV.Z(); + + // Convert back to mm and MeV/c + XYZVector x_err = (x_5_mm - x_4_mm); // Error in position (mm) + XYZVector p_err = (p_5_MeV - p_4_MeV); // Error in momentum (MeV/c) + + // Calculate the overall error + double ex = x_err.X() / (atol_pos + rtol * std::abs(x_5_mm.X())); + double ey = x_err.Y() / (atol_pos + rtol * std::abs(x_5_mm.Y())); + double ez = x_err.Z() / (atol_pos + rtol * std::abs(x_5_mm.Z())); + + double ep_x = p_err.X() / (atol_mom + rtol * std::abs(p_5_MeV.X())); + double ep_y = p_err.Y() / (atol_mom + rtol * std::abs(p_5_MeV.Y())); + double ep_z = p_err.Z() / (atol_mom + rtol * std::abs(p_5_MeV.Z())); + + // Combine errors (norm) + double err = std::sqrt(ex * ex + ey * ey + ez * ez + ep_x * ep_x + ep_y * ep_y + ep_z * ep_z); + + double factor = std::pow(err, -1.0 / 5.0); // Adjust step size based on error + factor = std::clamp(factor, 0.25, 4.0); // Clamp factor to reasonable limits + double hNew = h * factor; + // We now know the local error at this point. Now we need to decide to accept the point or not. + if (err <= 1.0) { + // Accept the step + result.pos = x_5_mm; // Update position in mm + result.mom = p_5_MeV; // Update momentum in MeV/c + LOG(info) << "Accepted step with error: " << err; + LOG(info) << "Step size: " << h << " m"; + LOG(info) << "New step size: " << hNew << " m"; + LOG(info) << "New Position: " << result.pos.X() << ", " << result.pos.Y() << ", " << result.pos.Z(); + LOG(info) << "New Momentum: " << result.mom.X() << ", " << result.mom.Y() << ", " << result.mom.Z(); + + // Adjust the step size for the next iteration + result.h = hNew; + result.success = true; // Step accepted + return result; + } else { + // Reject the step and reduce the step size + LOG(info) << "Rejected step with error: " << err; + LOG(info) << "Step size: " << h << " m"; + LOG(info) << "Reducing step size to: " << hNew << " m"; + + result.h = hNew; // Reduce step size + if (result.h < 1e-6) { + LOG(error) << "Step size too small, aborting propagation."; + result.success = false; + return result; // Abort propagation if step size is too small + } + } + } +} } // namespace AtTools diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 0cdbea65e..78e1f2694 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -128,14 +128,15 @@ class AtPropagator { * @param point The point to approach. * @param eLoss If not 0, constrain the energy loss to this value by adjusting fScalingFactor. */ - void PropagateToPoint(const XYZPoint &point, double eLoss); + void PropagateToPoint(const XYZPoint &point, double eLoss, AtStepper &stepper); /** * @brief Propagate the particle to the point of closest approach to the given point. * * @param point The point to approach. + * @param stepper The stepper to use for propagation. */ - void PropagateToPoint(const XYZPoint &point); + void PropagateToPoint(const XYZPoint &point, AtStepper &stepper); void PropagateToPointAdaptive(const XYZPoint &point); /** diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 813426116..5cf21a84a 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -106,6 +106,7 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) elossModel->LoadSrimTable( "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); AtPropagator propagator(charge, mass, std::move(elossModel)); + AtRK4Stepper stepper; double KE = 1; // Kinetic energy in MeV double E = KE + mass_p; @@ -120,7 +121,7 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); XYZPoint targetPoint(1e3, 0, 0); // Target point to propagate to (1 m) - propagator.PropagateToPoint(targetPoint); + propagator.PropagateToPoint(targetPoint, stepper); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); @@ -134,7 +135,7 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) startMom.SetXYZ(p, 0, 0); // Reset momentum propagator.SetState(startPos, startMom); - propagator.PropagateToPoint(targetPoint); // Propagate to range + propagator.PropagateToPoint(targetPoint, stepper); // Propagate to range finalPos = propagator.GetPosition(); finalMom = propagator.GetMomentum(); ASSERT_NEAR(finalPos.X(), 130, 10); // Final position in x-direction should be close to 130 mm @@ -149,6 +150,7 @@ TEST(AtPropagatorTest, PropagateToPoint_NoField) elossModel->LoadSrimTable( "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); AtPropagator propagator(charge, mass, std::move(elossModel)); + AtRK4Stepper stepper; double KE = 1; // Kinetic energy in MeV double E = KE + mass_p; @@ -167,7 +169,7 @@ TEST(AtPropagatorTest, PropagateToPoint_NoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); XYZPoint targetPoint(10, 0, 0); // Target point to propagate to 10 mm - propagator.PropagateToPoint(targetPoint); + propagator.PropagateToPoint(targetPoint, stepper); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); From 08c7ccba2b4f86515ab3211c018cb28b0938dd75 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 12:12:16 +0200 Subject: [PATCH 21/75] Adaptive test working with stepper --- AtTools/AtPropagator.cxx | 1 + AtTools/AtPropagatorTest.cxx | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 4a21f0919..7a070a64a 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -773,6 +773,7 @@ AtStepper::StepResult AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, LOG(info) << "Reducing step size to: " << hNew << " m"; result.h = hNew; // Reduce step size + h = hNew; // Update the step size for the next iteration if (result.h < 1e-6) { LOG(error) << "Step size too small, aborting propagation."; result.success = false; diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 5cf21a84a..55b1321a7 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -256,6 +256,7 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) elossModel->LoadSrimTable( "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); AtPropagator propagator(charge, mass, std::move(elossModel)); + AtRK4AdaptiveStepper stepper; double KE = 1; // Kinetic energy in MeV double E = KE + mass_p; @@ -275,7 +276,8 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); XYZPoint targetPoint(10, 0, 0); // Target point to propagate to (10 mm) - propagator.PropagateToPointAdaptive(targetPoint); + // propagator.PropagateToPointAdaptive(targetPoint); + propagator.PropagateToPoint(targetPoint, stepper); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); @@ -290,7 +292,8 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); - propagator.PropagateToPointAdaptive(targetPoint); + // propagator.PropagateToPointAdaptive(targetPoint); + propagator.PropagateToPoint(targetPoint, stepper); finalPos = propagator.GetPosition(); finalMom = propagator.GetMomentum(); From 365e2114c5b066ac17f95db474697748f534f017 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 12:29:21 +0200 Subject: [PATCH 22/75] Plane propagation uses AtStepper interface --- AtTools/AtPropagator.cxx | 327 +++++------------------------------ AtTools/AtPropagator.h | 39 +---- AtTools/AtPropagatorTest.cxx | 8 +- 3 files changed, 50 insertions(+), 324 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 7a070a64a..53a7c792b 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -61,273 +61,6 @@ AtPropagator::XYZVector AtPropagator::d2xds2(const XYZPoint &pos, const XYZVecto return 1 / p * (dpds_vec - phat * (phat.Dot(dpds_vec))); // Second derivative of position w.r.t. arc length } -void AtPropagator::PropagateToPointAdaptive(const XYZPoint &point) -{ - LOG(info) << "Propagating to point: " << point; - - auto KE_initial = Kinematics::KE(fMom, fMass); - while (true) { - fLastPos = fPos; - fLastMom = fMom; - LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - - auto acceptedStep = RK4StepAdaptive(fH); - if (!acceptedStep) { - LOG(error) << "RK4 step failed, aborting propagation."; - return; // Abort propagation if step failed - } - - if (!acceptedStep || !ReachedPOCA(point)) - continue; - - bool reachedMeasurementPoint = ReachedPOCA(point); - bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; - bool momentumReversed = (fLastMom.Dot(fMom) < 0); - - if (reachedMeasurementPoint) { - // We reached the measurement point, so we should figure out how far we are from the measurement point - // and update that remaining amount - LOG(info) << "------ Reached measurement point------"; - double finalH = (fLastPos - fPos).R(); // Distance traveled in the last step - double approach = (fLastPos - point).R(); // Distance to the measurement point - LOG(info) << "Distance to measurement point: " << approach << " mm"; - LOG(info) << "Final step size: " << finalH << " mm"; - - finalH = approach * 1e-3; // Convert to meters for the RK4 step - fPos = fLastPos; - fMom = fLastMom; - RK4StepAdaptive(finalH); // Propagate to the measurement point - } - - // If we stopped, then we should figure out about where we stopped assuming linear de/dx over this last step - if (particleStopped || momentumReversed) { - LOG(info) << "------ Particle stopped ------"; - - double finalH = (fPos - fLastPos).R(); // Distance traveled in the last step - double E_loss = - Kinematics::KE(fLastMom, fMass) + Kinematics::KE(fMom, fMass); // Energy loss in MeV in the last step - double dedx = E_loss / finalH; // Stopping power in MeV/mm - - LOG(info) << "Particle stopped with final step size: " << finalH << " mm"; - LOG(info) << "Energy loss in last step: " << E_loss << " MeV"; - LOG(info) << "Stopping power (dE/dx): " << dedx << " MeV/mm"; - LOG(info) << "Energy before stopping: " << Kinematics::KE(fLastMom, fMass) << " MeV"; - finalH = Kinematics::KE(fLastMom, fMass) / dedx; // Distance to stop in mm - LOG(info) << "Estimated distance to stop: " << finalH << " mm"; - - fPos = fLastPos; - fMom = fLastMom; // Reset to last position and momentum - RK4Step(finalH); // Propagate to the point where we stopped - fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped - fLastMom = fMom; - - LOG(info) << "Final Position after stopping: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "Final Momentum after stopping: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - } - - if (reachedMeasurementPoint || particleStopped || momentumReversed) { - // Undo the last step since we were closer last time. - double lastApproach = (fLastPos - point).R(); - double approach = (fPos - point).R(); - - double KE_final = Kinematics::KE(fMom, fMass); - auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV - LOG(info) << "------- End of RK4 interation ---------"; - LOG(info) << "Particle stopped: " << particleStopped; - LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; - LOG(info) << "Last approach: " << lastApproach << " Current approach: " << approach; - LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; - LOG(info) << "Scaling factor: " << fScalingFactor; - LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - LOG(info) << "Last Position: " << fLastPos.X() << ", " << fLastPos.Y() << ", " << fLastPos.Z(); - LOG(info) << "Last Momentum: " << fLastMom.X() << ", " << fLastMom.Y() << ", " << fLastMom.Z(); - - // fPos = fLastPos; - // fMom = fLastMom; - return; - } - } // End of loop over RK4 integration -} - -bool AtPropagator::RK4StepAdaptive(double &h) -{ - // Take h to be the step size in m. - // Use DP5(4) method for adaptive step size control. - - double atol_pos = 1e-2; // Absolute tolerance for position (mm) - double atol_mom = 1e-2; // Absolute tolerance for momentum (MeV/c) - double rtol = 1e-6; // Relative tolerance for both position and momentum - - auto x0_mm = fPos; - auto p0 = fMom; - LOG(info) << "Starting RK4 step with initial position: " << x0_mm.X() << ", " << x0_mm.Y() << ", " << x0_mm.Z(); - LOG(info) << "Initial momentum: " << p0.X() << ", " << p0.Y() << ", " << p0.Z(); - - while (true) { - auto x_SI = fPos * 1e-3; // Convert position to SI units (m) - auto p_SI = fReltoSImom * fMom; // Convert momentum to SI units (kg m/s) - XYZVector kx[7]; // kx[i] will hold the position derivatives (unitless) - XYZVector kp[7]; // kp[i] will hold the momentum derivatives (SI units) - - // anonymous lambda to calculate and store the kx and kp values. Input is SI units. - auto calc_k = [&](const XYZPoint &x, const XYZVector &p, int i) { - kx[i] = p.Unit(); // The derivative of the position is then just the unit vector of the momentum. - kp[i] = dpds(x * 1e-3, p / fReltoSImom); // The derivative of the momentum is dpds. - }; - - // anonymous lambda to calculate the position and momentum at the i-th stage - auto calc_xp = [&](int i) { - XYZVector dx(0, 0, 0); - XYZVector dp(0, 0, 0); - for (int j = 0; j < i; ++j) { - dx = dx + kx[j] * a[i][j]; - dp = dp + kp[j] * a[i][j]; - } - XYZPoint x = x_SI + dx * h; - XYZVector p = p_SI + dp * h; - return std::make_pair(x, p); - }; - - // Calculate kx and kp for each stage - // build stage 0 - calc_k(x_SI, p0, 0); - - // build stage 1 - auto [x1, p1] = calc_xp(1); - calc_k(x1, p1, 1); // k1 - - // build stage 2 - auto [x2, p2] = calc_xp(2); - calc_k(x2, p2, 2); // k2 - - // build stage 3 - auto [x3, p3] = calc_xp(3); - calc_k(x3, p3, 3); // k3 - - // build stage 4 - auto [x4, p4] = calc_xp(4); - calc_k(x4, p4, 4); // k4 - - // build stage 5 - auto [x5, p5] = calc_xp(5); - calc_k(x5, p5, 5); // k5 - - // build stage 6 - auto [x6, p6] = calc_xp(6); - calc_k(x6, p6, 6); // k6 - - // Calculate the new position and momentum using the 5th-order method - XYZVector dx(0, 0, 0); - XYZVector dp(0, 0, 0); - for (int i = 0; i < 7; ++i) { - dx = dx + kx[i] * b[i]; - dp = dp + kp[i] * b[i]; - } - XYZPoint x_new_5 = x_SI + dx * h; // New position in SI units (m) - XYZVector p_new_5 = p_SI + dp * h; // New momentum in SI units (kg m/s) - - // Calculate the new position and momentum using the 4th-order method - dx = XYZVector(0, 0, 0); - dp = XYZVector(0, 0, 0); - for (int i = 0; i < 7; ++i) { - dx = dx + kx[i] * bs[i]; - dp = dp + kp[i] * bs[i]; - } - XYZPoint x_new_4 = x_SI + dx * h; // New position in SI units (m) - XYZVector p_new_4 = p_SI + dp * h; // New momentum in SI units (kg m/s) - - auto x_4_mm = x_new_4 * 1e3; // Convert back to mm - auto p_4_MeV = p_new_4 / fReltoSImom; // Convert back to MeV/c - auto x_5_mm = x_new_5 * 1e3; // Convert back to mm - auto p_5_MeV = p_new_5 / fReltoSImom; // Convert back to MeV/c - LOG(info) << "New position (5th order): " << x_5_mm.X() << ", " << x_5_mm.Y() << ", " << x_5_mm.Z(); - LOG(info) << "New momentum (5th order): " << p_5_MeV.X() << ", " << p_5_MeV.Y() << ", " << p_5_MeV.Z(); - LOG(info) << "New position (4th order): " << x_4_mm.X() << ", " << x_4_mm.Y() << ", " << x_4_mm.Z(); - LOG(info) << "New momentum (4th order): " << p_4_MeV.X() << ", " << p_4_MeV.Y() << ", " << p_4_MeV.Z(); - - // Convert back to mm and MeV/c - XYZVector x_err = (x_5_mm - x_4_mm); // Error in position (mm) - XYZVector p_err = (p_5_MeV - p_4_MeV); // Error in momentum (MeV/c) - - // Calculate the overall error - double ex = x_err.X() / (atol_pos + rtol * std::abs(x_5_mm.X())); - double ey = x_err.Y() / (atol_pos + rtol * std::abs(x_5_mm.Y())); - double ez = x_err.Z() / (atol_pos + rtol * std::abs(x_5_mm.Z())); - - double ep_x = p_err.X() / (atol_mom + rtol * std::abs(p_5_MeV.X())); - double ep_y = p_err.Y() / (atol_mom + rtol * std::abs(p_5_MeV.Y())); - double ep_z = p_err.Z() / (atol_mom + rtol * std::abs(p_5_MeV.Z())); - - // Combine errors (norm) - double err = std::sqrt(ex * ex + ey * ey + ez * ez + ep_x * ep_x + ep_y * ep_y + ep_z * ep_z); - - double factor = std::pow(err, -1.0 / 5.0); // Adjust step size based on error - factor = std::clamp(factor, 0.25, 4.0); // Clamp factor to reasonable limits - double hNew = h * factor; - // We now know the local error at this point. Now we need to decide to accept the point or not. - if (err <= 1.0) { - // Accept the step - fPos = x_5_mm; // Update position in mm - fMom = p_5_MeV; // Update momentum in MeV/c - LOG(info) << "Accepted step with error: " << err; - LOG(info) << "Step size: " << h << " m"; - LOG(info) << "New step size: " << hNew << " m"; - LOG(info) << "New Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "New Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - - // Adjust the step size for the next iteration - h = hNew; - return true; // Step accepted - } else { - // Reject the step and reduce the step size - LOG(info) << "Rejected step with error: " << err; - LOG(info) << "Step size: " << h << " m"; - LOG(info) << "Reducing step size to: " << hNew << " m"; - - h = hNew; // Reduce step size - if (h < 1e-6) { - LOG(error) << "Step size too small, aborting propagation."; - return false; // Abort propagation if step size is too small - } - } - } -} - -void AtPropagator::RK4Step(double h) -{ - // Take h to be the step size in m. - - auto x_k1 = fMom.Unit(); // The derivative of the position is then just the unit vector of the momentum. - auto p_k1 = dpds(fPos, fMom); // The derivative of the momentum is dpds. - - auto x_2 = fPos + x_k1 * h / 2; // Position at the midpoint - auto p_2 = fMom + p_k1 * h / 2 / fReltoSImom; // Momentum at the midpoint - auto x_k2 = p_2.Unit(); - auto p_k2 = dpds(x_2, p_2); - - auto x_3 = fPos + x_k2 * h / 2; // Position at the second midpoint - auto p_3 = fMom + p_k2 * h / 2 / fReltoSImom; // Momentum at the second midpoint - auto x_k3 = p_3.Unit(); - auto p_k3 = dpds(x_3, p_3); - - auto x_4 = fPos + x_k3 * h; // Position at the end of the step - auto p_4 = fMom + p_k3 * h / fReltoSImom; // Momentum at the end of the step - auto x_k4 = p_4.Unit(); - auto p_k4 = dpds(x_4, p_4); - - auto dpds_SI = (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) / 6; // "Force" in SI units (N) - - auto mom_SI = fReltoSImom * fMom; - mom_SI += dpds_SI * h; // Update momentum in SI units (kg m/s) - fMom = mom_SI / fReltoSImom; // Convert back to - - auto pos_SI = fPos * 1e-3; // Convert position to SI units (m) - pos_SI += (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) * h / 6; // Update position in SI units (m - fPos = pos_SI * 1e3; // Convert back to mm -} - bool AtPropagator::ReachedPOCA(const XYZPoint &point) { // Here we need to check if we are getting closer or further away from the POCA. @@ -351,18 +84,23 @@ bool AtPropagator::IntersectedPlane(const Plane3D &plane) return (prevSign != currSign); } -void AtPropagator::PropagateToPlane(const Plane3D &plane) +void AtPropagator::PropagateToPlane(const Plane3D &plane, AtStepper &stepper) { LOG(info) << "Propagating to plane: " << plane; auto KE_initial = Kinematics::KE(fMom, fMass); + stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; + while (true) { - fLastPos = fPos; - fLastMom = fMom; - LOG(info) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - RK4Step(fH); + auto result = stepper.Step(fH, fPos, fMom); + if (!result.success) { + LOG(error) << "Integration step failed, aborting propagation."; + return; // Abort propagation if step failed + } + CopyFromState(result); // Copy the new state from the stepper bool reachedMeasurementPoint = IntersectedPlane(plane); bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; @@ -378,9 +116,14 @@ void AtPropagator::PropagateToPlane(const Plane3D &plane) LOG(info) << "Final step size: " << finalH << " mm"; finalH = approach * 1e-3; // Convert to meters for the RK4 step - fPos = fLastPos; - fMom = fLastMom; - RK4Step(finalH); // Propagate to the measurement point + result = stepper.Step(finalH, fLastPos, fLastMom); + if (!result.success) { + LOG(error) << "Failed to propagate to measurement point, aborting."; + return; // Abort propagation if step failed + } + auto origH = fH; // Save original step size + CopyFromState(result); // Update position and momentum to the new state + fH = origH; // Restore original step size } if (particleStopped || momentumReversed) { @@ -399,19 +142,31 @@ void AtPropagator::PropagateToPlane(const Plane3D &plane) double h_Stop = deltaE / fELossModel->GetdEdx(KE_last); // Distance to stop in mm LOG(info) << "Estimated distance to stop: " << h_Stop << " mm"; - RK4Step(h_Stop * 1e-3); + result = stepper.Step(h_Stop * 1e-3, fLastPos, fLastMom); + if (!result.success) { + LOG(error) << "Failed to propagate to stopping point, aborting."; + return; // Abort propagation if step failed + } + auto origH = fH; // Save original step size + CopyFromState(result); // Update position and momentum to the new state + fH = origH; // Restore original step size LOG(info) << "Propagated to stopping point: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); LOG(info) << "Energy after stopping: " << Kinematics::KE(fMom, fMass) << " MeV"; while (!IntersectedPlane(plane)) { - fScalingFactor = 0; // Turn off enregy loss. + fScalingFactor = 0; // Turn off energy loss. // If we still haven't intersected the plane, we need to adjust the step size double h = std::abs(plane.Distance(fPos)); // Reduce step size so we hit the plane if (h <= fDistTol) break; LOG(info) << "Propagating to plane after stopping with step size: " << h << " mm"; - RK4Step(h * 1e-3); // Convert mm to m for RK4 step + result = stepper.Step(h * 1e-3, fPos, fMom); + if (!result.success) { + LOG(error) << "Failed to propagate to plane after stopping, aborting."; + return; // Abort propagation if step failed + } + CopyFromState(result); // Update position and momentum to the new state LOG(info) << "New position after adjusting step size: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); } fLastMom = fMom; @@ -599,6 +354,10 @@ AtStepper::StepResult AtRK4Stepper::Step(double h, const XYZPoint &fPos, const X result.h = h; result.success = true; + LOG(debug) << "Starting RK4 step with initial position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(debug) << "Initial momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + LOG(debug) << "Step size (h): " << h << " m"; + auto [x_k1, p_k1] = fDeriv(fPos, fMom); // The derivative of the position is then just the unit vector of the momentum. @@ -616,14 +375,18 @@ AtStepper::StepResult AtRK4Stepper::Step(double h, const XYZPoint &fPos, const X auto [x_k4, p_k4] = fDeriv(x_4, p_4); auto dpds_SI = (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) / 6; // "Force" in SI units (N) + auto dxds_SI = (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) / 6; // Position derivative in SI units (m) + + LOG(debug) << "dp/ds (SI units): " << dpds_SI.X() << ", " << dpds_SI.Y() << ", " << dpds_SI.Z(); + LOG(debug) << "dx/ds (SI units): " << dxds_SI.X() << ", " << dxds_SI.Y() << ", " << dxds_SI.Z(); auto mom_SI = fReltoSImom * fMom; mom_SI += dpds_SI * h; // Update momentum in SI units (kg m/s) result.mom = mom_SI / fReltoSImom; // Convert back to - auto pos_SI = fPos * 1e-3; // Convert position to SI units (m) - pos_SI += (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) * h / 6; // Update position in SI units (m - result.pos = pos_SI * 1e3; // Convert back to mm + auto pos_SI = fPos * 1e-3; // Convert position to SI units (m) + pos_SI += dxds_SI * h; // Update position in SI units (m + result.pos = pos_SI * 1e3; // Convert back to mm return result; } diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 78e1f2694..13dc2d71e 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -59,7 +59,6 @@ class AtPropagator { using XYZVector = ROOT::Math::XYZVector; using XYZPoint = ROOT::Math::XYZPoint; using Plane3D = ROOT::Math::Plane3D; - using DistanceFunc = std::function; XYZVector fEField{0, 0, 0}; // Electric field vector XYZVector fBField{0, 0, 0}; // Magnetic field vector @@ -137,14 +136,13 @@ class AtPropagator { * @param stepper The stepper to use for propagation. */ void PropagateToPoint(const XYZPoint &point, AtStepper &stepper); - void PropagateToPointAdaptive(const XYZPoint &point); /** * @brief Propagate the particle to the given plane. * * @param plane The plane to approach. */ - void PropagateToPlane(const Plane3D &plane); + void PropagateToPlane(const Plane3D &plane, AtStepper &stepper); /** * @brief Calculate the force acting on the particle. @@ -188,41 +186,6 @@ class AtPropagator { } protected: - /** - * @brief Perform a single RK4 step for propagation. - * - * This method performs a single Runge-Kutta 4th order step to propagate the particle's state. - * Updates fPos and fMom. - * @param h Step size for the RK4 step in meters. - */ - void RK4Step(double h); - - /** - * @brief Perform a single RK4 step using the Nystrom method. - * This method performs a single Runge-Kutta 4th order step using the Nystrom method - * to propagate the particle's state. Updates fPos and fMom. - * @param h Step size for the RK4 step in meters. - */ - void RK4StepNystrom(double h); - - /** - * @brief Perform an adaptive RK4 step for propagation. - * This method performs an adaptive Runge-Kutta 4th order step to propagate the particle's state. - * Updates fPos and fMom. - * - * The error is based on the difference in the positions at the end of the step. i.e: - * \eps = \sqrt{\eps_x^2 + \eps_y^2 + \eps_z^2}, where - * \eps_x = 1/30*|x_1 - x_2|, where x_1 is using h/2 and x_2 is using h. - * - * Step size is adjust to ensure the local error is less than fDelta. - * - * @param h Step size for the RK4 step in seconds. Modified in place to reflect the new step size. - * @return True if the step was accepted, false otherwise. - */ - bool RK4StepAdaptive(double &h); - - void PropagateTo(DistanceFunc distanceFunc); - bool ReachedPOCA(const XYZPoint &point); bool IntersectedPlane(const Plane3D &plane); void CopyFromState(const AtStepper::StepResult &result) diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 55b1321a7..249f69487 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -186,6 +186,7 @@ TEST(AtPropagatorTest, PropagateToPlane_NoField) elossModel->LoadSrimTable( "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); AtPropagator propagator(charge, mass, std::move(elossModel)); + AtRK4Stepper stepper; double KE = 1; // Kinetic energy in MeV double E = KE + mass_p; @@ -206,7 +207,7 @@ TEST(AtPropagatorTest, PropagateToPlane_NoField) XYZPoint planePoint(10, 10, 10); // Target point to propagate to 10 mm XYZVector planeNormal(1, 0, 0); // Normal vector of the plane in x-direction Plane3D plane(planeNormal, planePoint); // Create the plane - propagator.PropagateToPlane(plane); + propagator.PropagateToPlane(plane, stepper); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); @@ -223,6 +224,7 @@ TEST(AtPropagatorTest, PropagateToPlane_StoppingNoField) elossModel->LoadSrimTable( "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); AtPropagator propagator(charge, mass, std::move(elossModel)); + AtRK4Stepper stepper; double KE = 1; // Kinetic energy in MeV double E = KE + mass_p; @@ -239,7 +241,7 @@ TEST(AtPropagatorTest, PropagateToPlane_StoppingNoField) XYZPoint planePoint(220, 0, 0); // Target point to propagate to 215 mm XYZVector planeNormal(1, 0, 0); // Normal vector of the plane in x-direction Plane3D plane(planeNormal, planePoint); // Create the plane - propagator.PropagateToPlane(plane); + propagator.PropagateToPlane(plane, stepper); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); @@ -276,7 +278,6 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); XYZPoint targetPoint(10, 0, 0); // Target point to propagate to (10 mm) - // propagator.PropagateToPointAdaptive(targetPoint); propagator.PropagateToPoint(targetPoint, stepper); auto finalPos = propagator.GetPosition(); @@ -292,7 +293,6 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); - // propagator.PropagateToPointAdaptive(targetPoint); propagator.PropagateToPoint(targetPoint, stepper); finalPos = propagator.GetPosition(); From 1d563a79d558fa7978f4ee43a5d0d37af08f7b54 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 13:57:50 +0200 Subject: [PATCH 23/75] Propogate to point using common surface formalism --- AtTools/AtPropagator.cxx | 129 +++++++++++++++++++++++++++++++++-- AtTools/AtPropagator.h | 44 ++++++++++++ AtTools/AtPropagatorTest.cxx | 16 +++-- 3 files changed, 177 insertions(+), 12 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 53a7c792b..25249f206 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -69,11 +69,7 @@ bool AtPropagator::ReachedPOCA(const XYZPoint &point) auto lastDeriv = (fLastPos - point).Dot(fLastMom.Unit()); // proportional missing constants auto currDeriv = (fPos - point).Dot(fMom.Unit()); LOG(debug) << "Last Derivative: " << lastDeriv << ", Current Derivative: " << currDeriv; - return lastDeriv * currDeriv < 0; - - auto lastApproach = (fLastPos - point).R(); - auto approach = (fPos - point).R(); - return (approach > lastApproach); + return lastDeriv * currDeriv <= 0; } bool AtPropagator::IntersectedPlane(const Plane3D &plane) @@ -247,6 +243,8 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point, AtStepper &stepper) if (particleStopped || momentumReversed) { LOG(info) << "------ Particle stopped before measurement point/surface------"; + result.mass = fMass; // Ensure mass is set in the result + // Calculate how far to travel before stopping double KE_last = Kinematics::KE(fLastMom, fMass); double deltaE = KE_last - fStopTol; @@ -298,6 +296,117 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point, AtStepper &stepper) } // End of loop over RK4 integration } +void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &surface, AtStepper &stepper) +{ + LOG(info) << "Propagating to measurement surface"; + + auto KE_initial = Kinematics::KE(fMom, fMass); + stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; + + while (true) { + LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + + auto result = stepper.Step(fH, fPos, fMom); + if (!result.success) { + LOG(error) << "Integration step failed, aborting propagation."; + return; // Abort propagation if step failed + } + CopyFromState(result); // Copy the new state from the stepper + + bool reachedMeasurementPoint = surface.PassedSurface(result); + bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; + bool momentumReversed = (fLastMom.Dot(fMom) < 0); + + if (reachedMeasurementPoint && !particleStopped && !momentumReversed) { + // We reached the measurement surface, so we should figure out how far we are from the measurement point + LOG(info) << "------ Reached measurement surface ------"; + double finalH = (fLastPos - fPos).R(); // Distance traveled in the last step + double approach = surface.Distance(fLastPos); + + LOG(info) << "Distance to plane: " << approach << " mm"; + LOG(info) << "Final step size: " << finalH << " mm"; + + finalH = approach * 1e-3; // Convert to meters for the RK4 step + result = stepper.Step(finalH, fLastPos, fLastMom); + if (!result.success) { + LOG(error) << "Failed to propagate to measurement point, aborting."; + return; // Abort propagation if step failed + } + auto origH = fH; // Save original step size + CopyFromState(result); // Update position and momentum to the new state + fH = origH; // Restore original step size + } + + if (particleStopped || momentumReversed) { + // In this case the particle stopped before hitting the plane + // we should throw a warning to let the user know that there wasn't + // enough energy to reach the surface. + LOG(warning) << "------ Particle stopped before reaching measurement surface ------"; + + // Calculate how far to travel before stopping + double KE_last = Kinematics::KE(fLastMom, fMass); + double deltaE = KE_last - fStopTol; + deltaE = std::max(deltaE, 0.0); // Ensure we don't have negative energy loss + + LOG(info) << "Last KE: " << KE_last << " MeV"; + LOG(info) << "Energy to loose to stop: " << deltaE << " MeV"; + double h_Stop = deltaE / fELossModel->GetdEdx(KE_last); // Distance to stop in mm + LOG(info) << "Estimated distance to stop: " << h_Stop << " mm"; + + result = stepper.Step(h_Stop * 1e-3, fLastPos, fLastMom); + if (!result.success) { + LOG(error) << "Failed to propagate to stopping point, aborting."; + return; // Abort propagation if step failed + } + auto origH = fH; // Save original step size + CopyFromState(result); // Update position and momentum to the new state + fH = origH; // Restore original step size + LOG(info) << "Propagated to stopping point: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Energy after stopping: " << Kinematics::KE(fMom, fMass) << " MeV"; + + while (surface.fClipToSurface && !surface.PassedSurface(result)) { + fScalingFactor = 0; // Turn off energy loss. + + // If we still haven't intersected the surface, we need to adjust the step size + double h = surface.Distance(fPos); // Reduce step size so we hit the surface + if (h <= fDistTol) + break; + LOG(info) << "Propagating to surface after stopping with step size: " << h << " mm"; + result = stepper.Step(h * 1e-3, fPos, fMom); + if (!result.success) { + LOG(error) << "Failed to propagate to surface after stopping, aborting."; + return; // Abort propagation if step failed + } + CopyFromState(result); // Update position and momentum to the new state + LOG(info) << "New position after adjusting step size: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + } + fLastMom = fMom; + fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped + reachedMeasurementPoint = true; + } + + if (reachedMeasurementPoint || particleStopped || momentumReversed) { + double distanceToSurface = surface.Distance(fPos); + + double KE_final = Kinematics::KE(fMom, fMass); + auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV + LOG(info) << "------- End of RK4 interation ---------"; + LOG(info) << "Particle stopped: " << particleStopped; + LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; + LOG(info) << "Distance to plane: " << distanceToSurface << " mm"; + LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; + LOG(info) << "Scaling factor: " << fScalingFactor; + LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + + fPos = surface.ProjectToSurface(fPos); + LOG(info) << "Projected Position on plane: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + return; + } + } // End of loop over RK4 integration +} + void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss, AtStepper &stepper) { LOG(info) << "Propagating to point: " << point << " with eLoss: " << eLoss; @@ -545,4 +654,14 @@ AtStepper::StepResult AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, } } } + +bool AtMeasurementPoint::PassedSurface(AtStepper::StepResult &result) const +{ + // Check if the particle has passed the measurement point + auto lastDeriv = (fPoint - result.lastPos).Dot(result.lastMom.Unit()); + auto currDeriv = (fPoint - result.pos).Dot(result.mom.Unit()); + LOG(debug) << "Last Derivative: " << lastDeriv << ", Current Derivative: " << currDeriv; + return lastDeriv * currDeriv <= 0; +} + } // namespace AtTools diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 13dc2d71e..07b3fc490 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -11,6 +11,7 @@ namespace AtTools { +class AtMeasurementSurface; class AtStepper { public: struct StepResult { @@ -18,6 +19,7 @@ class AtStepper { ROOT::Math::XYZVector mom; // Momentum of the particle in MeV/c ROOT::Math::XYZPoint lastPos; // Last position of the particle in mm ROOT::Math::XYZVector lastMom; // Last momentum of the particle in MeV/c + double mass; // Mass of the particle in MeV/c^2 double h; // Step size for the step in m bool success; // Whether the step was successful }; @@ -144,6 +146,8 @@ class AtPropagator { */ void PropagateToPlane(const Plane3D &plane, AtStepper &stepper); + void PropagateToMeasurementSurface(const AtMeasurementSurface &surface, AtStepper &stepper); + /** * @brief Calculate the force acting on the particle. * @@ -203,6 +207,9 @@ class AtPropagator { result.mom = fMom; result.lastPos = fLastPos; result.lastMom = fLastMom; + result.mass = fMass; + result.h = fH; + result.success = true; // Assume success unless proven otherwise return result; } }; @@ -216,5 +223,42 @@ class AtRK4AdaptiveStepper : public AtStepper { StepResult Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const override; }; +/** + * @brief Class for measurement surface in the AT-TPC. + * + * This class represents a measurement surface or point in the AT-TPC. It's used to define the stopping + * point and behavior of the propagator. + */ +class AtMeasurementSurface { +public: + bool fClipToSurface = false; // Whether to clip to the surface + + /** + * @brief Calculate the distance from the position to the surface. + */ + virtual double Distance(const ROOT::Math::XYZPoint &pos) const = 0; + + /** + * @brief Check if we have passed the surface between the last position and the current position. + */ + virtual bool PassedSurface(AtStepper::StepResult &result) const = 0; + + virtual ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const + { + return pos; // Default implementation returns the position as is + } +}; + +class AtMeasurementPoint : public AtMeasurementSurface { +protected: + ROOT::Math::XYZPoint fPoint; // The measurement point in mm + +public: + AtMeasurementPoint(const ROOT::Math::XYZPoint &point) : fPoint(point) {} + + double Distance(const ROOT::Math::XYZPoint &pos) const override { return (fPoint - pos).R(); } + + bool PassedSurface(AtStepper::StepResult &result) const override; +}; } // namespace AtTools #endif // #ifndef ATPROPAGATOR_H diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 249f69487..c945993bb 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -107,6 +107,7 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4Stepper stepper; + AtMeasurementPoint measurementPoint({1e3, 0, 0}); double KE = 1; // Kinetic energy in MeV double E = KE + mass_p; @@ -120,8 +121,7 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); - XYZPoint targetPoint(1e3, 0, 0); // Target point to propagate to (1 m) - propagator.PropagateToPoint(targetPoint, stepper); + propagator.PropagateToMeasurementSurface(measurementPoint, stepper); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); @@ -135,7 +135,7 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) startMom.SetXYZ(p, 0, 0); // Reset momentum propagator.SetState(startPos, startMom); - propagator.PropagateToPoint(targetPoint, stepper); // Propagate to range + propagator.PropagateToMeasurementSurface(measurementPoint, stepper); // Propagate to range finalPos = propagator.GetPosition(); finalMom = propagator.GetMomentum(); ASSERT_NEAR(finalPos.X(), 130, 10); // Final position in x-direction should be close to 130 mm @@ -151,6 +151,7 @@ TEST(AtPropagatorTest, PropagateToPoint_NoField) "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4Stepper stepper; + AtMeasurementPoint measurementPoint({10, 0, 0}); double KE = 1; // Kinetic energy in MeV double E = KE + mass_p; @@ -169,7 +170,8 @@ TEST(AtPropagatorTest, PropagateToPoint_NoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); XYZPoint targetPoint(10, 0, 0); // Target point to propagate to 10 mm - propagator.PropagateToPoint(targetPoint, stepper); + // propagator.PropagateToPoint(targetPoint, stepper); + propagator.PropagateToMeasurementSurface(measurementPoint, stepper); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); @@ -259,6 +261,7 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4AdaptiveStepper stepper; + AtMeasurementPoint measurementPoint({10, 0, 0}); double KE = 1; // Kinetic energy in MeV double E = KE + mass_p; @@ -277,8 +280,7 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); - XYZPoint targetPoint(10, 0, 0); // Target point to propagate to (10 mm) - propagator.PropagateToPoint(targetPoint, stepper); + propagator.PropagateToMeasurementSurface(measurementPoint, stepper); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); @@ -293,7 +295,7 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); - propagator.PropagateToPoint(targetPoint, stepper); + propagator.PropagateToMeasurementSurface(measurementPoint, stepper); finalPos = propagator.GetPosition(); finalMom = propagator.GetMomentum(); From dcccdfdc89c34d87d6a4f56d4920542448c05095 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 14:01:43 +0200 Subject: [PATCH 24/75] Migrate fixed eloss to new interfaces --- AtTools/AtPropagator.cxx | 110 ++------------------------------------- AtTools/AtPropagator.h | 10 +--- 2 files changed, 5 insertions(+), 115 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 25249f206..17d734816 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -194,108 +194,6 @@ void AtPropagator::PropagateToPlane(const Plane3D &plane, AtStepper &stepper) } // End of loop over RK4 integration } -void AtPropagator::PropagateToPoint(const XYZPoint &point, AtStepper &stepper) -{ - LOG(info) << "Propagating to point: " << point; - - auto KE_initial = Kinematics::KE(fMom, fMass); - stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; - - while (true) { - LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - - auto result = stepper.Step(fH, fPos, fMom); - if (!result.success) { - LOG(error) << "Integration step failed, aborting propagation."; - return; // Abort propagation if step failed - } - CopyFromState(result); // Copy the new state from the stepper - - if (!ReachedPOCA(point)) - continue; - - bool reachedMeasurementPoint = ReachedPOCA(point); - bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; - bool momentumReversed = (fLastMom.Dot(fMom) < 0); - - if (reachedMeasurementPoint && !particleStopped && !momentumReversed) { - // We reached the measurement point, so we should figure out how far we are from the measurement point - // and update that remaining amount - LOG(info) << "------ Reached measurement point------"; - double finalH = (fLastPos - fPos).R(); // Distance traveled in the last step - double approach = (fLastPos - point).R(); // Distance to the measurement point - LOG(info) << "Distance to measurement point: " << approach << " mm"; - LOG(info) << "Final step size: " << finalH << " mm"; - - finalH = approach * 1e-3; // Convert to meters for the RK4 step - result = stepper.Step(finalH, fLastPos, fLastMom); - if (!result.success) { - LOG(error) << "Failed to propagate to measurement point, aborting."; - return; // Abort propagation if step failed - } - auto origH = fH; // Save original step size - CopyFromState(result); // Update position and momentum to the new state - fH = origH; // Restore original step size - } - - // If we stopped, then we should figure out about where we stopped assuming linear de/dx over this last step - if (particleStopped || momentumReversed) { - LOG(info) << "------ Particle stopped before measurement point/surface------"; - - result.mass = fMass; // Ensure mass is set in the result - - // Calculate how far to travel before stopping - double KE_last = Kinematics::KE(fLastMom, fMass); - double deltaE = KE_last - fStopTol; - deltaE = std::max(deltaE, 0.0); // Ensure we don't have negative energy loss - double h_Stop = deltaE / fELossModel->GetdEdx(KE_last); // Distance to stop in mm - - LOG(info) << "KE at last point: " << KE_last << " MeV"; - LOG(info) << "Energy to lose to stop: " << deltaE << " MeV"; - LOG(info) << "Estimated distance to stop: " << h_Stop << " mm"; - LOG(info) << "dE/dx at last point: " << fELossModel->GetdEdx(KE_last) << " MeV/mm"; - - result = stepper.Step(h_Stop * 1e-3, fLastPos, fLastMom); - if (!result.success) { - LOG(error) << "Failed to propagate to stopping point, aborting."; - return; // Abort propagation if step failed - } - auto origH = fH; // Save original step size - CopyFromState(result); // Update position and momentum to the new state - fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped - fLastMom = fMom; // Update last momentum to zero - fH = origH; // Restore original step size - - LOG(info) << "Final Position after stopping: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "Final Momentum after stopping: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - } - - if (reachedMeasurementPoint || particleStopped || momentumReversed) { - // Undo the last step since we were closer last time. - double lastApproach = (fLastPos - point).R(); - double approach = (fPos - point).R(); - - double KE_final = Kinematics::KE(fMom, fMass); - auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV - LOG(info) << "------- End of RK4 interation ---------"; - LOG(info) << "Particle stopped: " << particleStopped; - LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; - LOG(info) << "Last approach: " << lastApproach << " Current approach: " << approach; - LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; - LOG(info) << "Scaling factor: " << fScalingFactor; - LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - LOG(info) << "Last Position: " << fLastPos.X() << ", " << fLastPos.Y() << ", " << fLastPos.Z(); - LOG(info) << "Last Momentum: " << fLastMom.X() << ", " << fLastMom.Y() << ", " << fLastMom.Z(); - - // fPos = fLastPos; - // fMom = fLastMom; - return; - } - } // End of loop over RK4 integration -} - void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &surface, AtStepper &stepper) { LOG(info) << "Propagating to measurement surface"; @@ -407,13 +305,13 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur } // End of loop over RK4 integration } -void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss, AtStepper &stepper) +void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &surface, double eLoss, AtStepper &stepper) { - LOG(info) << "Propagating to point: " << point << " with eLoss: " << eLoss; + LOG(info) << "Propagating to surface with eLoss: " << eLoss; if (eLoss == 0) { LOG(warn) << "No energy loss specified, propagating without energy loss adjustment."; - PropagateToPoint(point, stepper); + PropagateToMeasurementSurface(surface, stepper); return; } @@ -436,7 +334,7 @@ void AtPropagator::PropagateToPoint(const XYZPoint &point, double eLoss, AtStepp } iterations++; - PropagateToPoint(point, stepper); // Propagate without energy loss adjustment + PropagateToMeasurementSurface(surface, stepper); // Propagate without energy loss adjustment double KE_final = Kinematics::KE(fMom, fMass); calc_eLoss = KE_initial - KE_final; // Energy loss in MeV diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 07b3fc490..8e8e33c31 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -129,15 +129,7 @@ class AtPropagator { * @param point The point to approach. * @param eLoss If not 0, constrain the energy loss to this value by adjusting fScalingFactor. */ - void PropagateToPoint(const XYZPoint &point, double eLoss, AtStepper &stepper); - - /** - * @brief Propagate the particle to the point of closest approach to the given point. - * - * @param point The point to approach. - * @param stepper The stepper to use for propagation. - */ - void PropagateToPoint(const XYZPoint &point, AtStepper &stepper); + void PropagateToMeasurementSurface(const AtMeasurementSurface &point, double eLoss, AtStepper &stepper); /** * @brief Propagate the particle to the given plane. From df4ec309bfd8f2eba755b2365672e0173f54da26 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 14:08:23 +0200 Subject: [PATCH 25/75] Plans using interface --- AtTools/AtPropagator.cxx | 8 ++++++++ AtTools/AtPropagator.h | 10 ++++++++++ AtTools/AtPropagatorTest.cxx | 6 ++++-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 17d734816..4d87820b0 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -562,4 +562,12 @@ bool AtMeasurementPoint::PassedSurface(AtStepper::StepResult &result) const return lastDeriv * currDeriv <= 0; } +bool AtMeasurementPlane::PassedSurface(AtStepper::StepResult &result) const +{ + // Check if the particle has crossed the plane this step. + auto prevSign = fPlane.Distance(result.lastPos) > 0 ? 1 : -1; + auto currSign = fPlane.Distance(result.pos) > 0 ? 1 : -1; + return (prevSign != currSign); +} + } // namespace AtTools diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 8e8e33c31..5d52acd1e 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -252,5 +252,15 @@ class AtMeasurementPoint : public AtMeasurementSurface { bool PassedSurface(AtStepper::StepResult &result) const override; }; + +class AtMeasurementPlane : public AtMeasurementSurface { +protected: + ROOT::Math::Plane3D fPlane; // The measurement plane +public: + AtMeasurementPlane(const ROOT::Math::Plane3D &plane) : fPlane(plane) { fClipToSurface = true; } + + double Distance(const ROOT::Math::XYZPoint &pos) const override { return std::abs(fPlane.Distance(pos)); } + bool PassedSurface(AtStepper::StepResult &result) const override; +}; } // namespace AtTools #endif // #ifndef ATPROPAGATOR_H diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index c945993bb..75467531b 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -209,7 +209,8 @@ TEST(AtPropagatorTest, PropagateToPlane_NoField) XYZPoint planePoint(10, 10, 10); // Target point to propagate to 10 mm XYZVector planeNormal(1, 0, 0); // Normal vector of the plane in x-direction Plane3D plane(planeNormal, planePoint); // Create the plane - propagator.PropagateToPlane(plane, stepper); + AtMeasurementPlane measurementPlane(plane); + propagator.PropagateToMeasurementSurface(measurementPlane, stepper); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); @@ -243,7 +244,8 @@ TEST(AtPropagatorTest, PropagateToPlane_StoppingNoField) XYZPoint planePoint(220, 0, 0); // Target point to propagate to 215 mm XYZVector planeNormal(1, 0, 0); // Normal vector of the plane in x-direction Plane3D plane(planeNormal, planePoint); // Create the plane - propagator.PropagateToPlane(plane, stepper); + AtMeasurementPlane measurementPlane(plane); + propagator.PropagateToMeasurementSurface(measurementPlane, stepper); auto finalPos = propagator.GetPosition(); auto finalMom = propagator.GetMomentum(); From c847df19c2021a79334649b48e49372a8e47ef82 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 14:28:14 +0200 Subject: [PATCH 26/75] Refactor and only project if at surface --- AtTools/AtPropagator.cxx | 163 +++++---------------------------------- AtTools/AtPropagator.h | 97 ++++++++++------------- 2 files changed, 62 insertions(+), 198 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 4d87820b0..25bc5751a 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -61,139 +61,6 @@ AtPropagator::XYZVector AtPropagator::d2xds2(const XYZPoint &pos, const XYZVecto return 1 / p * (dpds_vec - phat * (phat.Dot(dpds_vec))); // Second derivative of position w.r.t. arc length } -bool AtPropagator::ReachedPOCA(const XYZPoint &point) -{ - // Here we need to check if we are getting closer or further away from the POCA. - // We may walk right past it so need to look for a change in the sign of the derivative or - // something like that. - auto lastDeriv = (fLastPos - point).Dot(fLastMom.Unit()); // proportional missing constants - auto currDeriv = (fPos - point).Dot(fMom.Unit()); - LOG(debug) << "Last Derivative: " << lastDeriv << ", Current Derivative: " << currDeriv; - return lastDeriv * currDeriv <= 0; -} - -bool AtPropagator::IntersectedPlane(const Plane3D &plane) -{ - // Check if the particle has crossed the plane this step. - auto prevSign = plane.Distance(fLastPos) > 0 ? 1 : -1; - auto currSign = plane.Distance(fPos) > 0 ? 1 : -1; - return (prevSign != currSign); -} - -void AtPropagator::PropagateToPlane(const Plane3D &plane, AtStepper &stepper) -{ - LOG(info) << "Propagating to plane: " << plane; - - auto KE_initial = Kinematics::KE(fMom, fMass); - stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; - - while (true) { - LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - - auto result = stepper.Step(fH, fPos, fMom); - if (!result.success) { - LOG(error) << "Integration step failed, aborting propagation."; - return; // Abort propagation if step failed - } - CopyFromState(result); // Copy the new state from the stepper - - bool reachedMeasurementPoint = IntersectedPlane(plane); - bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; - bool momentumReversed = (fLastMom.Dot(fMom) < 0); - - if (reachedMeasurementPoint && !particleStopped && !momentumReversed) { - // We reached the measurement point, so we should figure out how far we are from the measurement point - LOG(info) << "------ Reached measurement point ------"; - double finalH = (fLastPos - fPos).R(); // Distance traveled in the last step - double approach = std::abs(plane.Distance(fLastPos)); - - LOG(info) << "Distance to plane: " << approach << " mm"; - LOG(info) << "Final step size: " << finalH << " mm"; - - finalH = approach * 1e-3; // Convert to meters for the RK4 step - result = stepper.Step(finalH, fLastPos, fLastMom); - if (!result.success) { - LOG(error) << "Failed to propagate to measurement point, aborting."; - return; // Abort propagation if step failed - } - auto origH = fH; // Save original step size - CopyFromState(result); // Update position and momentum to the new state - fH = origH; // Restore original step size - } - - if (particleStopped || momentumReversed) { - // In this case the particle stopped before hitting the plane - // we should throw a warning to let the user know that there wasn't - // enough energy to reach the plane. - LOG(warning) << "------ Particle stopped before intersecting plane ------"; - - // Calculate how far to travel before stopping - double KE_last = Kinematics::KE(fLastMom, fMass); - double deltaE = KE_last - fStopTol; - deltaE = std::max(deltaE, 0.0); // Ensure we don't have negative energy loss - - LOG(info) << "Last KE: " << KE_last << " MeV"; - LOG(info) << "Energy to loose to stop: " << deltaE << " MeV"; - double h_Stop = deltaE / fELossModel->GetdEdx(KE_last); // Distance to stop in mm - LOG(info) << "Estimated distance to stop: " << h_Stop << " mm"; - - result = stepper.Step(h_Stop * 1e-3, fLastPos, fLastMom); - if (!result.success) { - LOG(error) << "Failed to propagate to stopping point, aborting."; - return; // Abort propagation if step failed - } - auto origH = fH; // Save original step size - CopyFromState(result); // Update position and momentum to the new state - fH = origH; // Restore original step size - LOG(info) << "Propagated to stopping point: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "Energy after stopping: " << Kinematics::KE(fMom, fMass) << " MeV"; - - while (!IntersectedPlane(plane)) { - fScalingFactor = 0; // Turn off energy loss. - - // If we still haven't intersected the plane, we need to adjust the step size - double h = std::abs(plane.Distance(fPos)); // Reduce step size so we hit the plane - if (h <= fDistTol) - break; - LOG(info) << "Propagating to plane after stopping with step size: " << h << " mm"; - result = stepper.Step(h * 1e-3, fPos, fMom); - if (!result.success) { - LOG(error) << "Failed to propagate to plane after stopping, aborting."; - return; // Abort propagation if step failed - } - CopyFromState(result); // Update position and momentum to the new state - LOG(info) << "New position after adjusting step size: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - } - fLastMom = fMom; - fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped - reachedMeasurementPoint = true; - } - - if (reachedMeasurementPoint || particleStopped || momentumReversed) { - double distanceToPlane = std::abs(plane.Distance(fPos)); - - double KE_final = Kinematics::KE(fMom, fMass); - auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV - LOG(info) << "------- End of RK4 interation ---------"; - LOG(info) << "Particle stopped: " << particleStopped; - LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; - LOG(info) << "Distance to plane: " << distanceToPlane << " mm"; - LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; - LOG(info) << "Scaling factor: " << fScalingFactor; - LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - - // Project the position onto the plane. Cannot use ProjectOnPlane since it is templated in such - // a way that it can't separate our XYZPoint and its internal XYZPoint. - double d = plane.Distance(fPos); // Distance from the point to the plane - fPos = XYZPoint(fPos.X() - plane.A() * d, fPos.Y() - plane.B() * d, fPos.Z() - plane.C() * d); - LOG(info) << "Projected Position on plane: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - return; - } - } // End of loop over RK4 integration -} - void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &surface, AtStepper &stepper) { LOG(info) << "Propagating to measurement surface"; @@ -263,13 +130,15 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur LOG(info) << "Propagated to stopping point: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); LOG(info) << "Energy after stopping: " << Kinematics::KE(fMom, fMass) << " MeV"; - while (surface.fClipToSurface && !surface.PassedSurface(result)) { + while (surface.fClipToSurface) { fScalingFactor = 0; // Turn off energy loss. // If we still haven't intersected the surface, we need to adjust the step size double h = surface.Distance(fPos); // Reduce step size so we hit the surface - if (h <= fDistTol) + if (h <= fDistTol || surface.PassedSurface(result)) { + reachedMeasurementPoint = true; break; + } LOG(info) << "Propagating to surface after stopping with step size: " << h << " mm"; result = stepper.Step(h * 1e-3, fPos, fMom); if (!result.success) { @@ -281,7 +150,6 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur } fLastMom = fMom; fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped - reachedMeasurementPoint = true; } if (reachedMeasurementPoint || particleStopped || momentumReversed) { @@ -297,7 +165,10 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur LOG(info) << "Scaling factor: " << fScalingFactor; LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - fPos = surface.ProjectToSurface(fPos); + // If we reached the measurement surface, we should project the position onto the surface + if (reachedMeasurementPoint) { + fPos = surface.ProjectToSurface(fPos); + } LOG(info) << "Projected Position on plane: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); return; @@ -352,10 +223,10 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur fScalingFactor = 1; // Reset scaling factor after convergence } -AtStepper::StepResult AtRK4Stepper::Step(double h, const XYZPoint &fPos, const XYZVector &fMom) const +AtStepper::StepState AtRK4Stepper::Step(double h, const XYZPoint &fPos, const XYZVector &fMom) const { // Take h to be the step size in m. - StepResult result; + StepState result; result.lastPos = fPos; result.lastMom = fMom; result.h = h; @@ -398,10 +269,10 @@ AtStepper::StepResult AtRK4Stepper::Step(double h, const XYZPoint &fPos, const X return result; } -AtStepper::StepResult AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, const XYZVector &fMom) const +AtStepper::StepState AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, const XYZVector &fMom) const { // Take h to be the step size in m. - StepResult result; + StepState result; result.lastPos = fPos; result.lastMom = fMom; result.h = h; @@ -553,7 +424,7 @@ AtStepper::StepResult AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, } } -bool AtMeasurementPoint::PassedSurface(AtStepper::StepResult &result) const +bool AtMeasurementPoint::PassedSurface(AtStepper::StepState &result) const { // Check if the particle has passed the measurement point auto lastDeriv = (fPoint - result.lastPos).Dot(result.lastMom.Unit()); @@ -562,7 +433,7 @@ bool AtMeasurementPoint::PassedSurface(AtStepper::StepResult &result) const return lastDeriv * currDeriv <= 0; } -bool AtMeasurementPlane::PassedSurface(AtStepper::StepResult &result) const +bool AtMeasurementPlane::PassedSurface(AtStepper::StepState &result) const { // Check if the particle has crossed the plane this step. auto prevSign = fPlane.Distance(result.lastPos) > 0 ? 1 : -1; @@ -570,4 +441,10 @@ bool AtMeasurementPlane::PassedSurface(AtStepper::StepResult &result) const return (prevSign != currSign); } +ROOT::Math::XYZPoint AtMeasurementPlane::ProjectToSurface(const ROOT::Math::XYZPoint &pos) const +{ + // Project the position onto the measurement plane + auto dist = fPlane.Distance(pos); + return pos - dist * fPlane.Normal(); +} } // namespace AtTools diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 5d52acd1e..cef8486d5 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -14,14 +14,15 @@ namespace AtTools { class AtMeasurementSurface; class AtStepper { public: - struct StepResult { - ROOT::Math::XYZPoint pos; // Position of the particle in mm - ROOT::Math::XYZVector mom; // Momentum of the particle in MeV/c - ROOT::Math::XYZPoint lastPos; // Last position of the particle in mm - ROOT::Math::XYZVector lastMom; // Last momentum of the particle in MeV/c - double mass; // Mass of the particle in MeV/c^2 - double h; // Step size for the step in m - bool success; // Whether the step was successful + struct StepState { + ROOT::Math::XYZPoint pos; /// Position of the particle in mm + ROOT::Math::XYZVector mom; /// Momentum of the particle in MeV/c + ROOT::Math::XYZPoint lastPos; /// Last position of the particle in mm + ROOT::Math::XYZVector lastMom; /// Last momentum of the particle in MeV/c + double mass; /// Mass of the particle in MeV/c^2 + double h; /// Step size to use in m + double hUsed; /// Step size used in this step in m + bool success; /// Whether the step was successful }; /** * @brief Function type defining the derivative of the position and momentum w.r.t. distance. @@ -39,12 +40,35 @@ class AtStepper { DerivFunc fDeriv; - virtual StepResult Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const = 0; + virtual StepState Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const = 0; protected: static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) }; +/** + * @brief Class for measurement surface in the AT-TPC. + * + * This class represents a measurement surface or point in the AT-TPC. It's used to define the stopping + * point and behavior of the propagator. + */ +class AtMeasurementSurface { +public: + bool fClipToSurface = false; // Whether to clip to the surface + + /** + * @brief Calculate the distance from the position to the surface. + */ + virtual double Distance(const ROOT::Math::XYZPoint &pos) const = 0; + + /** + * @brief Check if we have passed the surface between the last position and the current position. + */ + virtual bool PassedSurface(AtStepper::StepState &result) const = 0; + + virtual ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const = 0; +}; + /** * @brief Class for propagating particles through a medium. * @@ -70,8 +94,6 @@ class AtPropagator { // Internal state variables for the propagator double fH = 1e-4; /// Step size for propagation in m - double fDelta = 1e-3; /// Relative error tolerance for adaptive step size. 10^-3 means each 1m of propagation - /// introduces at most 1mm of error. double fETol = 1e-4; /// Energy tolerance for convergence when fixing energy loss double fStopTol = 0.01; /// Maximum kinetic energy to consider the particle stopped (MeV) double fDistTol = 1e-2; /// Distance tolerance when considering positions equal. (mm) @@ -114,7 +136,6 @@ class AtPropagator { fMom = mom; } - void SetDelta(double delta) { fDelta = delta; } void SetH(double h) { fH = h; } XYZPoint GetPosition() const { return fPos; } @@ -131,13 +152,6 @@ class AtPropagator { */ void PropagateToMeasurementSurface(const AtMeasurementSurface &point, double eLoss, AtStepper &stepper); - /** - * @brief Propagate the particle to the given plane. - * - * @param plane The plane to approach. - */ - void PropagateToPlane(const Plane3D &plane, AtStepper &stepper); - void PropagateToMeasurementSurface(const AtMeasurementSurface &surface, AtStepper &stepper); /** @@ -182,9 +196,7 @@ class AtPropagator { } protected: - bool ReachedPOCA(const XYZPoint &point); - bool IntersectedPlane(const Plane3D &plane); - void CopyFromState(const AtStepper::StepResult &result) + void CopyFromState(const AtStepper::StepState &result) { fPos = result.pos; fMom = result.mom; @@ -192,9 +204,9 @@ class AtPropagator { fLastMom = result.lastMom; fH = result.h; } - AtStepper::StepResult CopyToState() const + AtStepper::StepState CopyToState() const { - AtStepper::StepResult result; + AtStepper::StepState result; result.pos = fPos; result.mom = fMom; result.lastPos = fLastPos; @@ -208,37 +220,11 @@ class AtPropagator { class AtRK4Stepper : public AtStepper { public: - StepResult Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const override; + StepState Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const override; }; class AtRK4AdaptiveStepper : public AtStepper { public: - StepResult Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const override; -}; - -/** - * @brief Class for measurement surface in the AT-TPC. - * - * This class represents a measurement surface or point in the AT-TPC. It's used to define the stopping - * point and behavior of the propagator. - */ -class AtMeasurementSurface { -public: - bool fClipToSurface = false; // Whether to clip to the surface - - /** - * @brief Calculate the distance from the position to the surface. - */ - virtual double Distance(const ROOT::Math::XYZPoint &pos) const = 0; - - /** - * @brief Check if we have passed the surface between the last position and the current position. - */ - virtual bool PassedSurface(AtStepper::StepResult &result) const = 0; - - virtual ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const - { - return pos; // Default implementation returns the position as is - } + StepState Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const override; }; class AtMeasurementPoint : public AtMeasurementSurface { @@ -249,8 +235,8 @@ class AtMeasurementPoint : public AtMeasurementSurface { AtMeasurementPoint(const ROOT::Math::XYZPoint &point) : fPoint(point) {} double Distance(const ROOT::Math::XYZPoint &pos) const override { return (fPoint - pos).R(); } - - bool PassedSurface(AtStepper::StepResult &result) const override; + bool PassedSurface(AtStepper::StepState &result) const override; + ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const override { return fPoint; } }; class AtMeasurementPlane : public AtMeasurementSurface { @@ -260,7 +246,8 @@ class AtMeasurementPlane : public AtMeasurementSurface { AtMeasurementPlane(const ROOT::Math::Plane3D &plane) : fPlane(plane) { fClipToSurface = true; } double Distance(const ROOT::Math::XYZPoint &pos) const override { return std::abs(fPlane.Distance(pos)); } - bool PassedSurface(AtStepper::StepResult &result) const override; + bool PassedSurface(AtStepper::StepState &result) const override; + ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const override; }; } // namespace AtTools #endif // #ifndef ATPROPAGATOR_H From 1a8bdcc0533ec9d2ee117edd333afc43d7e95f31 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 14:34:13 +0200 Subject: [PATCH 27/75] Refactor state struct --- AtTools/AtPropagator.cxx | 34 +++++++++++++++++----------------- AtTools/AtPropagator.h | 36 +++++++++++++++++++----------------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 25bc5751a..b99be9a6f 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -227,8 +227,8 @@ AtStepper::StepState AtRK4Stepper::Step(double h, const XYZPoint &fPos, const XY { // Take h to be the step size in m. StepState result; - result.lastPos = fPos; - result.lastMom = fMom; + result.fLastPos = fPos; + result.fLastMom = fMom; result.h = h; result.success = true; @@ -259,12 +259,12 @@ AtStepper::StepState AtRK4Stepper::Step(double h, const XYZPoint &fPos, const XY LOG(debug) << "dx/ds (SI units): " << dxds_SI.X() << ", " << dxds_SI.Y() << ", " << dxds_SI.Z(); auto mom_SI = fReltoSImom * fMom; - mom_SI += dpds_SI * h; // Update momentum in SI units (kg m/s) - result.mom = mom_SI / fReltoSImom; // Convert back to + mom_SI += dpds_SI * h; // Update momentum in SI units (kg m/s) + result.fMom = mom_SI / fReltoSImom; // Convert back to - auto pos_SI = fPos * 1e-3; // Convert position to SI units (m) - pos_SI += dxds_SI * h; // Update position in SI units (m - result.pos = pos_SI * 1e3; // Convert back to mm + auto pos_SI = fPos * 1e-3; // Convert position to SI units (m) + pos_SI += dxds_SI * h; // Update position in SI units (m + result.fPos = pos_SI * 1e3; // Convert back to mm return result; } @@ -273,8 +273,8 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, { // Take h to be the step size in m. StepState result; - result.lastPos = fPos; - result.lastMom = fMom; + result.fLastPos = fPos; + result.fLastMom = fMom; result.h = h; result.success = true; @@ -395,13 +395,13 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, // We now know the local error at this point. Now we need to decide to accept the point or not. if (err <= 1.0) { // Accept the step - result.pos = x_5_mm; // Update position in mm - result.mom = p_5_MeV; // Update momentum in MeV/c + result.fPos = x_5_mm; // Update position in mm + result.fMom = p_5_MeV; // Update momentum in MeV/c LOG(info) << "Accepted step with error: " << err; LOG(info) << "Step size: " << h << " m"; LOG(info) << "New step size: " << hNew << " m"; - LOG(info) << "New Position: " << result.pos.X() << ", " << result.pos.Y() << ", " << result.pos.Z(); - LOG(info) << "New Momentum: " << result.mom.X() << ", " << result.mom.Y() << ", " << result.mom.Z(); + LOG(info) << "New Position: " << result.fPos.X() << ", " << result.fPos.Y() << ", " << result.fPos.Z(); + LOG(info) << "New Momentum: " << result.fMom.X() << ", " << result.fMom.Y() << ", " << result.fMom.Z(); // Adjust the step size for the next iteration result.h = hNew; @@ -427,8 +427,8 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, bool AtMeasurementPoint::PassedSurface(AtStepper::StepState &result) const { // Check if the particle has passed the measurement point - auto lastDeriv = (fPoint - result.lastPos).Dot(result.lastMom.Unit()); - auto currDeriv = (fPoint - result.pos).Dot(result.mom.Unit()); + auto lastDeriv = (fPoint - result.fLastPos).Dot(result.fLastMom.Unit()); + auto currDeriv = (fPoint - result.fPos).Dot(result.fMom.Unit()); LOG(debug) << "Last Derivative: " << lastDeriv << ", Current Derivative: " << currDeriv; return lastDeriv * currDeriv <= 0; } @@ -436,8 +436,8 @@ bool AtMeasurementPoint::PassedSurface(AtStepper::StepState &result) const bool AtMeasurementPlane::PassedSurface(AtStepper::StepState &result) const { // Check if the particle has crossed the plane this step. - auto prevSign = fPlane.Distance(result.lastPos) > 0 ? 1 : -1; - auto currSign = fPlane.Distance(result.pos) > 0 ? 1 : -1; + auto prevSign = fPlane.Distance(result.fLastPos) > 0 ? 1 : -1; + auto currSign = fPlane.Distance(result.fPos) > 0 ? 1 : -1; return (prevSign != currSign); } diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index cef8486d5..2c7dafb6d 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -15,14 +15,14 @@ class AtMeasurementSurface; class AtStepper { public: struct StepState { - ROOT::Math::XYZPoint pos; /// Position of the particle in mm - ROOT::Math::XYZVector mom; /// Momentum of the particle in MeV/c - ROOT::Math::XYZPoint lastPos; /// Last position of the particle in mm - ROOT::Math::XYZVector lastMom; /// Last momentum of the particle in MeV/c - double mass; /// Mass of the particle in MeV/c^2 - double h; /// Step size to use in m - double hUsed; /// Step size used in this step in m - bool success; /// Whether the step was successful + ROOT::Math::XYZPoint fPos; /// Position of the particle in mm + ROOT::Math::XYZVector fMom; /// Momentum of the particle in MeV/c + ROOT::Math::XYZPoint fLastPos; /// Last position of the particle in mm + ROOT::Math::XYZVector fLastMom; /// Last momentum of the particle in MeV/c + double fMass; /// Mass of the particle in MeV/c^2 + double h; /// Step size to use in m + double hUsed; /// Step size used in this step in m + bool success; /// Whether the step was successful }; /** * @brief Function type defining the derivative of the position and momentum w.r.t. distance. @@ -99,6 +99,8 @@ class AtPropagator { double fDistTol = 1e-2; /// Distance tolerance when considering positions equal. (mm) double fScalingFactor = 1.0; /// Scaling factor for energy loss + AtStepper::StepState fState; // Current state of the particle + XYZPoint fPos; // Current position of the particle in mm XYZVector fMom; // Current momentum of the particle in MeV/c @@ -198,20 +200,20 @@ class AtPropagator { protected: void CopyFromState(const AtStepper::StepState &result) { - fPos = result.pos; - fMom = result.mom; - fLastPos = result.lastPos; - fLastMom = result.lastMom; + fPos = result.fPos; + fMom = result.fMom; + fLastPos = result.fLastPos; + fLastMom = result.fLastMom; fH = result.h; } AtStepper::StepState CopyToState() const { AtStepper::StepState result; - result.pos = fPos; - result.mom = fMom; - result.lastPos = fLastPos; - result.lastMom = fLastMom; - result.mass = fMass; + result.fPos = fPos; + result.fMom = fMom; + result.fLastPos = fLastPos; + result.fLastMom = fLastMom; + result.fMass = fMass; result.h = fH; result.success = true; // Assume success unless proven otherwise return result; From ec81eb11b81e7eb4980cb54a06e5d5fede3c1a67 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 15:08:48 +0200 Subject: [PATCH 28/75] Refactor to passing around state struct Old variables need removed along with debug statements. --- AtTools/AtPropagator.cxx | 127 ++++++++++++++++++++++++--------------- AtTools/AtPropagator.h | 60 +++++++++--------- 2 files changed, 106 insertions(+), 81 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index b99be9a6f..4339d0e01 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -65,41 +65,51 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur { LOG(info) << "Propagating to measurement surface"; - auto KE_initial = Kinematics::KE(fMom, fMass); + auto KE_initial = Kinematics::KE(fState.fMom, fState.fMass); stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; while (true) { - LOG(debug) << "Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(debug) << "Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + LOG(info) << "Position: " << GetPosition().X() << ", " << GetPosition().Y() << ", " << GetPosition().Z(); + LOG(info) << "Momentum: " << GetMomentum().X() << ", " << GetMomentum().Y() << ", " << GetMomentum().Z(); - auto result = stepper.Step(fH, fPos, fMom); + auto result = stepper.Step(fState); if (!result.success) { LOG(error) << "Integration step failed, aborting propagation."; return; // Abort propagation if step failed } CopyFromState(result); // Copy the new state from the stepper + fState = result; // Update the internal state - bool reachedMeasurementPoint = surface.PassedSurface(result); - bool particleStopped = Kinematics::KE(fMom, fMass) < fStopTol; - bool momentumReversed = (fLastMom.Dot(fMom) < 0); + bool reachedMeasurementPoint = surface.PassedSurface(fState); + bool particleStopped = Kinematics::KE(fState.fMom, fState.fMass) < fStopTol; + bool momentumReversed = (fState.fLastMom.Dot(fState.fMom) < 0); if (reachedMeasurementPoint && !particleStopped && !momentumReversed) { // We reached the measurement surface, so we should figure out how far we are from the measurement point LOG(info) << "------ Reached measurement surface ------"; - double finalH = (fLastPos - fPos).R(); // Distance traveled in the last step - double approach = surface.Distance(fLastPos); + double finalH = (fState.fLastPos - fState.fPos).R(); // Distance traveled in the last step + double approach = surface.Distance(fState.fLastPos); LOG(info) << "Distance to plane: " << approach << " mm"; LOG(info) << "Final step size: " << finalH << " mm"; - - finalH = approach * 1e-3; // Convert to meters for the RK4 step - result = stepper.Step(finalH, fLastPos, fLastMom); + LOG(info) << "Current position: " << fState.fLastPos.X() << ", " << fState.fLastPos.Y() << ", " + << fState.fLastPos.Z(); + LOG(info) << "Current momentum: " << fState.fLastMom.X() << ", " << fState.fLastMom.Y() << ", " + << fState.fLastMom.Z(); + + finalH = approach * 1e-3; // Convert to meters for the RK4 step + fState.h = finalH; // Set the step size to the distance to the surface + fState.fPos = fState.fLastPos; // Set position to last position + fState.fMom = fState.fLastMom; // Set momentum to last momentum + result = stepper.Step(fState); if (!result.success) { LOG(error) << "Failed to propagate to measurement point, aborting."; return; // Abort propagation if step failed } auto origH = fH; // Save original step size CopyFromState(result); // Update position and momentum to the new state + fState = result; // Update the internal state + fState.h = origH; // Restore original step size fH = origH; // Restore original step size } @@ -110,7 +120,7 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur LOG(warning) << "------ Particle stopped before reaching measurement surface ------"; // Calculate how far to travel before stopping - double KE_last = Kinematics::KE(fLastMom, fMass); + double KE_last = Kinematics::KE(fState.fLastMom, fState.fMass); double deltaE = KE_last - fStopTol; deltaE = std::max(deltaE, 0.0); // Ensure we don't have negative energy loss @@ -119,58 +129,69 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur double h_Stop = deltaE / fELossModel->GetdEdx(KE_last); // Distance to stop in mm LOG(info) << "Estimated distance to stop: " << h_Stop << " mm"; - result = stepper.Step(h_Stop * 1e-3, fLastPos, fLastMom); + fState.h = h_Stop * 1e-3; // Convert to meters for the RK4 step + fState.fPos = fState.fLastPos; // Set position to last position + fState.fMom = fState.fLastMom; // Set momentum to last momentum + result = stepper.Step(fState); if (!result.success) { LOG(error) << "Failed to propagate to stopping point, aborting."; return; // Abort propagation if step failed } auto origH = fH; // Save original step size CopyFromState(result); // Update position and momentum to the new state + fState = result; // Update the internal state + fState.h = origH; // Restore original step size fH = origH; // Restore original step size - LOG(info) << "Propagated to stopping point: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "Energy after stopping: " << Kinematics::KE(fMom, fMass) << " MeV"; + LOG(info) << "Propagated to stopping point: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " + << fState.fPos.Z(); + LOG(info) << "Energy after stopping: " << Kinematics::KE(fState.fMom, fState.fMass) << " MeV"; while (surface.fClipToSurface) { fScalingFactor = 0; // Turn off energy loss. // If we still haven't intersected the surface, we need to adjust the step size - double h = surface.Distance(fPos); // Reduce step size so we hit the surface + double h = surface.Distance(fState.fPos); // Reduce step size so we hit the surface if (h <= fDistTol || surface.PassedSurface(result)) { reachedMeasurementPoint = true; break; } LOG(info) << "Propagating to surface after stopping with step size: " << h << " mm"; - result = stepper.Step(h * 1e-3, fPos, fMom); + fState.h = h * 1e-3; // Convert to meters for the RK4 step + result = stepper.Step(fState); if (!result.success) { LOG(error) << "Failed to propagate to surface after stopping, aborting."; return; // Abort propagation if step failed } CopyFromState(result); // Update position and momentum to the new state - LOG(info) << "New position after adjusting step size: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + fState = result; // Update the internal state + LOG(info) << "New position after adjusting step size: " << fState.fPos.X() << ", " << fState.fPos.Y() + << ", " << fState.fPos.Z(); } - fLastMom = fMom; - fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped + fState.fLastMom = fState.fMom; + fState.fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped } if (reachedMeasurementPoint || particleStopped || momentumReversed) { - double distanceToSurface = surface.Distance(fPos); + double distanceToSurface = surface.Distance(fState.fPos); - double KE_final = Kinematics::KE(fMom, fMass); + double KE_final = Kinematics::KE(fState.fMom, fState.fMass); + LOG(info) << "Initial KE" << KE_initial << " MeV"; + LOG(info) << "Final KE: " << KE_final << " MeV"; auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV LOG(info) << "------- End of RK4 interation ---------"; LOG(info) << "Particle stopped: " << particleStopped; LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; - LOG(info) << "Distance to plane: " << distanceToSurface << " mm"; + LOG(info) << "Distance to surface: " << distanceToSurface << " mm"; LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; LOG(info) << "Scaling factor: " << fScalingFactor; - LOG(info) << "Final Position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Final Position: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " << fState.fPos.Z(); // If we reached the measurement surface, we should project the position onto the surface if (reachedMeasurementPoint) { - fPos = surface.ProjectToSurface(fPos); + fState.fPos = surface.ProjectToSurface(fState.fPos); } - LOG(info) << "Projected Position on plane: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "Final Momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + LOG(info) << "Projected on surface: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " << fState.fPos.Z(); + LOG(info) << "Final Momentum: " << fState.fMom.X() << ", " << fState.fMom.Y() << ", " << fState.fMom.Z(); return; } } // End of loop over RK4 integration @@ -188,13 +209,13 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur int iterations = 0; double calc_eLoss = 0; - double KE_initial = Kinematics::KE(fMom, fMass); - auto initialMom = fMom; // Save initial momentum for energy loss calculation - auto initialPos = fPos; // Save initial position for energy loss calculation + double KE_initial = Kinematics::KE(fState.fMom, fState.fMass); + auto initialMom = fState.fMom; // Save initial momentum for energy loss calculation + auto initialPos = fState.fPos; // Save initial position for energy loss calculation while (std::abs(calc_eLoss - eLoss) > 1e-4) { - fMom = initialMom; // Reset position and momentum to initial values for the next iteration - fPos = initialPos; + fState.fMom = initialMom; // Reset position and momentum to initial values for the next iteration + fState.fPos = initialPos; LOG(debug) << "Running iteration " << iterations << " with scaling factor: " << fScalingFactor << " and energy loss: " << calc_eLoss; @@ -207,7 +228,7 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur iterations++; PropagateToMeasurementSurface(surface, stepper); // Propagate without energy loss adjustment - double KE_final = Kinematics::KE(fMom, fMass); + double KE_final = Kinematics::KE(fState.fMom, fState.fMass); calc_eLoss = KE_initial - KE_final; // Energy loss in MeV fScalingFactor *= eLoss / calc_eLoss; LOG(info) << "Desired energy loss: " << eLoss << " MeV"; @@ -223,18 +244,22 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur fScalingFactor = 1; // Reset scaling factor after convergence } -AtStepper::StepState AtRK4Stepper::Step(double h, const XYZPoint &fPos, const XYZVector &fMom) const +AtStepper::StepState AtRK4Stepper::Step(const StepState &state) const { // Take h to be the step size in m. - StepState result; - result.fLastPos = fPos; - result.fLastMom = fMom; - result.h = h; + StepState result = state; + result.fLastPos = state.fPos; + result.fLastMom = state.fMom; + result.hUsed = state.h; result.success = true; - LOG(debug) << "Starting RK4 step with initial position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(debug) << "Initial momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - LOG(debug) << "Step size (h): " << h << " m"; + auto h = state.h; // Step size in m + auto fPos = state.fPos; + auto fMom = state.fMom; + + LOG(info) << "Starting RK4 step with initial position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(info) << "Initial momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + LOG(info) << "Step size (h): " << h << " m"; auto [x_k1, p_k1] = fDeriv(fPos, fMom); // The derivative of the position is then just the unit vector of the momentum. @@ -255,8 +280,8 @@ AtStepper::StepState AtRK4Stepper::Step(double h, const XYZPoint &fPos, const XY auto dpds_SI = (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) / 6; // "Force" in SI units (N) auto dxds_SI = (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) / 6; // Position derivative in SI units (m) - LOG(debug) << "dp/ds (SI units): " << dpds_SI.X() << ", " << dpds_SI.Y() << ", " << dpds_SI.Z(); - LOG(debug) << "dx/ds (SI units): " << dxds_SI.X() << ", " << dxds_SI.Y() << ", " << dxds_SI.Z(); + LOG(info) << "dp/ds (SI units): " << dpds_SI.X() << ", " << dpds_SI.Y() << ", " << dpds_SI.Z(); + LOG(info) << "dx/ds (SI units): " << dxds_SI.X() << ", " << dxds_SI.Y() << ", " << dxds_SI.Z(); auto mom_SI = fReltoSImom * fMom; mom_SI += dpds_SI * h; // Update momentum in SI units (kg m/s) @@ -269,14 +294,16 @@ AtStepper::StepState AtRK4Stepper::Step(double h, const XYZPoint &fPos, const XY return result; } -AtStepper::StepState AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, const XYZVector &fMom) const +AtStepper::StepState AtRK4AdaptiveStepper::Step(const StepState &state) const { // Take h to be the step size in m. - StepState result; - result.fLastPos = fPos; - result.fLastMom = fMom; - result.h = h; + StepState result = state; + result.fLastPos = state.fPos; + result.fLastMom = state.fMom; result.success = true; + auto h = state.h; // Step size in m + auto fPos = state.fPos; + auto fMom = state.fMom; // Take h to be the step size in m. // Use DP5(4) method for adaptive step size control. @@ -405,6 +432,7 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, // Adjust the step size for the next iteration result.h = hNew; + result.hUsed = h; // Store the step size used result.success = true; // Step accepted return result; } else { @@ -418,6 +446,7 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(double h, const XYZPoint &fPos, if (result.h < 1e-6) { LOG(error) << "Step size too small, aborting propagation."; result.success = false; + result.hUsed = h; return result; // Abort propagation if step size is too small } } diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 2c7dafb6d..c1f6e2f6a 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -20,6 +20,7 @@ class AtStepper { ROOT::Math::XYZPoint fLastPos; /// Last position of the particle in mm ROOT::Math::XYZVector fLastMom; /// Last momentum of the particle in MeV/c double fMass; /// Mass of the particle in MeV/c^2 + double fQ; /// Charge of the particle in Coulombs double h; /// Step size to use in m double hUsed; /// Step size used in this step in m bool success; /// Whether the step was successful @@ -40,7 +41,7 @@ class AtStepper { DerivFunc fDeriv; - virtual StepState Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const = 0; + virtual StepState Step(const StepState &state) const = 0; protected: static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) @@ -99,20 +100,22 @@ class AtPropagator { double fDistTol = 1e-2; /// Distance tolerance when considering positions equal. (mm) double fScalingFactor = 1.0; /// Scaling factor for energy loss - AtStepper::StepState fState; // Current state of the particle - - XYZPoint fPos; // Current position of the particle in mm - XYZVector fMom; // Current momentum of the particle in MeV/c - - XYZPoint fLastPos; - XYZVector fLastMom; - + AtStepper::StepState fState; // Current state of the particle static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) public: + /** + * @brief Constructor for AtPropagator. + * @param charge Charge of the particle in Coulombs. + * @param mass Mass of the particle in MeV/c^2. + * @param elossModel Energy loss model to use for the particle. + */ AtPropagator(double charge, double mass, std::unique_ptr elossModel) : fQ(charge), fMass(mass), fELossModel(std::move(elossModel)) { + fState.fMass = mass; + fState.fQ = charge; + fState.h = fH; // Initialize step size } /** * @brief Set the electric field (V/m) @@ -134,14 +137,18 @@ class AtPropagator { */ void SetState(const XYZPoint &pos, const XYZVector &mom) { - fPos = pos; - fMom = mom; + fState.fPos = pos; + fState.fMom = mom; } - void SetH(double h) { fH = h; } + void SetH(double h) + { + fH = h; + fState.h = h; + } // Set the step size in m - XYZPoint GetPosition() const { return fPos; } - XYZVector GetMomentum() const { return fMom; } + XYZPoint GetPosition() const { return fState.fPos; } + XYZVector GetMomentum() const { return fState.fMom; } /** * @brief Propagate the particle to the point of closest approach to the given point. @@ -200,33 +207,22 @@ class AtPropagator { protected: void CopyFromState(const AtStepper::StepState &result) { - fPos = result.fPos; - fMom = result.fMom; - fLastPos = result.fLastPos; - fLastMom = result.fLastMom; + fState = result; + // fPos = result.fPos; + // fMom = result.fMom; + // fLastPos = result.fLastPos; + // fLastMom = result.fLastMom; fH = result.h; } - AtStepper::StepState CopyToState() const - { - AtStepper::StepState result; - result.fPos = fPos; - result.fMom = fMom; - result.fLastPos = fLastPos; - result.fLastMom = fLastMom; - result.fMass = fMass; - result.h = fH; - result.success = true; // Assume success unless proven otherwise - return result; - } }; class AtRK4Stepper : public AtStepper { public: - StepState Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const override; + StepState Step(const StepState &state) const override; }; class AtRK4AdaptiveStepper : public AtStepper { public: - StepState Step(double h, const ROOT::Math::XYZPoint &pos, const ROOT::Math::XYZVector &mom) const override; + StepState Step(const StepState &state) const override; }; class AtMeasurementPoint : public AtMeasurementSurface { From 7332716f71cb7b62b27bfa91bb609209af5dc322 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 15:55:45 +0200 Subject: [PATCH 29/75] Remove double counted variables from state --- AtTools/AtPropagator.cxx | 10 ++++------ AtTools/AtPropagator.h | 11 +++-------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 4339d0e01..99966deae 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -69,8 +69,8 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; while (true) { - LOG(info) << "Position: " << GetPosition().X() << ", " << GetPosition().Y() << ", " << GetPosition().Z(); - LOG(info) << "Momentum: " << GetMomentum().X() << ", " << GetMomentum().Y() << ", " << GetMomentum().Z(); + LOG(debug) << "Position: " << GetPosition().X() << ", " << GetPosition().Y() << ", " << GetPosition().Z(); + LOG(debug) << "Momentum: " << GetMomentum().X() << ", " << GetMomentum().Y() << ", " << GetMomentum().Z(); auto result = stepper.Step(fState); if (!result.success) { @@ -106,11 +106,10 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur LOG(error) << "Failed to propagate to measurement point, aborting."; return; // Abort propagation if step failed } - auto origH = fH; // Save original step size + auto origH = fState.h; // Save original step size CopyFromState(result); // Update position and momentum to the new state fState = result; // Update the internal state fState.h = origH; // Restore original step size - fH = origH; // Restore original step size } if (particleStopped || momentumReversed) { @@ -137,11 +136,10 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur LOG(error) << "Failed to propagate to stopping point, aborting."; return; // Abort propagation if step failed } - auto origH = fH; // Save original step size + auto origH = fState.h; // Save original step size CopyFromState(result); // Update position and momentum to the new state fState = result; // Update the internal state fState.h = origH; // Restore original step size - fH = origH; // Restore original step size LOG(info) << "Propagated to stopping point: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " << fState.fPos.Z(); LOG(info) << "Energy after stopping: " << Kinematics::KE(fState.fMom, fState.fMass) << " MeV"; diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index c1f6e2f6a..3c37cb39c 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -94,7 +94,6 @@ class AtPropagator { const std::unique_ptr fELossModel; // Energy loss model // Internal state variables for the propagator - double fH = 1e-4; /// Step size for propagation in m double fETol = 1e-4; /// Energy tolerance for convergence when fixing energy loss double fStopTol = 0.01; /// Maximum kinetic energy to consider the particle stopped (MeV) double fDistTol = 1e-2; /// Distance tolerance when considering positions equal. (mm) @@ -115,7 +114,7 @@ class AtPropagator { { fState.fMass = mass; fState.fQ = charge; - fState.h = fH; // Initialize step size + fState.h = 1e-4; // Initial step size in m } /** * @brief Set the electric field (V/m) @@ -141,11 +140,7 @@ class AtPropagator { fState.fMom = mom; } - void SetH(double h) - { - fH = h; - fState.h = h; - } // Set the step size in m + void SetH(double h) { fState.h = h; } // Set the step size in m XYZPoint GetPosition() const { return fState.fPos; } XYZVector GetMomentum() const { return fState.fMom; } @@ -212,7 +207,7 @@ class AtPropagator { // fMom = result.fMom; // fLastPos = result.fLastPos; // fLastMom = result.fLastMom; - fH = result.h; + // fH = result.h; } }; From 03e6d77720207c6a10913ed3cd4430c65b7f252b Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 16:07:40 +0200 Subject: [PATCH 30/75] Remove debug and more refactoring --- AtTools/AtPropagator.cxx | 52 ++++++++++++++++-------------------- AtTools/AtPropagator.h | 57 +++++++++++++++++++--------------------- 2 files changed, 49 insertions(+), 60 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 99966deae..ef78b2177 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -33,12 +33,12 @@ namespace AtTools { AtPropagator::XYZVector AtPropagator::Force(XYZPoint pos, XYZVector mom) const { - auto v = Kinematics::GetVel(mom, fMass); + auto v = Kinematics::GetVel(mom, fState.fMass); - auto F_lorentz = fQ * (fEField + v.Cross(fBField)); + auto F_lorentz = fState.fQ * (fEField + v.Cross(fBField)); LOG(debug) << "F_lorentz: " << F_lorentz; - auto dedx = fScalingFactor * fELossModel->GetdEdx(Kinematics::KE(mom, fMass)); // Stopping power in MeV/mm - auto dedx_si = dedx * 1.60218e-10; // de_dx in SI units (J/m) + auto dedx = fScalingFactor * fELossModel->GetdEdx(Kinematics::KE(mom, fState.fMass)); // Stopping power in MeV/mm + auto dedx_si = dedx * 1.60218e-10; // de_dx in SI units (J/m) auto drag = -dedx_si * mom.Unit(); LOG(debug) << "drag: " << drag << " mom " << mom << " dedx " << dedx_si; @@ -49,7 +49,7 @@ AtPropagator::XYZVector AtPropagator::Force(XYZPoint pos, XYZVector mom) const AtPropagator::XYZVector AtPropagator::dpds(const XYZPoint &pos, const XYZVector &mom) const { // Calculate the force acting on the particle at the given position and momentum - auto speed = Kinematics::GetSpeed(mom.R(), fMass); // Speed in m/s + auto speed = Kinematics::GetSpeed(mom.R(), fState.fMass); // Speed in m/s return Force(pos, mom) / speed; } AtPropagator::XYZVector AtPropagator::d2xds2(const XYZPoint &pos, const XYZVector &mom) const @@ -77,8 +77,7 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur LOG(error) << "Integration step failed, aborting propagation."; return; // Abort propagation if step failed } - CopyFromState(result); // Copy the new state from the stepper - fState = result; // Update the internal state + fState = result; // Update the internal state bool reachedMeasurementPoint = surface.PassedSurface(fState); bool particleStopped = Kinematics::KE(fState.fMom, fState.fMass) < fStopTol; @@ -107,7 +106,6 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur return; // Abort propagation if step failed } auto origH = fState.h; // Save original step size - CopyFromState(result); // Update position and momentum to the new state fState = result; // Update the internal state fState.h = origH; // Restore original step size } @@ -137,7 +135,6 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur return; // Abort propagation if step failed } auto origH = fState.h; // Save original step size - CopyFromState(result); // Update position and momentum to the new state fState = result; // Update the internal state fState.h = origH; // Restore original step size LOG(info) << "Propagated to stopping point: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " @@ -160,8 +157,7 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur LOG(error) << "Failed to propagate to surface after stopping, aborting."; return; // Abort propagation if step failed } - CopyFromState(result); // Update position and momentum to the new state - fState = result; // Update the internal state + fState = result; // Update the internal state LOG(info) << "New position after adjusting step size: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " << fState.fPos.Z(); } @@ -299,6 +295,7 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(const StepState &state) const result.fLastPos = state.fPos; result.fLastMom = state.fMom; result.success = true; + auto h = state.h; // Step size in m auto fPos = state.fPos; auto fMom = state.fMom; @@ -306,14 +303,10 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(const StepState &state) const // Take h to be the step size in m. // Use DP5(4) method for adaptive step size control. - double atol_pos = 1e-2; // Absolute tolerance for position (mm) - double atol_mom = 1e-2; // Absolute tolerance for momentum (MeV/c) - double rtol = 1e-6; // Relative tolerance for both position and momentum - auto x0_mm = fPos; auto p0 = fMom; - LOG(info) << "Starting RK4 step with initial position: " << x0_mm.X() << ", " << x0_mm.Y() << ", " << x0_mm.Z(); - LOG(info) << "Initial momentum: " << p0.X() << ", " << p0.Y() << ", " << p0.Z(); + LOG(debug) << "Starting RK4 step with initial position: " << x0_mm.X() << ", " << x0_mm.Y() << ", " << x0_mm.Z(); + LOG(debug) << "Initial momentum: " << p0.X() << ", " << p0.Y() << ", " << p0.Z(); while (true) { auto x_SI = fPos * 1e-3; // Convert position to SI units (m) @@ -403,13 +396,13 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(const StepState &state) const XYZVector p_err = (p_5_MeV - p_4_MeV); // Error in momentum (MeV/c) // Calculate the overall error - double ex = x_err.X() / (atol_pos + rtol * std::abs(x_5_mm.X())); - double ey = x_err.Y() / (atol_pos + rtol * std::abs(x_5_mm.Y())); - double ez = x_err.Z() / (atol_pos + rtol * std::abs(x_5_mm.Z())); + double ex = x_err.X() / (fAtolPos + fRtol * std::abs(x_5_mm.X())); + double ey = x_err.Y() / (fAtolPos + fRtol * std::abs(x_5_mm.Y())); + double ez = x_err.Z() / (fAtolPos + fRtol * std::abs(x_5_mm.Z())); - double ep_x = p_err.X() / (atol_mom + rtol * std::abs(p_5_MeV.X())); - double ep_y = p_err.Y() / (atol_mom + rtol * std::abs(p_5_MeV.Y())); - double ep_z = p_err.Z() / (atol_mom + rtol * std::abs(p_5_MeV.Z())); + double ep_x = p_err.X() / (fAtolMom + fRtol * std::abs(p_5_MeV.X())); + double ep_y = p_err.Y() / (fAtolMom + fRtol * std::abs(p_5_MeV.Y())); + double ep_z = p_err.Z() / (fAtolMom + fRtol * std::abs(p_5_MeV.Z())); // Combine errors (norm) double err = std::sqrt(ex * ex + ey * ey + ez * ez + ep_x * ep_x + ep_y * ep_y + ep_z * ep_z); @@ -428,8 +421,7 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(const StepState &state) const LOG(info) << "New Position: " << result.fPos.X() << ", " << result.fPos.Y() << ", " << result.fPos.Z(); LOG(info) << "New Momentum: " << result.fMom.X() << ", " << result.fMom.Y() << ", " << result.fMom.Z(); - // Adjust the step size for the next iteration - result.h = hNew; + result.h = hNew; // Adjust the step size for the next iteration result.hUsed = h; // Store the step size used result.success = true; // Step accepted return result; @@ -439,13 +431,13 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(const StepState &state) const LOG(info) << "Step size: " << h << " m"; LOG(info) << "Reducing step size to: " << hNew << " m"; - result.h = hNew; // Reduce step size - h = hNew; // Update the step size for the next iteration - if (result.h < 1e-6) { - LOG(error) << "Step size too small, aborting propagation."; + result.h = hNew; // Reduce step size for next iteration + h = hNew; // Update h for the next iteration + if (result.h < fMinStep || result.h > fMaxStep) { + LOG(error) << "Step size out of bounds, aborting propagation."; result.success = false; result.hUsed = h; - return result; // Abort propagation if step size is too small + return result; // Abort propagation if step size is out of bounds } } } diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 3c37cb39c..e1c357dfa 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -86,20 +86,21 @@ class AtPropagator { using XYZVector = ROOT::Math::XYZVector; using XYZPoint = ROOT::Math::XYZPoint; using Plane3D = ROOT::Math::Plane3D; - XYZVector fEField{0, 0, 0}; // Electric field vector - XYZVector fBField{0, 0, 0}; // Magnetic field vector - const double fQ; // Charge of the particle in Coulombs - const double fMass; // Mass of the particle in MeV/c^2 + // Variables used for the force + XYZVector fEField{0, 0, 0}; // Electric field vector + XYZVector fBField{0, 0, 0}; // Magnetic field vector const std::unique_ptr fELossModel; // Energy loss model // Internal state variables for the propagator - double fETol = 1e-4; /// Energy tolerance for convergence when fixing energy loss - double fStopTol = 0.01; /// Maximum kinetic energy to consider the particle stopped (MeV) - double fDistTol = 1e-2; /// Distance tolerance when considering positions equal. (mm) double fScalingFactor = 1.0; /// Scaling factor for energy loss + AtStepper::StepState fState; /// Current state of the particle + + // Tolerances and limits + double fETol = 1e-4; /// Energy tolerance for convergence when fixing energy loss + double fStopTol = 0.01; /// Maximum kinetic energy to consider the particle stopped (MeV) + double fDistTol = 1e-2; /// Distance tolerance when considering positions equal. (mm) - AtStepper::StepState fState; // Current state of the particle static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) public: @@ -110,7 +111,7 @@ class AtPropagator { * @param elossModel Energy loss model to use for the particle. */ AtPropagator(double charge, double mass, std::unique_ptr elossModel) - : fQ(charge), fMass(mass), fELossModel(std::move(elossModel)) + : fELossModel(std::move(elossModel)) { fState.fMass = mass; fState.fQ = charge; @@ -176,18 +177,6 @@ class AtPropagator { */ XYZVector dpds(const XYZPoint &pos, const XYZVector &mom) const; - /** - * @brief Calculate the second derivative of the position w.r.t. arc length. - * - * \frac{d^2\vec{x}}{ds^2} = \frac{1}{p} \left( \frac{d\vec{p}}{ds} - \hat{p} (\hat{p} \cdot \frac{d\vec{p}}{ds}) - * \right) - * - * @param pos Position of the particle in mm. - * @param mom Momentum of the particle in MeV/c. - * @return The second derivative of the position w.r.t. arc length in m/m^2. - */ - XYZVector d2xds2(const XYZPoint &pos, const XYZVector &mom) const; - XYZVector dxds(const XYZPoint &pos, const XYZVector &mom) const { return mom.Unit(); // The derivative of the position is just the unit vector of the momentum. @@ -200,15 +189,17 @@ class AtPropagator { } protected: - void CopyFromState(const AtStepper::StepState &result) - { - fState = result; - // fPos = result.fPos; - // fMom = result.fMom; - // fLastPos = result.fLastPos; - // fLastMom = result.fLastMom; - // fH = result.h; - } + /** + * @brief Calculate the second derivative of the position w.r.t. arc length. + * + * \frac{d^2\vec{x}}{ds^2} = \frac{1}{p} \left( \frac{d\vec{p}}{ds} - \hat{p} (\hat{p} \cdot \frac{d\vec{p}}{ds}) + * \right) + * + * @param pos Position of the particle in mm. + * @param mom Momentum of the particle in MeV/c. + * @return The second derivative of the position w.r.t. arc length in m/m^2. + */ + XYZVector d2xds2(const XYZPoint &pos, const XYZVector &mom) const; }; class AtRK4Stepper : public AtStepper { @@ -217,6 +208,12 @@ class AtRK4Stepper : public AtStepper { }; class AtRK4AdaptiveStepper : public AtStepper { public: + double fAtolPos = 1e-2; /// Absolute tolerance for position in mm + double fAtolMom = 1e-2; /// Absolute tolerance for momentum in MeV/c + double fRtol = 1e-6; /// Relative tolerance for position and momentum + double fMinStep = 1e-6; /// Minimum step size in m + double fMaxStep = 10.0; /// Maximum step size in m + StepState Step(const StepState &state) const override; }; From 4801616b620f34fd01403ed638e5ac4752bdc8f9 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 18:36:02 +0200 Subject: [PATCH 31/75] Refactor step size defaults --- AtTools/AtPropagator.cxx | 80 +++++++++--------- AtTools/AtPropagator.h | 154 +++++++++++++++++++---------------- AtTools/AtPropagatorTest.cxx | 6 +- 3 files changed, 124 insertions(+), 116 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index ef78b2177..fd526d77f 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -4,12 +4,10 @@ #include -// Butcher tableau (c, a_ij) and the two b vectors for Dormand–Prince 5(4) -// c1 = 0 // Butcher tableau coefficients for Dormand–Prince 5(4) method +// https://en.wikipedia.org/wiki/Dormand%E2%80%93Prince_method static constexpr double c[7] = {0.0, 1.0 / 5.0, 3.0 / 10.0, 4.0 / 5.0, 8.0 / 9.0, 1.0, 1.0}; - static constexpr double a[7][6] = { {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {1.0 / 5.0, 0.0, 0.0, 0.0, 0.0, 0.0}, @@ -18,11 +16,9 @@ static constexpr double a[7][6] = { {19372.0 / 6561.0, -25360.0 / 2187.0, 64448.0 / 6561.0, -212.0 / 729.0, 0.0, 0.0}, {9017.0 / 3168.0, -355.0 / 33.0, 46732.0 / 5247.0, 49.0 / 176.0, -5103.0 / 18656.0, 0.0}, {35.0 / 384.0, 0.0, 500.0 / 1113.0, 125.0 / 192.0, -2187.0 / 6784.0, 11.0 / 84.0}}; - // b (5th-order) static constexpr double b[7] = {35.0 / 384.0, 0.0, 500.0 / 1113.0, 125.0 / 192.0, -2187.0 / 6784.0, 11.0 / 84.0, 0.0}; - -// b* (4th-order, “star”) +// b* (4th-order) static constexpr double bs[7] = {5179.0 / 57600.0, 0.0, 7571.0 / 16695.0, 393.0 / 640.0, -92097.0 / 339200.0, 187.0 / 2100.0, 1.0 / 40.0}; @@ -64,6 +60,7 @@ AtPropagator::XYZVector AtPropagator::d2xds2(const XYZPoint &pos, const XYZVecto void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &surface, AtStepper &stepper) { LOG(info) << "Propagating to measurement surface"; + fState.h = stepper.GetInitialStep(); // Set the initial step size auto KE_initial = Kinematics::KE(fState.fMom, fState.fMass); stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; @@ -73,7 +70,7 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur LOG(debug) << "Momentum: " << GetMomentum().X() << ", " << GetMomentum().Y() << ", " << GetMomentum().Z(); auto result = stepper.Step(fState); - if (!result.success) { + if (!result) { LOG(error) << "Integration step failed, aborting propagation."; return; // Abort propagation if step failed } @@ -101,7 +98,7 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur fState.fPos = fState.fLastPos; // Set position to last position fState.fMom = fState.fLastMom; // Set momentum to last momentum result = stepper.Step(fState); - if (!result.success) { + if (!result) { LOG(error) << "Failed to propagate to measurement point, aborting."; return; // Abort propagation if step failed } @@ -130,7 +127,7 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur fState.fPos = fState.fLastPos; // Set position to last position fState.fMom = fState.fLastMom; // Set momentum to last momentum result = stepper.Step(fState); - if (!result.success) { + if (!result) { LOG(error) << "Failed to propagate to stopping point, aborting."; return; // Abort propagation if step failed } @@ -153,7 +150,7 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur LOG(info) << "Propagating to surface after stopping with step size: " << h << " mm"; fState.h = h * 1e-3; // Convert to meters for the RK4 step result = stepper.Step(fState); - if (!result.success) { + if (!result) { LOG(error) << "Failed to propagate to surface after stopping, aborting."; return; // Abort propagation if step failed } @@ -238,22 +235,22 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur fScalingFactor = 1; // Reset scaling factor after convergence } -AtStepper::StepState AtRK4Stepper::Step(const StepState &state) const +AtPropagator::StepState AtRK4Stepper::Step(const AtPropagator::StepState &state) const { - // Take h to be the step size in m. - StepState result = state; + + auto result = state; result.fLastPos = state.fPos; result.fLastMom = state.fMom; result.hUsed = state.h; - result.success = true; + result.status = AtPropagator::StepStateStatus::kSuccess; auto h = state.h; // Step size in m auto fPos = state.fPos; auto fMom = state.fMom; - LOG(info) << "Starting RK4 step with initial position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); - LOG(info) << "Initial momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); - LOG(info) << "Step size (h): " << h << " m"; + LOG(debug) << "Starting RK4 step with initial position: " << fPos.X() << ", " << fPos.Y() << ", " << fPos.Z(); + LOG(debug) << "Initial momentum: " << fMom.X() << ", " << fMom.Y() << ", " << fMom.Z(); + LOG(debug) << "Step size (h): " << h << " m"; auto [x_k1, p_k1] = fDeriv(fPos, fMom); // The derivative of the position is then just the unit vector of the momentum. @@ -274,8 +271,8 @@ AtStepper::StepState AtRK4Stepper::Step(const StepState &state) const auto dpds_SI = (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) / 6; // "Force" in SI units (N) auto dxds_SI = (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) / 6; // Position derivative in SI units (m) - LOG(info) << "dp/ds (SI units): " << dpds_SI.X() << ", " << dpds_SI.Y() << ", " << dpds_SI.Z(); - LOG(info) << "dx/ds (SI units): " << dxds_SI.X() << ", " << dxds_SI.Y() << ", " << dxds_SI.Z(); + LOG(debug) << "dp/ds (SI units): " << dpds_SI.X() << ", " << dpds_SI.Y() << ", " << dpds_SI.Z(); + LOG(debug) << "dx/ds (SI units): " << dxds_SI.X() << ", " << dxds_SI.Y() << ", " << dxds_SI.Z(); auto mom_SI = fReltoSImom * fMom; mom_SI += dpds_SI * h; // Update momentum in SI units (kg m/s) @@ -288,13 +285,14 @@ AtStepper::StepState AtRK4Stepper::Step(const StepState &state) const return result; } -AtStepper::StepState AtRK4AdaptiveStepper::Step(const StepState &state) const +AtPropagator::StepState AtRK4AdaptiveStepper::Step(const AtPropagator::StepState &state) const { + // Take h to be the step size in m. - StepState result = state; + auto result = state; result.fLastPos = state.fPos; result.fLastMom = state.fMom; - result.success = true; + result.status = AtPropagator::StepStateStatus::kSuccess; auto h = state.h; // Step size in m auto fPos = state.fPos; @@ -386,10 +384,10 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(const StepState &state) const auto p_4_MeV = p_new_4 / fReltoSImom; // Convert back to MeV/c auto x_5_mm = x_new_5 * 1e3; // Convert back to mm auto p_5_MeV = p_new_5 / fReltoSImom; // Convert back to MeV/c - LOG(info) << "New position (5th order): " << x_5_mm.X() << ", " << x_5_mm.Y() << ", " << x_5_mm.Z(); - LOG(info) << "New momentum (5th order): " << p_5_MeV.X() << ", " << p_5_MeV.Y() << ", " << p_5_MeV.Z(); - LOG(info) << "New position (4th order): " << x_4_mm.X() << ", " << x_4_mm.Y() << ", " << x_4_mm.Z(); - LOG(info) << "New momentum (4th order): " << p_4_MeV.X() << ", " << p_4_MeV.Y() << ", " << p_4_MeV.Z(); + LOG(debug) << "New position (5th order): " << x_5_mm.X() << ", " << x_5_mm.Y() << ", " << x_5_mm.Z(); + LOG(debug) << "New momentum (5th order): " << p_5_MeV.X() << ", " << p_5_MeV.Y() << ", " << p_5_MeV.Z(); + LOG(debug) << "New position (4th order): " << x_4_mm.X() << ", " << x_4_mm.Y() << ", " << x_4_mm.Z(); + LOG(debug) << "New momentum (4th order): " << p_4_MeV.X() << ", " << p_4_MeV.Y() << ", " << p_4_MeV.Z(); // Convert back to mm and MeV/c XYZVector x_err = (x_5_mm - x_4_mm); // Error in position (mm) @@ -415,27 +413,27 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(const StepState &state) const // Accept the step result.fPos = x_5_mm; // Update position in mm result.fMom = p_5_MeV; // Update momentum in MeV/c - LOG(info) << "Accepted step with error: " << err; - LOG(info) << "Step size: " << h << " m"; - LOG(info) << "New step size: " << hNew << " m"; - LOG(info) << "New Position: " << result.fPos.X() << ", " << result.fPos.Y() << ", " << result.fPos.Z(); - LOG(info) << "New Momentum: " << result.fMom.X() << ", " << result.fMom.Y() << ", " << result.fMom.Z(); - - result.h = hNew; // Adjust the step size for the next iteration - result.hUsed = h; // Store the step size used - result.success = true; // Step accepted + LOG(debug) << "Accepted step with error: " << err; + LOG(debug) << "Step size: " << h << " m"; + LOG(debug) << "New step size: " << hNew << " m"; + LOG(debug) << "New Position: " << result.fPos.X() << ", " << result.fPos.Y() << ", " << result.fPos.Z(); + LOG(debug) << "New Momentum: " << result.fMom.X() << ", " << result.fMom.Y() << ", " << result.fMom.Z(); + + result.h = hNew; // Adjust the step size for the next iteration + result.hUsed = h; // Store the step size used + result.status = AtPropagator::StepStateStatus::kSuccess; // Step accepted return result; } else { // Reject the step and reduce the step size - LOG(info) << "Rejected step with error: " << err; - LOG(info) << "Step size: " << h << " m"; - LOG(info) << "Reducing step size to: " << hNew << " m"; + LOG(debug) << "Rejected step with error: " << err; + LOG(debug) << "Step size: " << h << " m"; + LOG(debug) << "Reducing step size to: " << hNew << " m"; result.h = hNew; // Reduce step size for next iteration h = hNew; // Update h for the next iteration if (result.h < fMinStep || result.h > fMaxStep) { LOG(error) << "Step size out of bounds, aborting propagation."; - result.success = false; + result.status = AtPropagator::StepStateStatus::kInvalidStepSize; result.hUsed = h; return result; // Abort propagation if step size is out of bounds } @@ -443,7 +441,7 @@ AtStepper::StepState AtRK4AdaptiveStepper::Step(const StepState &state) const } } -bool AtMeasurementPoint::PassedSurface(AtStepper::StepState &result) const +bool AtMeasurementPoint::PassedSurface(AtPropagator::StepState &result) const { // Check if the particle has passed the measurement point auto lastDeriv = (fPoint - result.fLastPos).Dot(result.fLastMom.Unit()); @@ -452,7 +450,7 @@ bool AtMeasurementPoint::PassedSurface(AtStepper::StepState &result) const return lastDeriv * currDeriv <= 0; } -bool AtMeasurementPlane::PassedSurface(AtStepper::StepState &result) const +bool AtMeasurementPlane::PassedSurface(AtPropagator::StepState &result) const { // Check if the particle has crossed the plane this step. auto prevSign = fPlane.Distance(result.fLastPos) > 0 ? 1 : -1; diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index e1c357dfa..f5e4b13b9 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -12,63 +12,7 @@ namespace AtTools { class AtMeasurementSurface; -class AtStepper { -public: - struct StepState { - ROOT::Math::XYZPoint fPos; /// Position of the particle in mm - ROOT::Math::XYZVector fMom; /// Momentum of the particle in MeV/c - ROOT::Math::XYZPoint fLastPos; /// Last position of the particle in mm - ROOT::Math::XYZVector fLastMom; /// Last momentum of the particle in MeV/c - double fMass; /// Mass of the particle in MeV/c^2 - double fQ; /// Charge of the particle in Coulombs - double h; /// Step size to use in m - double hUsed; /// Step size used in this step in m - bool success; /// Whether the step was successful - }; - /** - * @brief Function type defining the derivative of the position and momentum w.r.t. distance. - * - * This function takes the current position and momentum and returns the derivate of the position and momentum. - * - * @param pos Current position of the particle in mm. - * @param mom Current momentum of the particle in MeV/c. - * @return A pair containing the derivatives of the position and momentum in SI units (m and kg m/s). - * The first element is the derivative of the position, and the second element is the derivative - * of the momentum. - */ - using DerivFunc = std::function( - const ROOT::Math::XYZPoint &, const ROOT::Math::XYZVector &)>; - - DerivFunc fDeriv; - - virtual StepState Step(const StepState &state) const = 0; - -protected: - static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) -}; - -/** - * @brief Class for measurement surface in the AT-TPC. - * - * This class represents a measurement surface or point in the AT-TPC. It's used to define the stopping - * point and behavior of the propagator. - */ -class AtMeasurementSurface { -public: - bool fClipToSurface = false; // Whether to clip to the surface - - /** - * @brief Calculate the distance from the position to the surface. - */ - virtual double Distance(const ROOT::Math::XYZPoint &pos) const = 0; - - /** - * @brief Check if we have passed the surface between the last position and the current position. - */ - virtual bool PassedSurface(AtStepper::StepState &result) const = 0; - - virtual ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const = 0; -}; +class AtStepper; /** * @brief Class for propagating particles through a medium. @@ -82,6 +26,25 @@ class AtMeasurementSurface { * class if the material or particle type changes. */ class AtPropagator { +public: + enum class StepStateStatus { + kSuccess, /// Step was successful + kInvalidStepSize /// Step failed + }; + struct StepState { + ROOT::Math::XYZPoint fPos; /// Position of the particle in mm + ROOT::Math::XYZVector fMom; /// Momentum of the particle in MeV/c + ROOT::Math::XYZPoint fLastPos; /// Last position of the particle in mm + ROOT::Math::XYZVector fLastMom; /// Last momentum of the particle in MeV/c + double fMass = 0; /// Mass of the particle in MeV/c^2 + double fQ = 0; /// Charge of the particle in Coulombs + double h = 0; /// Step size to use in m + double hUsed = 0; /// Step size used in this step in m + StepStateStatus status = StepStateStatus::kSuccess; /// Whether the step was successful + + operator bool() const { return status == StepStateStatus::kSuccess; } + }; + protected: using XYZVector = ROOT::Math::XYZVector; using XYZPoint = ROOT::Math::XYZPoint; @@ -94,7 +57,7 @@ class AtPropagator { // Internal state variables for the propagator double fScalingFactor = 1.0; /// Scaling factor for energy loss - AtStepper::StepState fState; /// Current state of the particle + StepState fState; /// Current state of the particle // Tolerances and limits double fETol = 1e-4; /// Energy tolerance for convergence when fixing energy loss @@ -115,8 +78,8 @@ class AtPropagator { { fState.fMass = mass; fState.fQ = charge; - fState.h = 1e-4; // Initial step size in m } + /** * @brief Set the electric field (V/m) */ @@ -141,8 +104,6 @@ class AtPropagator { fState.fMom = mom; } - void SetH(double h) { fState.h = h; } // Set the step size in m - XYZPoint GetPosition() const { return fState.fPos; } XYZVector GetMomentum() const { return fState.fMom; } @@ -201,22 +162,73 @@ class AtPropagator { */ XYZVector d2xds2(const XYZPoint &pos, const XYZVector &mom) const; }; +class AtStepper { +public: + /** + * @brief Function type defining the derivative of the position and momentum w.r.t. distance. + * + * This function takes the current position and momentum and returns the derivate of the position and momentum. + * + * @param pos Current position of the particle in mm. + * @param mom Current momentum of the particle in MeV/c. + * @return A pair containing the derivatives of the position and momentum in SI units (m and kg m/s). + * The first element is the derivative of the position, and the second element is the derivative + * of the momentum. + */ + using DerivFunc = std::function( + const ROOT::Math::XYZPoint &, const ROOT::Math::XYZVector &)>; + + DerivFunc fDeriv; + + virtual AtPropagator::StepState Step(const AtPropagator::StepState &state) const = 0; + virtual double GetInitialStep() const { return 1e-4; } /// Default initial step size in m + +protected: + static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) +}; class AtRK4Stepper : public AtStepper { + double fStepSize = 1e-4; + public: - StepState Step(const StepState &state) const override; + AtPropagator::StepState Step(const AtPropagator::StepState &state) const override; + double GetInitialStep() const override { return fStepSize; } /// Default initial step size in m }; class AtRK4AdaptiveStepper : public AtStepper { public: - double fAtolPos = 1e-2; /// Absolute tolerance for position in mm - double fAtolMom = 1e-2; /// Absolute tolerance for momentum in MeV/c - double fRtol = 1e-6; /// Relative tolerance for position and momentum - double fMinStep = 1e-6; /// Minimum step size in m - double fMaxStep = 10.0; /// Maximum step size in m - - StepState Step(const StepState &state) const override; + double fAtolPos = 1e-2; /// Absolute tolerance for position in mm + double fAtolMom = 1e-2; /// Absolute tolerance for momentum in MeV/c + double fRtol = 1e-6; /// Relative tolerance for position and momentum + double fMinStep = 1e-6; /// Minimum step size in m + double fMaxStep = 10.0; /// Maximum step size in m + double fInitialStep = 1e-4; /// Initial step size in m + + AtPropagator::StepState Step(const AtPropagator::StepState &state) const override; + double GetInitialStep() const override { return fInitialStep; } /// Default initial step size in m }; +/** + * @brief Class for measurement surface in the AT-TPC. + * + * This class represents a measurement surface or point in the AT-TPC. It's used to define the stopping + * point and behavior of the propagator. + */ +class AtMeasurementSurface { +public: + bool fClipToSurface = false; // Whether to clip to the surface + + /** + * @brief Calculate the distance from the position to the surface. + */ + virtual double Distance(const ROOT::Math::XYZPoint &pos) const = 0; + + /** + * @brief Check if we have passed the surface between the last position and the current position. + */ + virtual bool PassedSurface(AtPropagator::StepState &result) const = 0; + + virtual ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const = 0; +}; class AtMeasurementPoint : public AtMeasurementSurface { protected: ROOT::Math::XYZPoint fPoint; // The measurement point in mm @@ -225,7 +237,7 @@ class AtMeasurementPoint : public AtMeasurementSurface { AtMeasurementPoint(const ROOT::Math::XYZPoint &point) : fPoint(point) {} double Distance(const ROOT::Math::XYZPoint &pos) const override { return (fPoint - pos).R(); } - bool PassedSurface(AtStepper::StepState &result) const override; + bool PassedSurface(AtPropagator::StepState &result) const override; ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const override { return fPoint; } }; @@ -236,7 +248,7 @@ class AtMeasurementPlane : public AtMeasurementSurface { AtMeasurementPlane(const ROOT::Math::Plane3D &plane) : fPlane(plane) { fClipToSurface = true; } double Distance(const ROOT::Math::XYZPoint &pos) const override { return std::abs(fPlane.Distance(pos)); } - bool PassedSurface(AtStepper::StepState &result) const override; + bool PassedSurface(AtPropagator::StepState &result) const override; ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const override; }; } // namespace AtTools diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 75467531b..608cb220a 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -169,8 +169,6 @@ TEST(AtPropagatorTest, PropagateToPoint_NoField) ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); - XYZPoint targetPoint(10, 0, 0); // Target point to propagate to 10 mm - // propagator.PropagateToPoint(targetPoint, stepper); propagator.PropagateToMeasurementSurface(measurementPoint, stepper); auto finalPos = propagator.GetPosition(); @@ -278,7 +276,7 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) propagator.SetState(startPos, startMom); propagator.SetEField({0, 0, 0}); // No electric field propagator.SetBField({0, 0, 0}); // No magnetic field - propagator.SetH(1); // Set initial step size to 1 s + stepper.fInitialStep = 1; // Set initial step size to 1 m ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); @@ -293,7 +291,7 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) propagator.SetState(startPos, startMom); propagator.SetEField({0, 0, 0}); // No electric field propagator.SetBField({0, 0, 0}); // No magnetic field - propagator.SetH(1e-6); // Set initial step size to 1e-6 m + stepper.fInitialStep = 1e-6; // Set initial step size to 1e-6 m ASSERT_NEAR(propagator.GetMomentum().X(), 43.331, 1e-1); From 9d06ade3f2e4940053753e3a644cf51d7a5ae857 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 22 Jul 2025 20:36:14 +0200 Subject: [PATCH 32/75] Fix comments that implied AtEnergyLoss density was mg/cm^3 --- AtTools/AtELossModel.cxx | 4 +++- AtTools/AtELossTable.cxx | 9 +++++++-- AtTools/AtPropagator.cxx | 4 +++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/AtTools/AtELossModel.cxx b/AtTools/AtELossModel.cxx index 7ad30bdca..bda0f6966 100644 --- a/AtTools/AtELossModel.cxx +++ b/AtTools/AtELossModel.cxx @@ -1,10 +1,12 @@ #include "AtELossModel.h" +#include "FairLogger.h" + #include namespace AtTools { /** - * Set the density of the material we are calculating energy losses for in mg/cm^3. + * Set the density of the material we are calculating energy losses for in g/cm^3. * Likely not fully tested, but I want to keep it around to remind myself of it. */ void AtELossModel::SetDensity(double density) diff --git a/AtTools/AtELossTable.cxx b/AtTools/AtELossTable.cxx index c31ba26ed..8f093a118 100644 --- a/AtTools/AtELossTable.cxx +++ b/AtTools/AtELossTable.cxx @@ -198,7 +198,7 @@ void AtELossTable::LoadSrimTable(std::string fileName) try { if (atConversion && tokens.at(1) == "MeV" && tokens.at(3) == "mm") { conversion = std::stod(tokens.at(0)); - LOG(info) << "Using conversion factor of " << conversion; + LOG(info) << "Using conversion factor of " << conversion << " from " << tokens.at(0); break; } @@ -221,8 +221,13 @@ void AtELossTable::LoadSrimTable(std::string fileName) } } // end loop over file - for (auto &dedx : dEdX) + for (auto &dedx : dEdX) { dedx *= conversion; + } + + for (int i = 0; i < dEdX.size(); ++i) { + LOG(debug) << "Energy: " << energy[i] << " MeV dEdX: " << dEdX[i] << " MeV/mm"; + } LoadTable(energy, dEdX); LoadRangeVariance(energy, rangeVar); diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index fd526d77f..ee1a0cb6c 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -33,6 +33,7 @@ AtPropagator::XYZVector AtPropagator::Force(XYZPoint pos, XYZVector mom) const auto F_lorentz = fState.fQ * (fEField + v.Cross(fBField)); LOG(debug) << "F_lorentz: " << F_lorentz; + auto dedx = fScalingFactor * fELossModel->GetdEdx(Kinematics::KE(mom, fState.fMass)); // Stopping power in MeV/mm auto dedx_si = dedx * 1.60218e-10; // de_dx in SI units (J/m) @@ -66,7 +67,8 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; while (true) { - LOG(debug) << "Position: " << GetPosition().X() << ", " << GetPosition().Y() << ", " << GetPosition().Z(); + LOG(info) << "Position: " << GetPosition().X() / 10 << ", " << GetPosition().Y() / 10 << ", " + << GetPosition().Z() / 10; LOG(debug) << "Momentum: " << GetMomentum().X() << ", " << GetMomentum().Y() << ", " << GetMomentum().Z(); auto result = stepper.Step(fState); From caffb8b6fe7bae471ecdaaeff07c345c6d0ea1df Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 23 Jul 2025 11:41:36 +0200 Subject: [PATCH 33/75] Possibly make tests portable. --- AtTools/AtPropagator.h | 7 +-- AtTools/AtPropagatorTest.cxx | 84 +++++++++++++++++++++++++++++++----- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index f5e4b13b9..0fd07435c 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -56,8 +56,7 @@ class AtPropagator { const std::unique_ptr fELossModel; // Energy loss model // Internal state variables for the propagator - double fScalingFactor = 1.0; /// Scaling factor for energy loss - StepState fState; /// Current state of the particle + StepState fState; /// Current state of the particle // Tolerances and limits double fETol = 1e-4; /// Energy tolerance for convergence when fixing energy loss @@ -67,6 +66,8 @@ class AtPropagator { static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) public: + double fScalingFactor = 1.0; /// Scaling factor for energy loss + /** * @brief Constructor for AtPropagator. * @param charge Charge of the particle in Coulombs. @@ -238,7 +239,7 @@ class AtMeasurementPoint : public AtMeasurementSurface { double Distance(const ROOT::Math::XYZPoint &pos) const override { return (fPoint - pos).R(); } bool PassedSurface(AtPropagator::StepState &result) const override; - ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const override { return fPoint; } + ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const override { return pos; } }; class AtMeasurementPlane : public AtMeasurementSurface { diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 608cb220a..81cd1ad5b 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -103,8 +103,9 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) double charge = charge_p; // Charge in Coulombs double mass = mass_p; // Mass in MeV/c^2 auto elossModel = std::make_unique(0); - elossModel->LoadSrimTable( - "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + // elossModel->LoadSrimTable( + // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4Stepper stepper; AtMeasurementPoint measurementPoint({1e3, 0, 0}); @@ -147,8 +148,9 @@ TEST(AtPropagatorTest, PropagateToPoint_NoField) double charge = charge_p; // Charge in Coulombs double mass = mass_p; // Mass in MeV/c^2 auto elossModel = std::make_unique(0); - elossModel->LoadSrimTable( - "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + // elossModel->LoadSrimTable( + // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4Stepper stepper; AtMeasurementPoint measurementPoint({10, 0, 0}); @@ -183,8 +185,9 @@ TEST(AtPropagatorTest, PropagateToPlane_NoField) double charge = charge_p; // Charge in Coulombs double mass = mass_p; // Mass in MeV/c^2 auto elossModel = std::make_unique(0); - elossModel->LoadSrimTable( - "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + // elossModel->LoadSrimTable( + // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4Stepper stepper; @@ -222,8 +225,9 @@ TEST(AtPropagatorTest, PropagateToPlane_StoppingNoField) double charge = charge_p; // Charge in Coulombs double mass = mass_p; // Mass in MeV/c^2 auto elossModel = std::make_unique(0); - elossModel->LoadSrimTable( - "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + // elossModel->LoadSrimTable( + // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4Stepper stepper; @@ -257,8 +261,9 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) double charge = charge_p; // Charge in Coulombs double mass = mass_p; // Mass in MeV/c^2 auto elossModel = std::make_unique(0); - elossModel->LoadSrimTable( - "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + // elossModel->LoadSrimTable( + // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4AdaptiveStepper stepper; AtMeasurementPoint measurementPoint({10, 0, 0}); @@ -302,4 +307,61 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) ASSERT_NEAR(finalPos.X(), 10, 10 * 1e-3); // Final position in x-direction should be close to 10 mm ASSERT_NEAR(finalMom.X(), p_fin, 0.1); -} \ No newline at end of file +} + +TEST(AtPropagatorTest, PropagateToPoint_Field) +{ + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + auto elossModel = std::make_unique(0); + // elossModel->LoadSrimTable( + // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); + elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools + elossModel->SetDensity(3.3084e-05); // Set density in g/cm^3 for 300 torr H2 + AtPropagator propagator(charge, mass, std::move(elossModel)); + propagator.SetEField({0, 0, 0}); // No electric field + propagator.SetBField({0, 0, 2.85}); // Magnetic field + AtRK4Stepper stepper; + + XYZPoint startPos(-3.40046e-05, -1.49863e-05, 0.10018); // Start position in cm + startPos *= 10; // Convert to mm + XYZVector startMom(0.00935463, -0.0454279, 0.00826042); // Start momentum in GeV/c + startMom *= 1e3; // Convert to MeV/c + + auto KE = Kinematics::KE(startMom, mass); // Convert momentum to kinetic energy + std::cout << "Propagating proton with KE: " << KE << " MeV" << std::endl; + std::cout << "Initial position: " << startPos.X() << ", " << startPos.Y() << ", " << startPos.Z() << std::endl; + + propagator.SetState(startPos, startMom); + + XYZPoint point({-1.4895, -4.8787, 1.01217}); // measurement point in cm + point *= 10; // Convert to mm + AtMeasurementPoint measurementPoint(point); + + propagator.PropagateToMeasurementSurface(measurementPoint, stepper); + + auto finalPos = propagator.GetPosition(); + auto finalMom = propagator.GetMomentum(); + + ASSERT_NEAR(finalPos.X(), point.X(), 1); // Check final position is within 1 mm of the measurement point + ASSERT_NEAR(finalPos.Y(), point.Y(), 1); + ASSERT_NEAR(finalPos.Z(), point.Z(), 1); + std::cout << "Difference in position: " << measurementPoint.Distance(finalPos) << " mm" << std::endl; + + /*** Propagate to new measurement point ****/ + propagator.SetState(startPos, startMom); + + point = XYZPoint({-3.6942, -6.13106, 1.45025}); // measurement point in cm + point *= 10; // Convert to mm + measurementPoint = AtMeasurementPoint(point); + + propagator.PropagateToMeasurementSurface(measurementPoint, stepper); + + finalPos = propagator.GetPosition(); + finalMom = propagator.GetMomentum(); + + ASSERT_NEAR(finalPos.X(), point.X(), 1); // Check final position is within 1 mm of the measurement point + ASSERT_NEAR(finalPos.Y(), point.Y(), 1); + ASSERT_NEAR(finalPos.Z(), point.Z(), 1); + std::cout << "Difference in position: " << measurementPoint.Distance(finalPos) << " mm" << std::endl; +} From 2d24c7aaae6e67962d3e60042e32c71e498bba35 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 23 Jul 2025 11:47:15 +0200 Subject: [PATCH 34/75] Try to setup env in github runner --- AtTools/AtPropagatorTest.cxx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 81cd1ad5b..8f1e67a5c 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -17,6 +17,14 @@ using namespace AtTools; const double mass_p = 938.272; // Mass of proton in MeV/c^2 const double charge_p = 1.602176634e-19; // Charge of proton +std::string getEnergyPath() +{ + auto env = std::getenv("VMCWORKDIR"); + if (env == nullptr) { + return "../../resources/energy_loss/HinH.txt"; // Default path assuming cwd is build/AtTools + } + return std::string(env) + "/resources/energy_loss/HinH.txt"; // Use environment variable +} class DummyELossModel : public AtELossModel { public: double eLoss = 1; @@ -105,7 +113,8 @@ TEST(AtPropagatorTest, PropagateToPoint_StoppingNoField) auto elossModel = std::make_unique(0); // elossModel->LoadSrimTable( // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); - elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools + + elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4Stepper stepper; AtMeasurementPoint measurementPoint({1e3, 0, 0}); @@ -150,7 +159,7 @@ TEST(AtPropagatorTest, PropagateToPoint_NoField) auto elossModel = std::make_unique(0); // elossModel->LoadSrimTable( // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); - elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools + elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4Stepper stepper; AtMeasurementPoint measurementPoint({10, 0, 0}); @@ -187,7 +196,7 @@ TEST(AtPropagatorTest, PropagateToPlane_NoField) auto elossModel = std::make_unique(0); // elossModel->LoadSrimTable( // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); - elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools + elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4Stepper stepper; @@ -227,7 +236,7 @@ TEST(AtPropagatorTest, PropagateToPlane_StoppingNoField) auto elossModel = std::make_unique(0); // elossModel->LoadSrimTable( // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); - elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools + elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4Stepper stepper; @@ -263,7 +272,7 @@ TEST(AtPropagatorTest, PropagateToPointAdaptive_NoField) auto elossModel = std::make_unique(0); // elossModel->LoadSrimTable( // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); - elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools + elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path AtPropagator propagator(charge, mass, std::move(elossModel)); AtRK4AdaptiveStepper stepper; AtMeasurementPoint measurementPoint({10, 0, 0}); @@ -316,8 +325,8 @@ TEST(AtPropagatorTest, PropagateToPoint_Field) auto elossModel = std::make_unique(0); // elossModel->LoadSrimTable( // "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); - elossModel->LoadSrimTable("../../resources/energy_loss/HinH.txt"); // Assumes cwd is build/AtTools - elossModel->SetDensity(3.3084e-05); // Set density in g/cm^3 for 300 torr H2 + elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path + elossModel->SetDensity(3.3084e-05); // Set density in g/cm^3 for 300 torr H2 AtPropagator propagator(charge, mass, std::move(elossModel)); propagator.SetEField({0, 0, 0}); // No electric field propagator.SetBField({0, 0, 2.85}); // Magnetic field From 0f009dcf0cd813e9658009fe479512e244d9427d Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 23 Jul 2025 11:59:13 +0200 Subject: [PATCH 35/75] Second attempt at setting CI env --- .github/workflows/CI-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/CI-tests.yml b/.github/workflows/CI-tests.yml index 810be15de..1f4652135 100644 --- a/.github/workflows/CI-tests.yml +++ b/.github/workflows/CI-tests.yml @@ -25,6 +25,9 @@ jobs: # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + - name: Configure Environment + run: source ${{github.workspace}}/build/config.sh + - name: Build # Build your program with the given configuration run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} -j $(nproc) From 8a29bd05f3281409ad4333899f5eb33534126829 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 23 Jul 2025 12:04:15 +0200 Subject: [PATCH 36/75] Try 3 - didn't run at all --- .github/workflows/CI-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CI-tests.yml b/.github/workflows/CI-tests.yml index 1f4652135..fd8ccb341 100644 --- a/.github/workflows/CI-tests.yml +++ b/.github/workflows/CI-tests.yml @@ -27,6 +27,7 @@ jobs: - name: Configure Environment run: source ${{github.workspace}}/build/config.sh + shell: bash - name: Build # Build your program with the given configuration From aacb5273aba9549ef6d59b678a0182f41d75dcc9 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 23 Jul 2025 12:06:54 +0200 Subject: [PATCH 37/75] Probably really works --- .github/workflows/CI-tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/CI-tests.yml b/.github/workflows/CI-tests.yml index fd8ccb341..810be15de 100644 --- a/.github/workflows/CI-tests.yml +++ b/.github/workflows/CI-tests.yml @@ -25,10 +25,6 @@ jobs: # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} - - name: Configure Environment - run: source ${{github.workspace}}/build/config.sh - shell: bash - - name: Build # Build your program with the given configuration run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} -j $(nproc) From 9b41edb9141f1e0070761875af5dc2f33f5cd8fa Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 23 Jul 2025 12:27:55 +0200 Subject: [PATCH 38/75] Add integration test for drawing figures --- AtTools/AtPropagator.cxx | 15 ++ AtTools/AtPropagator.h | 9 + macro/tests/UKF/AtPropagator.C | 76 ++++++++ macro/tests/UKF/hits.txt | 314 +++++++++++++++++++++++++++++++++ 4 files changed, 414 insertions(+) create mode 100644 macro/tests/UKF/AtPropagator.C create mode 100644 macro/tests/UKF/hits.txt diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index ee1a0cb6c..da36cca07 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -58,6 +58,21 @@ AtPropagator::XYZVector AtPropagator::d2xds2(const XYZPoint &pos, const XYZVecto return 1 / p * (dpds_vec - phat * (phat.Dot(dpds_vec))); // Second derivative of position w.r.t. arc length } +void AtPropagator::PropagateOneStep(AtStepper &stepper) +{ + if (fState.h == 0) + fState.h = stepper.GetInitialStep(); // Set the initial step size + + stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; + + auto result = stepper.Step(fState); + if (!result) { + LOG(error) << "Integration step failed, aborting propagation."; + return; // Abort propagation if step failed + } + fState = result; // Update the internal state +} + void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &surface, AtStepper &stepper) { LOG(info) << "Propagating to measurement surface"; diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 0fd07435c..2e199b497 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -104,6 +104,7 @@ class AtPropagator { fState.fPos = pos; fState.fMom = mom; } + const StepState &GetState() const { return fState; } XYZPoint GetPosition() const { return fState.fPos; } XYZVector GetMomentum() const { return fState.fMom; } @@ -121,6 +122,14 @@ class AtPropagator { void PropagateToMeasurementSurface(const AtMeasurementSurface &surface, AtStepper &stepper); + /** + * @brief Propagate the particle using the given stepper. + * Propagates one step using the provided stepper. + * + * @param stepper The stepper to use for propagation. + */ + void PropagateOneStep(AtStepper &stepper); + /** * @brief Calculate the force acting on the particle. * diff --git a/macro/tests/UKF/AtPropagator.C b/macro/tests/UKF/AtPropagator.C new file mode 100644 index 000000000..80774a23c --- /dev/null +++ b/macro/tests/UKF/AtPropagator.C @@ -0,0 +1,76 @@ +std::string getEnergyPath() +{ + auto env = std::getenv("VMCWORKDIR"); + if (env == nullptr) { + return "../../resources/energy_loss/HinH.txt"; // Default path assuming cwd is build/AtTools + } + return std::string(env) + "/resources/energy_loss/HinH.txt"; // Use environment variable +} + +const double mass_p = 938.272; // Mass of proton in MeV/c^2 +const double charge_p = 1.602176634e-19; // Charge of proton + +// This test should plot the trajectory of a particle in a magnetic field using +// the output from GEANT and the AtPropagator class. +void AtPropagator() +{ + using namespace AtTools; + + std::vector x, y, z; + std::vector x2, y2, z2; + + std::ifstream infile("hits.txt"); + double xi, yi, zi, Ei; + while (infile >> xi >> yi >> zi >> Ei) { + x.push_back(xi * 10); + y.push_back(yi * 10); + z.push_back(zi * 10); + } + + // Our propagator setup + double charge = charge_p; // Charge in Coulombs + double mass = mass_p; // Mass in MeV/c^2 + auto elossModel = std::make_unique(0); + elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path + elossModel->SetDensity(3.553e-5); // Set density in g/cm^3 for 300 torr H2 + AtTools::AtPropagator propagator(charge, mass, std::move(elossModel)); + propagator.SetEField({0, 0, 0}); // No electric field + propagator.SetBField({0, 0, 2.85}); // Magnetic field + AtTools::AtRK4Stepper stepper; + + XYZPoint startPos(-3.40046e-05, -1.49863e-05, 0.10018); // Start position in cm + startPos *= 10; // Convert to mm + XYZVector startMom(0.00935463, -0.0454279, 0.00826042); // Start momentum in GeV/c + startMom *= 1e3; // Convert to MeV/c + + propagator.SetState(startPos, startMom); + + // Loop through until the particle is stopped + while (Kinematics::KE(propagator.GetState().fMom, propagator.GetState().fMass) > 0.1) { + // Propagate to the next point + propagator.PropagateOneStep(stepper); + + // Get the current position and momentum + auto pos = propagator.GetPosition(); + + // Store the position for plotting + x2.push_back(pos.X()); + y2.push_back(pos.Y()); + z2.push_back(pos.Z()); + } + + TGraph2D *track = new TGraph2D(x.size(), x.data(), y.data(), z.data()); + track->SetTitle("Particle Track;X [mm];Y [mm];Z [mm]"); + track->SetMarkerStyle(20); + track->SetMarkerSize(0.8); + + TGraph2D *track2 = new TGraph2D(x2.size(), x2.data(), y2.data(), z2.data()); + track2->SetTitle("Propagated Particle Track;X [mm];Y [mm];Z [mm]"); + track2->SetMarkerStyle(21); + track2->SetMarkerSize(0.8); + track2->SetMarkerColor(kRed); + + TCanvas *c1 = new TCanvas("c1", "Particle Track", 800, 600); + track->Draw("P"); + track2->Draw("PSAME"); +} \ No newline at end of file diff --git a/macro/tests/UKF/hits.txt b/macro/tests/UKF/hits.txt new file mode 100644 index 000000000..5f76f895d --- /dev/null +++ b/macro/tests/UKF/hits.txt @@ -0,0 +1,314 @@ +-3.40046e-05 -1.49863e-05 0.10018 0 +0.0189471 -0.0966173 0.117714 0.00258037 +0.036285 -0.193528 0.135248 0.0010415 +0.0519068 -0.290728 0.152794 0.000541655 +0.0656858 -0.388206 0.170338 0.00194783 +0.0777186 -0.485911 0.187907 0.00412452 +0.0878053 -0.583817 0.205582 0.00169653 +0.0960944 -0.681881 0.223318 0.00205692 +0.102599 -0.780065 0.241129 0.000151766 +0.107452 -0.878339 0.258972 0.00118051 +0.110508 -0.976677 0.276864 0.00104222 +0.111671 -1.07506 0.294729 0.0035253 +0.111012 -1.17344 0.312619 0.00308229 +0.108501 -1.27179 0.330529 0.00160089 +0.104209 -1.37008 0.348443 0.000583303 +0.0982273 -1.46827 0.366392 0.00753745 +0.0903449 -1.56634 0.384264 0.00259151 +0.0806727 -1.66425 0.402186 0.00123866 +0.0691679 -1.76195 0.420098 0.00378331 +0.0559072 -1.85943 0.438033 0.00155726 +0.0403906 -1.95681 0.454628 0.000931355 +0.0230265 -2.05397 0.470691 0.00707037 +0.00386458 -2.1508 0.486689 0.00451979 +-0.0172429 -2.24723 0.502676 0.000756807 +-0.040074 -2.34325 0.518767 0.00152287 +-0.0646638 -2.43883 0.53488 0.00209089 +-0.0909762 -2.53395 0.550959 0.00253983 +-0.119025 -2.62858 0.567065 0.000634231 +-0.148834 -2.72266 0.583183 0.00193713 +-0.180375 -2.81617 0.599325 0.000131761 +-0.213376 -2.90915 0.615642 0.00343676 +-0.248073 -3.00151 0.631919 0.0029619 +-0.284436 -3.0932 0.648338 0.00100025 +-0.322544 -3.1842 0.664698 0.00209536 +-0.362341 -3.27446 0.681099 0.00147181 +-0.403855 -3.36395 0.697465 0.00263272 +-0.446964 -3.45269 0.713791 0.00307259 +-0.49172 -3.54062 0.73008 0.000251718 +-0.538022 -3.62773 0.74641 0.000878999 +-0.585925 -3.71398 0.762729 0.00357119 +-0.63545 -3.79932 0.778955 0.00164382 +-0.686629 -3.88363 0.795469 0.0012815 +-0.739481 -3.9669 0.811974 0.00430081 +-0.793872 -4.04916 0.828547 0.000666631 +-0.849819 -4.13036 0.845154 0.00341725 +-0.907271 -4.2105 0.861818 0.00646584 +-0.966224 -4.28955 0.878398 0.0016157 +-1.02671 -4.36742 0.895043 0.00470272 +-1.08867 -4.44412 0.911731 0.000640672 +-1.15203 -4.51964 0.928504 0.00518449 +-1.21675 -4.59399 0.945318 0.00193881 +-1.28287 -4.66712 0.962047 0.000694573 +-1.35037 -4.73898 0.978776 0.00125284 +-1.41928 -4.80951 0.995432 0.000281865 +-1.4895 -4.8787 1.01217 0.00203059 +-1.56108 -4.94649 1.02892 0.0011867 +-1.63397 -5.01288 1.04565 0.00125706 +-1.70809 -5.07786 1.06248 0.00812622 +-1.78342 -5.14141 1.0794 0.00211417 +-1.85991 -5.20359 1.09625 0.00246438 +-1.93756 -5.26433 1.113 0.00164815 +-2.01636 -5.32357 1.12976 0.000725691 +-2.09635 -5.38118 1.14655 0.00244181 +-2.17745 -5.43721 1.16334 0.00103049 +-2.25963 -5.49165 1.18015 0.00192944 +-2.34288 -5.54443 1.19699 0.00149925 +-2.42712 -5.59561 1.21388 0.00251826 +-2.51229 -5.64519 1.23083 0.000926565 +-2.59843 -5.69307 1.24776 0.0020667 +-2.68552 -5.73922 1.26464 0.000874783 +-2.77346 -5.78369 1.28164 0.00165935 +-2.86226 -5.82643 1.29861 0.000426512 +-2.95185 -5.86746 1.31565 0.00106954 +-3.0422 -5.90683 1.33254 0.00296486 +-3.13335 -5.94435 1.34937 0.00219723 +-3.22523 -5.98001 1.36629 0.000638691 +-3.31777 -6.01396 1.38312 0.00169432 +-3.41098 -6.04601 1.4 0.000985825 +-3.5048 -6.07624 1.41687 0.00221741 +-3.59923 -6.10458 1.43358 0.000573825 +-3.6942 -6.13106 1.45025 0.00238183 +-3.78972 -6.15558 1.46684 0.00207704 +-3.88566 -6.17838 1.48345 0.000456725 +-3.98202 -6.19931 1.50007 0.000939948 +-4.07874 -6.21849 1.5167 0.000472584 +-4.17584 -6.23571 1.53327 0.00105617 +-4.27324 -6.25105 1.5499 0.0010503 +-4.37094 -6.26453 1.56645 0.00100485 +-4.46887 -6.27607 1.58306 0.00503887 +-4.56699 -6.2857 1.59976 0.00703127 +-4.66531 -6.29321 1.61638 0.00334247 +-4.76374 -6.29885 1.63314 0.00331044 +-4.86224 -6.30255 1.64995 0.00073014 +-4.9608 -6.30434 1.66675 0.0033623 +-5.05941 -6.30428 1.68339 0.00535778 +-5.158 -6.30231 1.69999 0.00125925 +-5.25655 -6.29836 1.71646 0.00204625 +-5.355 -6.29256 1.73302 0.00118473 +-5.45332 -6.28474 1.74952 0.000353303 +-5.55145 -6.2749 1.76601 0.00177079 +-5.64938 -6.2631 1.78248 0.00324638 +-5.74706 -6.24947 1.79897 0.00210638 +-5.84443 -6.2338 1.81553 0.00294219 +-5.94147 -6.21615 1.83199 0.00309201 +-6.03816 -6.19668 1.84845 0.00203747 +-6.13449 -6.17531 1.8647 0.00511082 +-6.23034 -6.15191 1.88094 0.00501013 +-6.32571 -6.12657 1.89718 0.00261361 +-6.42049 -6.09919 1.91348 0.00235029 +-6.51472 -6.06993 1.92974 0.00343822 +-6.60831 -6.0387 1.946 0.00569468 +-6.70125 -6.00557 1.96227 0.00298844 +-6.79347 -5.97049 1.97856 0.00283765 +-6.88492 -5.93348 1.99486 0.00251219 +-6.9756 -5.89462 2.0112 0.00333844 +-7.06541 -5.85385 2.02769 0.00139711 +-7.15432 -5.81119 2.04427 0.00457198 +-7.24233 -5.76668 2.06081 0.00183194 +-7.32941 -5.72039 2.07733 0.00210649 +-7.41551 -5.67229 2.09385 0.00146382 +-7.50064 -5.6225 2.11039 0.00213869 +-7.58468 -5.57094 2.12706 0.0044235 +-7.66763 -5.51765 2.1438 0.000694616 +-7.74947 -5.46267 2.16051 0.00246119 +-7.83012 -5.40594 2.17712 0.0023732 +-7.9096 -5.34756 2.19371 0.00286895 +-7.98777 -5.28744 2.21029 0.000563268 +-8.06466 -5.22571 2.22691 0.00198034 +-8.14019 -5.16232 2.24354 0.00232113 +-8.21438 -5.09739 2.26026 0.00149609 +-8.28724 -5.03093 2.27682 0.00112195 +-8.35863 -4.96291 2.29342 0.00461114 +-8.42857 -4.8934 2.31008 0.00188045 +-8.49702 -4.8224 2.32661 0.00237145 +-8.56394 -4.74996 2.34315 0.000600861 +-8.6293 -4.67611 2.35968 0.00217227 +-8.69297 -4.60078 2.37615 0.00469567 +-8.75504 -4.5241 2.39252 0.00190909 +-8.81544 -4.44612 2.40896 0.00436395 +-8.87398 -4.36673 2.42536 0.00375093 +-8.93089 -4.28614 2.44172 0.00169305 +-8.98603 -4.20437 2.45822 0.00205361 +-9.03946 -4.12146 2.47469 0.00651101 +-9.09112 -4.03747 2.4913 0.00196265 +-9.14097 -3.95242 2.50803 0.000512267 +-9.18891 -3.86627 2.52481 0.00193968 +-9.23497 -3.77914 2.54172 0.00264933 +-9.27901 -3.69096 2.55858 0.00512826 +-9.32122 -3.60189 2.57545 0.00234888 +-9.36127 -3.51181 2.59219 0.0041278 +-9.39927 -3.42083 2.60889 0.00394476 +-9.43523 -3.32903 2.62559 0.00626612 +-9.4692 -3.23646 2.64222 0.000954117 +-9.50126 -3.14323 2.65897 0.00068102 +-9.53154 -3.04941 2.67572 0.00117896 +-9.55987 -2.95499 2.69251 0.00440072 +-9.58636 -2.86011 2.70969 0.00454022 +-9.61094 -2.76471 2.72688 0.00324835 +-9.63324 -2.66874 2.74393 0.00130486 +-9.65335 -2.57229 2.76103 0.00461174 +-9.67143 -2.47544 2.77816 0.00257374 +-9.68746 -2.37823 2.79528 0.00149974 +-9.70129 -2.28067 2.81234 0.00222129 +-9.71289 -2.1828 2.82926 0.00402998 +-9.72231 -2.08472 2.84631 0.00510242 +-9.72954 -1.98643 2.86322 0.00427729 +-9.73469 -1.888 2.88007 0.00285612 +-9.73773 -1.78947 2.89691 0.00215436 +-9.73855 -1.69093 2.91392 0.00178831 +-9.73715 -1.5924 2.93087 0.00886926 +-9.7336 -1.4939 2.94776 0.00359489 +-9.72787 -1.39549 2.96459 0.00169257 +-9.71993 -1.29722 2.9813 0.00271018 +-9.70987 -1.19916 2.99809 0.00207105 +-9.69759 -1.10135 3.01489 0.00110787 +-9.68311 -1.00385 3.03175 0.00443925 +-9.66639 -0.906729 3.04869 0.00466506 +-9.64755 -0.809974 3.06552 0.00218618 +-9.62655 -0.713686 3.08246 0.00540695 +-9.60321 -0.617922 3.09933 0.00178606 +-9.57794 -0.522661 3.11624 0.00490372 +-9.55052 -0.427988 3.13312 0.0020954 +-9.5209 -0.334002 3.15012 0.00149254 +-9.48924 -0.240682 3.16712 0.00538447 +-9.45551 -0.148121 3.18427 0.00312085 +-9.41973 -0.0563445 3.20148 0.00155854 +-9.38187 0.0346355 3.21849 0.00141054 +-9.34195 0.124743 3.23541 0.00628454 +-9.29985 0.213875 3.25223 0.00376027 +-9.25561 0.301932 3.26921 0.00207038 +-9.20936 0.388958 3.28615 0.00546088 +-9.16116 0.474953 3.3029 0.00611755 +-9.11083 0.559761 3.31945 0.00548134 +-9.05847 0.643318 3.33608 0.00600543 +-9.00422 0.725669 3.35267 0.00233206 +-8.94817 0.806832 3.36911 0.00304746 +-8.89026 0.886668 3.38559 0.000929448 +-8.83035 0.96505 3.40192 0.00375323 +-8.76866 1.04206 3.41817 0.00100678 +-8.70529 1.11771 3.43432 0.00250021 +-8.64014 1.19182 3.45052 0.00498279 +-8.57323 1.26439 3.46653 0.0045065 +-8.50462 1.33533 3.48265 0.00271847 +-8.43438 1.40462 3.4989 0.00125119 +-8.36242 1.47215 3.51502 0.00592709 +-8.28883 1.53789 3.53123 0.00554028 +-8.21372 1.60193 3.54727 0.00357857 +-8.13701 1.66402 3.5634 0.00670072 +-8.05872 1.72404 3.57977 0.0056806 +-7.97891 1.78203 3.59609 0.00242805 +-7.89768 1.83806 3.61225 0.00477683 +-7.81498 1.89188 3.62853 0.00434346 +-7.73107 1.94389 3.64443 0.00370326 +-7.64573 1.99355 3.66029 0.00179178 +-7.55918 2.0411 3.67601 0.00395621 +-7.47151 2.0865 3.69186 0.00552891 +-7.38269 2.12963 3.70769 0.00401387 +-7.29272 2.17023 3.72372 0.00357124 +-7.20175 2.20856 3.73968 0.0023659 +-7.10988 2.24472 3.75555 0.000686133 +-7.01708 2.27832 3.77159 0.00335168 +-6.92342 2.30947 3.78762 0.00215076 +-6.82897 2.33807 3.8038 0.0038759 +-6.73384 2.36436 3.81987 0.00601634 +-6.63806 2.38822 3.83587 0.00322579 +-6.54173 2.40968 3.85197 0.00534104 +-6.44488 2.42855 3.86824 0.00193304 +-6.34768 2.44507 3.88488 0.000624437 +-6.25007 2.4593 3.90132 0.00186719 +-6.15216 2.47101 3.9179 0.00585969 +-6.05396 2.48007 3.9345 0.00482671 +-5.95555 2.48643 3.95102 0.00396111 +-5.85699 2.49014 3.96753 0.00325424 +-5.75836 2.49127 3.98395 0.00156398 +-5.65969 2.48966 4.00013 0.00376579 +-5.56111 2.48531 4.0163 0.00445 +-5.46269 2.47853 4.03264 0.00726225 +-5.36453 2.46889 4.0491 0.00526993 +-5.26666 2.45677 4.06564 0.00606185 +-5.16916 2.44191 4.08217 0.00703524 +-5.07198 2.42499 4.0986 0.00514467 +-4.97514 2.40634 4.11514 0.00376785 +-4.8789 2.38496 4.13185 0.00565096 +-4.78333 2.36073 4.14855 0.0037355 +-4.68843 2.3339 4.1651 0.00553483 +-4.59438 2.30437 4.18186 0.00654986 +-4.50109 2.27238 4.19839 0.00664659 +-4.40862 2.23804 4.21478 0.00382731 +-4.31694 2.20158 4.23109 0.00598498 +-4.22605 2.16309 4.2471 0.0048853 +-4.13638 2.12183 4.26309 0.00251647 +-4.04781 2.07827 4.27914 0.00667167 +-3.96054 2.03206 4.29488 0.00526986 +-3.87461 1.98342 4.31068 0.00493183 +-3.79018 1.93218 4.32638 0.0049293 +-3.70734 1.87843 4.34207 0.00384186 +-3.62593 1.82247 4.35762 0.00262308 +-3.54625 1.76407 4.37309 0.00171779 +-3.46825 1.70342 4.3885 0.00421162 +-3.39201 1.64052 4.40366 0.00472597 +-3.31764 1.57537 4.41864 0.00473131 +-3.24538 1.50787 4.43347 0.00393331 +-3.17543 1.43802 4.44858 0.00539462 +-3.10784 1.3659 4.4637 0.00439951 +-3.04248 1.29179 4.47902 0.00379672 +-2.97943 1.21573 4.4945 0.00658802 +-2.91898 1.13763 4.51017 0.00606156 +-2.86095 1.05768 4.52567 0.00384396 +-2.80568 0.975826 4.54127 0.00531629 +-2.75292 0.892352 4.55699 0.00849088 +-2.70273 0.807312 4.57274 0.00465122 +-2.6553 0.720703 4.58852 0.00597102 +-2.61083 0.63255 4.60435 0.00444418 +-2.56834 0.543396 4.62001 0.00679056 +-2.52841 0.453078 4.63572 0.00762534 +-2.49192 0.361235 4.65097 0.00403043 +-2.45884 0.268045 4.66581 0.00671071 +-2.42906 0.173723 4.68048 0.00419242 +-2.40246 0.0784021 4.6948 0.00486919 +-2.37922 -0.0177892 4.70915 0.00907706 +-2.35945 -0.114742 4.72358 0.00737428 +-2.34287 -0.212286 4.73804 0.00748826 +-2.32996 -0.310375 4.75256 0.00449589 +-2.32048 -0.408811 4.76736 0.00571873 +-2.31453 -0.50755 4.78199 0.00665646 +-2.31252 -0.606374 4.7971 0.00376963 +-2.31409 -0.705117 4.81279 0.00617805 +-2.31923 -0.803835 4.82785 0.00547875 +-2.32858 -0.902283 4.84266 0.00787066 +-2.34165 -1.00028 4.85765 0.00620081 +-2.35864 -1.09758 4.87318 0.00880822 +-2.37961 -1.19409 4.88882 0.00935348 +-2.40467 -1.28948 4.9053 0.00793634 +-2.43373 -1.38378 4.92148 0.00829069 +-2.46697 -1.47665 4.93789 0.0098699 +-2.50301 -1.56848 4.95432 0.0101575 +-2.54236 -1.65894 4.97067 0.00987816 +-2.58544 -1.74788 4.98578 0.00680355 +-2.63341 -1.83437 5.00048 0.0109609 +-2.68466 -1.91879 5.01616 0.0104423 +-2.74016 -2.0004 5.03219 0.0145097 +-2.80018 -2.07879 5.04797 0.0122098 +-2.86443 -2.15356 5.06462 0.0123009 +-2.93286 -2.22468 5.08063 0.0087081 +-3.0053 -2.29149 5.09751 0.0125225 +-3.08243 -2.35284 5.1143 0.0133418 +-3.16441 -2.40731 5.13179 0.0142218 +-3.25044 -2.45546 5.14835 0.0142074 +-3.34096 -2.49508 5.16332 0.0134456 +-3.43483 -2.52575 5.17852 0.0119897 +-3.52011 -2.5456 5.19268 0.0120695 +-3.58404 -2.5518 5.20013 0.00473408 +-3.63536 -2.54465 5.19907 0.00380166 +-3.66951 -2.53396 5.20118 0.00210337 From b3efecb50503bd7543bfc390b0e3f9b3d6a0a904 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 24 Jul 2025 12:06:25 +0200 Subject: [PATCH 39/75] Remove old UKF tests in prep for real first test --- .../kalman_filter/TrackFitterUKFTest.cxx | 380 +----------------- 1 file changed, 8 insertions(+), 372 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index 183e96c0c..ca0671954 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -218,45 +218,17 @@ class TrackFitterUKFPhysicsTest : public testing::Test { static constexpr float FLOAT_EPSILON{0.001F}; - static constexpr size_t DIM_X{6}; - static constexpr size_t DIM_V{2}; - static constexpr size_t DIM_Z{3}; - static constexpr size_t DIM_N{3}; + static constexpr size_t DIM_X{6}; // Set state vector to be (x,y,z,p,theta,phi) + static constexpr size_t DIM_V{1}; // Noise vector is energy straggling + static constexpr size_t DIM_Z{3}; // Measurement vector is (x,y,z) + static constexpr size_t DIM_N{3}; // Measurement noise vector - static XYZVector fBField; // B-field in tesla - static XYZVector fEField; // E-field in V/m + static XYZVector fBField{0, 0, 2.85}; // B-field in tesla + static XYZVector fEField{0, 0, 0}; // E-field in V/m static constexpr double fC = 299792458; // m/s kf::TrackFitterUKF m_ukf; - /** - * @param pos Position of particle in mm - * @param mom Momentum of particle in MeV/c - * @param charge charge of the particle in Coulombs - * @param mass mass of particle in MeV/c^2 - * @param dedx Stopping power in MeV/mm - * - * @returns Force in N - */ - static XYZVector Force(XYZVector pos, XYZVector mom, double charge, double mass, double dedx) - { - - // auto fourMom = AtTools::Kinematics::Get4Vector(mom, mass); - // auto v = mom / fourMom.E() * c; // m/s - auto v = AtTools::Kinematics::GetVel(mom, mass); - - auto F_lorentz = charge * (fEField + v.Cross(fBField)); - // std::cout << "F_lorentz: " << F_lorentz << std::endl; - auto dedx_si = dedx * 1.60218e-10; // de_dx in SI units (J/m) - - auto drag = -dedx_si * mom.Unit(); - // std::cout << "drag: " << drag << " mom " << mom << " dedx " << dedx_si << std::endl; - - return F_lorentz + drag; - } - - static double dist(const XYZVector &x, const XYZVector &z) { return std::sqrt((x - z).Mag2()); } - /// @brief to propagate the state vector using the process model /// @param x state vector /// @param v process noise vector @@ -264,341 +236,5 @@ class TrackFitterUKFPhysicsTest : public testing::Test { /// @return propagated (unaugmented) state vector static kf::Vector funcF(const kf::Vector &x, const kf::Vector &v, const kf::Vector &z) { - std::cout << "Staring to run funcF" << std::endl; - // TODO: This needs to be filled with an RK4 solver for the physics model - kf::Vector y{x}; - XYZVector measurement(z[0], z[1], z[2]); // Measurement point in mm - - double charge = x[6]; - double eLoss = v[0]; - double mass = 938.272; // Mass in MeV/c^2 - - double mat_density = 0; // Density of the material in g/cm^3 - AtTools::AtELossTable dedxModel(mat_density); - dedxModel.LoadSrimTable( - "/home/adam/fair_install/ATTPCROOTv2/AtReconstruction/AtFitter/OpenKF/kalman_filter/HinH.txt"); // Load the - // SRIM table - // for energy - // loss - double scalingFactor = 1.0; - int iterations = 0; - double calc_eLoss = 0; - - while (std::abs(calc_eLoss - eLoss) > 1e-4) { - std::cout << "Running iteration " << iterations << " with scaling factor: " << scalingFactor - << " and energy loss: " << calc_eLoss << std::endl; - - if (iterations > 100) { - // If we are not converging, we should probably throw an error. - throw std::runtime_error("Energy loss did not converge after 100 iterations."); - } - - // Variables needed in a single run of the RK4 solver. This section needs to be repeated - // until the energy loss converges to the correct value. - double h = 1e-10; // Timestep in s (100 ns to start) - double lastApproach = std::numeric_limits::max(); - bool approaching = true; - iterations++; - - XYZVector pos(x[0], x[1], x[2]); - XYZVector mom(x[3], x[4], x[5]); - - double KE_initial = - AtTools::Kinematics::KE(mom, mass); // std::sqrt(mom.Mag2() + mass * mass) - mass; // Kinetic energy in MeV - - while (true) { - XYZVector lastPos = pos; - XYZVector lastMom = mom; - std::cout << "Position: " << pos.X() << ", " << pos.Y() << ", " << pos.Z() << std::endl; - std::cout << "Momentum: " << mom.X() << ", " << mom.Y() << ", " << mom.Z() << std::endl; - - // Using timestep, propagate state forward one step. - double KE = AtTools::Kinematics::KE(mom, mass); // Kinetic energy in MeV - auto dedx = scalingFactor * dedxModel.GetdEdx(KE); // Get the stopping power in MeV/mm - // std::cout << "KE: " << KE << " dedx: " << dedx << std::endl; - - auto spline = dedxModel.GetSpline(); - // std::cout << "Spline: " << spline.get_x_min() << " to " << spline.get_x_max() << std::endl; - // std::cout << "dxde " << spline(KE) << " dxde " << dedx << std::endl; - - auto x_k1 = AtTools::Kinematics::GetVel(mom, mass); - auto p_k1 = Force(pos, mom, charge, mass, dedx); - // std::cout << "vel: " << x_k1 << " speed " << x_k1.R() << std::endl; - // std::cout << "Force: " << p_k1 << std::endl; - - auto x_k2 = AtTools::Kinematics::GetVel(mom + p_k1 * h / 2, mass); - auto p_k2 = Force(pos + x_k1 * h / 2, mom + p_k1 * h / 2, charge, mass, dedx); - // std::cout << "vel: " << x_k2 << " speed " << x_k2.R() << std::endl; - // std::cout << "Force: " << p_k2 << std::endl; - - auto x_k3 = AtTools::Kinematics::GetVel(mom + p_k2 * h / 2, mass); - auto p_k3 = Force(pos + x_k2 * h / 2, mom + p_k2 * h / 2, charge, mass, dedx); - // std::cout << "vel: " << x_k3 << " speed " << x_k3.R() << std::endl; - // std::cout << "Force: " << p_k3 << std::endl; - - auto x_k4 = AtTools::Kinematics::GetVel(mom + p_k3 * h, mass); - auto p_k4 = Force(pos + x_k3 * h, mom + p_k3 * h, charge, mass, dedx); - // std::cout << "vel: " << x_k4 << " speed " << x_k4.R() << std::endl; - // std::cout << "Force: " << p_k4 << std::endl; - - auto mom_SItoMeV = 1.60218e-13 / 299792458; // Factor to convert momentum to MeV/c from kg*m/s - auto F_SI = (p_k1 + 2 * p_k2 + 2 * p_k3 + p_k4) / 6; - - // Convert momentum to SI, update, and convert back to MeV/c - auto mom_SI = mom * mom_SItoMeV; - mom_SI += F_SI * h; - mom = mom_SI / mom_SItoMeV; // Convert back to MeV/c - - // Convert position to SI, update, and convert back to mm - auto pos_SI = pos / 1e3; // Convert mm to m - pos_SI += (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) * h / 6; - pos = pos_SI * 1e3; // Convert back to mm - - // std::cout << "Average force: " << F_SI << " N" << std::endl; - // std::cout << "Momentum: " << mom * mom_SItoMeV << " kg m/s" << std::endl; - // std::cout << "Delta x: " << (x_k1 + 2 * x_k2 + 2 * x_k3 + x_k4) / 6 << " m/s" << std::endl; - - auto approach = dist(pos, measurement); - // std::cout << "pos: " << pos << " measurement" << measurement << std::endl; - // std::cout << "Approach: " << approach << " last approach: " << lastApproach << std::endl; - if (approach < lastApproach) { - // We are still approaching the measurement point - approaching = true; - lastApproach = approach; - - continue; - } - - bool reachedMeasurementPoint = (approaching && approach > lastApproach); - bool particleStopped = std::sqrt(mom.Mag2() + mass * mass) - mass < 0.01; - if (reachedMeasurementPoint || particleStopped) { - // Last iteration we were still approaching the measurement point. Now we are further away - // then before. We have probably reached the measurement point if things are well behaved. - // I can think of cases where this will not be true. A better solution might be to run - // tracking the point of closest approach until the distance between the current state and - // the measurement point is larger than the distance between the last state and the measurement point. - - // Undo the last step since we were closer last time. - y[0] = lastPos.X(); - y[1] = lastPos.Y(); - y[2] = lastPos.Z(); - y[3] = lastMom.X(); - y[4] = lastMom.Y(); - y[5] = lastMom.Z(); - - // Update the scaling factor - double KE_final = std::sqrt(lastMom.Mag2() + mass * mass) - mass; - calc_eLoss = KE_initial - KE_final; // Energy loss in MeV - scalingFactor *= eLoss / calc_eLoss; - std::cout << "------- End of RK4 interation " << iterations << " ---------" << std::endl; - std::cout << "Particle stopped: " << particleStopped << std::endl; - std::cout << "Reached measurement point: " << reachedMeasurementPoint << std::endl; - std::cout << "Last approach: " << lastApproach << " Current approach: " << approach << std::endl; - std::cout << "Desired energy loss: " << eLoss << " MeV" << std::endl; - std::cout << "Calculated energy loss: " << calc_eLoss << " MeV" << std::endl; - std::cout << "Difference: " << calc_eLoss - eLoss << " MeV" << std::endl; - std::cout << "New scaling factor: " << scalingFactor << std::endl; - std::cout << "Final Position: " << pos.X() << ", " << pos.Y() << ", " << pos.Z() << std::endl; - std::cout << "Final Momentum: " << mom.X() << ", " << mom.Y() << ", " << mom.Z() << std::endl; - break; - // return y; - } - } // End of loop over RK4 integration - } // End loop over energy loss convergence - - return y; - } - - /// @brief to apply the measurement model to the state vector - /// @param x the state vector of the system - /// @return the measurement vector - static kf::Vector funcH(const kf::Vector &x) - { - kf::Vector y; - y[0] = x[0]; - y[1] = x[1]; - y[2] = x[2]; - - return y; - } - - const double mass_p = 938.272; // Mass of proton in MeV/c^2 - const double charge_p = 1.602176634e-19; // Charge of proton -}; - -// Definition of static member variables -TrackFitterUKFPhysicsTest::XYZVector TrackFitterUKFPhysicsTest::fBField; -TrackFitterUKFPhysicsTest::XYZVector TrackFitterUKFPhysicsTest::fEField; - -TEST_F(TrackFitterUKFPhysicsTest, PhysicsPrediction) -{ - // TODO: This needs to be filled with a proper physics test. - kf::Vector x; // Initial state vector - - kf::Matrix P; // Initial state vector covariance matrix - - // Note: process noise is defined in the UKF class, so we don't need to set it here. - - kf::Vector z; // Measurement vector to be used in the correction step - z << std::sqrt(5), std::atan2(1.f, 2.f); - - kf::Matrix R; // Covariance matrix for the measurement noise - - m_ukf.vecX() = x; - m_ukf.matP() = P; - - // m_ukf.setCovarianceR(R); - // m_ukf.predictUKF(funcF, z); - ASSERT_EQ(true, true); -} -/* - -TEST_F(TrackFitterUKFPhysicsTest, TestForceNoFields) -{ - XYZVector pos(0, 0, 0); // Position in mm - XYZVector mom(100, 0, 0); // Momentum in MeV/c - fBField = XYZVector(0, 0, 0); // B-field in tesla - fEField = XYZVector(0, 0, 0); // E-field - - double charge = charge_p; // Charge in Coulombs - double mass = mass_p; // Mass in MeV/c^2 - double dedx = 1; // Stopping power in MeV/mm - - auto force = Force(pos, mom, charge, mass, dedx); - - ASSERT_NEAR(force.X(), -1.602e-10, FLOAT_EPSILON); - ASSERT_NEAR(force.Y(), 0, FLOAT_EPSILON); - ASSERT_NEAR(force.Z(), 0, FLOAT_EPSILON); - - mom = XYZVector(100, 0, 100); // Reset momentum - force = Force(pos, mom, charge, mass, dedx); - ASSERT_NEAR(force.X(), -1.602e-10 / std::sqrt(2), FLOAT_EPSILON); - ASSERT_NEAR(force.Y(), 0, FLOAT_EPSILON); - ASSERT_NEAR(force.Z(), -1.602e-10 / std::sqrt(2), FLOAT_EPSILON); -} - -TEST_F(TrackFitterUKFPhysicsTest, TestForceEField) -{ - XYZVector pos(0, 0, 0); // Position in mm - XYZVector mom(100, 0, 0); // Momentum in MeV/c - fBField = XYZVector(0, 0, 1); // B-field in tesla - fEField = XYZVector(0, 0, 0); // E-field in V/m - - double charge = charge_p; // Charge in Coulombs - double mass = mass_p; // Mass in MeV/c^2 - double dedx = 0; // Stopping power in MeV/mm - - auto force = Force(pos, mom, charge, mass, dedx); - - ASSERT_NEAR(force.X(), 0, FLOAT_EPSILON); - ASSERT_NEAR(force.Y(), 0, FLOAT_EPSILON); - ASSERT_NEAR(force.Z(), 1.121e-14, FLOAT_EPSILON); -} - -TEST_F(TrackFitterUKFPhysicsTest, TestForceBField) -{ - XYZVector pos(0, 0, 0); // Position in mm - XYZVector mom(100, 0, 0); // Momentum in MeV/c - fBField = XYZVector(0, 0, 1); // B-field in tesla - fEField = XYZVector(0, 0, 0); // E-field - - double charge = charge_p; // Charge in Coulombs - double mass = mass_p; // Mass in MeV/c^2 - double dedx = 0; // Stopping power in MeV/mm - - auto force = Force(pos, mom, charge, mass, dedx); - - ASSERT_NEAR(force.X(), 0, FLOAT_EPSILON); - ASSERT_NEAR(force.Y(), -5.09e-12, FLOAT_EPSILON); - ASSERT_NEAR(force.Z(), 0, FLOAT_EPSILON); -} - -TEST_F(TrackFitterUKFPhysicsTest, TestPropagatorStoppingNoField) -{ - - double KE = 1; // Kinetic energy in MeV - double E = KE + mass_p; - double p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c - fBField = XYZVector(0, 0, 0); // B-field in tesla - fEField = XYZVector(0, 0, 0); // E-field - - kf::Vector x; // Initial state vector - x[0] = 0; - x[1] = 0; - x[2] = 0; - x[3] = p; // p_x - x[4] = 0; - x[5] = 0; - - ASSERT_NEAR(x[3], 43.331, 1e-1); // Make sure momentum is calculated correctly - - kf::Vector v; // Process noise vector - v[0] = 1; // Energy loss in MeV - v[1] = 0.0; // No process noise in this example - - kf::Vector z; // Measurement vector - z[0] = 1e3; - z[1] = 0; - z[2] = 0; - - auto final = funcF(x, v, z); // Propagate the state vector using the process model - - // Check the final position is close to the stopping point from LISE - ASSERT_NEAR(final[3], 0, 0.1); // Final momentum in x-direction should be close to 0 - ASSERT_NEAR(final[0], 210, 10); // Final position in x-direction should be close to 210 mm - - KE = 0.5; - v[0] = 0.5; // Energy loss in MeV - E = KE + mass_p; - p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c - x[3] = p; // Reset momentum - final = funcF(x, v, z); // Propagate the state vector using the - - ASSERT_NEAR(final[3], 0, 0.1); // Final momentum in x-direction should be close to 0 - ASSERT_NEAR(final[0], 68.6, 5); // Final position in x -} -*/ - -TEST_F(TrackFitterUKFPhysicsTest, TestPropagatorNoField) -{ - double KE = 1; // Kinetic energy in MeV - double E = KE + mass_p; - double p = std::sqrt(E * E - mass_p * mass_p); // Momentum in MeV/c - fBField = XYZVector(0, 0, 0); // B-field in tesla - fEField = XYZVector(0, 0, 0); // E-field - - kf::Vector x; // Initial state vector - x[0] = 0; - x[1] = 0; - x[2] = 0; - x[3] = p; // p_x - x[4] = 0; - x[5] = 0; - - ASSERT_NEAR(x[3], 43.331, 1e-1); // Make sure momentum is calculated correctly - - kf::Vector v; // Process noise vector - v[0] = 0.0285; // Energy loss in MeV - v[1] = 0.0; // No process noise in this example - - kf::Vector z; // Measurement vector - z[0] = 10; // Measure after 10 mm - z[1] = 0; - z[2] = 0; - - auto final = funcF(x, v, z); // Propagate the state vector using the process model - - // Check the final position is close to the stopping point from LISE - double E_fin = KE - v[0] + mass_p; - double p_fin = std::sqrt(E_fin * E_fin - mass_p * mass_p); - ASSERT_NEAR(final[3], p_fin, 0.1); // Final momentum in x-direction - ASSERT_NEAR(final[0], 10, .5); // Final position in x-direction should be close to 10 mm - - v[0] = 0.3237; // Energy loss in MeV - z[0] = 100; // Measure after 100 mm - final = funcF(x, v, z); // Propagate the state vector using the process model - E_fin = KE - v[0] + mass_p; - p_fin = std::sqrt(E_fin * E_fin - mass_p * mass_p); - ASSERT_NEAR(final[3], p_fin, 0.1); // Final momentum - ASSERT_NEAR(final[0], 100, .5); // Final position -} + ASSERT_EQ(true, true); + } \ No newline at end of file From a711861b80c14bcb41388976941a0d0a03e8607e Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 24 Jul 2025 14:01:37 +0200 Subject: [PATCH 40/75] Reset KF changes --- AtTools/AtPropagator.h | 15 +++++++++++---- AtTools/AtPropagatorTest.cxx | 5 +++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 2e199b497..153d4fa88 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -51,9 +51,9 @@ class AtPropagator { using Plane3D = ROOT::Math::Plane3D; // Variables used for the force - XYZVector fEField{0, 0, 0}; // Electric field vector - XYZVector fBField{0, 0, 0}; // Magnetic field vector - const std::unique_ptr fELossModel; // Energy loss model + XYZVector fEField{0, 0, 0}; // Electric field vector + XYZVector fBField{0, 0, 0}; // Magnetic field vector + std::unique_ptr fELossModel; // Energy loss model // Internal state variables for the propagator StepState fState; /// Current state of the particle @@ -80,7 +80,7 @@ class AtPropagator { fState.fMass = mass; fState.fQ = charge; } - + AtPropagator(AtPropagator &&) = default; /** * @brief Set the electric field (V/m) */ @@ -105,6 +105,7 @@ class AtPropagator { fState.fMom = mom; } const StepState &GetState() const { return fState; } + const AtELossModel *GetELossModel() const { return fELossModel.get(); } XYZPoint GetPosition() const { return fState.fPos; } XYZVector GetMomentum() const { return fState.fMom; } @@ -246,6 +247,12 @@ class AtMeasurementPoint : public AtMeasurementSurface { public: AtMeasurementPoint(const ROOT::Math::XYZPoint &point) : fPoint(point) {} + template + AtMeasurementPoint(const T &point) + { + fPoint = ROOT::Math::XYZPoint(point[0], point[1], point[2]); + } + double Distance(const ROOT::Math::XYZPoint &pos) const override { return (fPoint - pos).R(); } bool PassedSurface(AtPropagator::StepState &result) const override; ROOT::Math::XYZPoint ProjectToSurface(const ROOT::Math::XYZPoint &pos) const override { return pos; } diff --git a/AtTools/AtPropagatorTest.cxx b/AtTools/AtPropagatorTest.cxx index 8f1e67a5c..dbcd93c95 100644 --- a/AtTools/AtPropagatorTest.cxx +++ b/AtTools/AtPropagatorTest.cxx @@ -16,6 +16,7 @@ using namespace AtTools; const double mass_p = 938.272; // Mass of proton in MeV/c^2 const double charge_p = 1.602176634e-19; // Charge of proton +namespace { std::string getEnergyPath() { @@ -25,6 +26,7 @@ std::string getEnergyPath() } return std::string(env) + "/resources/energy_loss/HinH.txt"; // Use environment variable } +} // namespace class DummyELossModel : public AtELossModel { public: double eLoss = 1; @@ -34,6 +36,9 @@ class DummyELossModel : public AtELossModel { double GetRange(double /*energyIni*/, double /*energyFin = 0*/) const override { return 1.0; } double GetEnergyLoss(double /*energyIni*/, double /*distance*/) const override { return 1.0; } double GetEnergy(double /*energyIni*/, double /*distance*/) const override { return 1.0; } + double GetElossStraggling(double /*energyIni*/, double /*energyFin*/) const override { return 0.0; } + double GetdEdxStraggling(double /*energyIni*/, double /*energyFin*/) const override { return 0.0; } + double GetRangeVariance(double /*energy*/) const override { return 0.0; } }; TEST(AtPropagatorTest, ForceNoField) From 6f74ffda5cd02c1bffad4753d82f899be0270f36 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 24 Jul 2025 14:03:38 +0200 Subject: [PATCH 41/75] Original tests building again. Need fresh slate --- .../AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index ca0671954..c2979bb18 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -223,8 +223,8 @@ class TrackFitterUKFPhysicsTest : public testing::Test { static constexpr size_t DIM_Z{3}; // Measurement vector is (x,y,z) static constexpr size_t DIM_N{3}; // Measurement noise vector - static XYZVector fBField{0, 0, 2.85}; // B-field in tesla - static XYZVector fEField{0, 0, 0}; // E-field in V/m + XYZVector fBField{0, 0, 2.85}; // B-field in tesla + XYZVector fEField{0, 0, 0}; // E-field in V/m static constexpr double fC = 299792458; // m/s kf::TrackFitterUKF m_ukf; @@ -236,5 +236,6 @@ class TrackFitterUKFPhysicsTest : public testing::Test { /// @return propagated (unaugmented) state vector static kf::Vector funcF(const kf::Vector &x, const kf::Vector &v, const kf::Vector &z) { - ASSERT_EQ(true, true); - } \ No newline at end of file + return {}; + } +}; \ No newline at end of file From b288edd702f53b8616bc4cad2fa152d82ac54643 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 24 Jul 2025 14:06:09 +0200 Subject: [PATCH 42/75] Base class with virtual tests pass --- .../AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h | 10 +++++----- .../OpenKF/kalman_filter/TrackFitterUKFTest.cxx | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index 3481cb98f..a6f062941 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -24,7 +24,7 @@ namespace kf { /// @tparam DIM_V Dimension of the process noise vector. /// @tparam DIM_N Dimension of the measurement noise vector. template -class TrackFitterUKF : public KalmanFilter { +class TrackFitterUKFBase : public KalmanFilter { public: // Augmented state vector is just the process noise and state vector. The measurement noise is not included as that // is independent of the propagation and measurement model and just adds linearly. @@ -38,7 +38,7 @@ class TrackFitterUKF : public KalmanFilter { Matrix m_matSigmaXa{Matrix::Zero()}; ///< @brief Sigma points matrix - TrackFitterUKF() + TrackFitterUKFBase() : KalmanFilter(), m_kappa(3 - DIM_A), m_matQ(Matrix::Zero()), m_matR(Matrix::Zero()) { @@ -46,7 +46,7 @@ class TrackFitterUKF : public KalmanFilter { updateWeights(); } - ~TrackFitterUKF() {} + ~TrackFitterUKFBase() {} void setKappa(float32_t kappa) { @@ -95,7 +95,7 @@ class TrackFitterUKF : public KalmanFilter { } } - std::array calculateProcessNoiseMean() + virtual std::array calculateProcessNoiseMean() { // Calculate the expectation value of the process noise using the current value of the state vector m_vecX std::array processNoiseMean{0}; @@ -104,7 +104,7 @@ class TrackFitterUKF : public KalmanFilter { return processNoiseMean; } - Matrix calculateProcessNoiseCovariance() + virtual Matrix calculateProcessNoiseCovariance() { // Calculate the process noise covariance matrix Matrix matQ{Matrix::Zero()}; diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index c2979bb18..d9714f0da 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -18,7 +18,7 @@ class TrackFitterUKFExampleTest : public testing::Test { static constexpr size_t DIM_Z{2}; static constexpr size_t DIM_N{2}; - kf::TrackFitterUKF m_ukf; + kf::TrackFitterUKFBase m_ukf; /// @brief to propagate the state vector using the process model /// @param x state vector @@ -227,7 +227,7 @@ class TrackFitterUKFPhysicsTest : public testing::Test { XYZVector fEField{0, 0, 0}; // E-field in V/m static constexpr double fC = 299792458; // m/s - kf::TrackFitterUKF m_ukf; + kf::TrackFitterUKFBase m_ukf; /// @brief to propagate the state vector using the process model /// @param x state vector From 73f294b1bca8893e62690a1fe5eb0a20d6032279 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 24 Jul 2025 14:19:02 +0200 Subject: [PATCH 43/75] UKF w/ prop is instantiating --- .../OpenKF/kalman_filter/TrackFitterUKF.h | 73 ++++++++++++++++++- .../kalman_filter/TrackFitterUKFTest.cxx | 34 ++++++++- 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index a6f062941..c0d2ad4cf 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -9,8 +9,10 @@ /// @file unscented_kalman_filter.h /// -#ifndef UNSCENTED_KALMAN_FILTER_LIB_H -#define UNSCENTED_KALMAN_FILTER_LIB_H +#ifndef TRACKFITTERUKF_H +#define TRACKFITTERUKF_H + +#include "AtPropagator.h" #include "kalman_filter.h" #include "kf_util.h" @@ -370,6 +372,71 @@ class TrackFitterUKFBase : public KalmanFilter { return matPxy; } }; + +// Define template dimension variables for clarity and reuse + +class TrackFitterUKF : public TrackFitterUKFBase<6, 1, 3, 3> { +protected: + static constexpr int32_t TRACKFITTER_DIM_X = 6; + static constexpr int32_t TRACKFITTER_DIM_Z = 1; + static constexpr int32_t TRACKFITTER_DIM_V = 3; + static constexpr int32_t TRACKFITTER_DIM_N = 3; + + AtTools::AtPropagator fPropagator; ///< @brief Propagator for the track fitter + AtTools::AtPropagator::StepState fMeanStep; /// Holds the step information for POCA propagation of mean state + +public: + /** + * @brief Constructor for the TrackFitterUKF class. + * @param propagator The propagator to be used for the track fitting, must be passed as an rvalue reference. + * + * Example usage: + * ``` + * AtTools::AtPropagator propagator; + * kf::TrackFitterUKF trackFitterUKF(std::move(propagator)); + * ``` + */ + TrackFitterUKF(AtTools::AtPropagator &&propagator) : TrackFitterUKFBase(), fPropagator(std::move(propagator)) {} + + template + void predictUKF(PredictionModelCallback predictionModelFunc, const Vector &vecZ) + { + // First we need to propagate the mean state vector to the next measurement point. + AtTools::AtRK4Stepper stepper; // Use RK4 stepper for propagation + fPropagator.PropagateToMeasurementSurface(AtTools::AtMeasurementPoint(vecZ), stepper); + fMeanStep = fPropagator.GetState(); // Get the mean step information from the propagator + } + +protected: + std::array calculateProcessNoiseMean() override + { + // The process noise is the scaling factor for dedx. By definition the mean should be 1 + std::array processNoiseMean{1}; + return processNoiseMean; + } + + Matrix calculateProcessNoiseCovariance() override + { + assert(TRACKFITTER_DIM_V == 1 && "Process noise covariance is only implemented for DIM_V = 1"); + // Calculate the process noise covariance matrix + Matrix matQ{Matrix::Zero()}; + + // We need to know what the energy of the particle before/after transport. + double eIn = 0; + double eOut = 0; + + if (const auto *elossModel = fPropagator.GetELossModel()) { + double dedx_straggle = elossModel->GetdEdxStraggling(eIn, eOut); + double factor = dedx_straggle / elossModel->GetdEdx(eIn); + matQ(0, 0) = factor * factor; // Variance for the dedx straggling. + + } else { + throw std::runtime_error("Cannot calculate process noise covariance without an energy loss model"); + } + // TODO: Add multiple scattering + return matQ; + } +}; } // namespace kf -#endif // UNSCENTED_KALMAN_FILTER_LIB_H +#endif // TRACKFITTERUKF_H diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index d9714f0da..b43064d0a 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -209,11 +209,22 @@ TEST_F(TrackFitterUKFExampleTest, PredictionAndCorrection) ASSERT_NEAR(m_ukf.matP()(3, 3), 0.1333886F, FLOAT_EPSILON); } -class TrackFitterUKFPhysicsTest : public testing::Test { +namespace { + +std::string getEnergyPath() +{ + auto env = std::getenv("VMCWORKDIR"); + if (env == nullptr) { + return "../../resources/energy_loss/HinH.txt"; // Default path assuming cwd is build/AtTools + } + return std::string(env) + "/resources/energy_loss/HinH.txt"; // Use environment variable +} +} // namespace + +class TrackFitterUKFFixture : public testing::Test { public: using XYZVector = ROOT::Math::XYZVector; - virtual void SetUp() override {} virtual void TearDown() override {} static constexpr float FLOAT_EPSILON{0.001F}; @@ -227,7 +238,17 @@ class TrackFitterUKFPhysicsTest : public testing::Test { XYZVector fEField{0, 0, 0}; // E-field in V/m static constexpr double fC = 299792458; // m/s - kf::TrackFitterUKFBase m_ukf; + std::unique_ptr m_ukf{nullptr}; + + void SetUp() override + { + const double mass_p = 938.272; // Mass of proton in MeV/c^2 + const double charge_p = 1.602176634e-19; // Charge of protonble + auto elossModel = std::make_unique(0); + elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path + AtTools::AtPropagator propagator(charge_p, mass_p, std::move(elossModel)); + m_ukf = std::make_unique(std::move(propagator)); + } /// @brief to propagate the state vector using the process model /// @param x state vector @@ -238,4 +259,9 @@ class TrackFitterUKFPhysicsTest : public testing::Test { { return {}; } -}; \ No newline at end of file +}; + +TEST_F(TrackFitterUKFFixture, TestInstantiation) +{ + assert(true); +} \ No newline at end of file From 8d670562939730516182a3fc7e37be413e232640 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 24 Jul 2025 19:00:06 +0200 Subject: [PATCH 44/75] Prediction code runs --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 86 +++++++++ .../OpenKF/kalman_filter/TrackFitterUKF.h | 118 ++++++------ .../kalman_filter/TrackFitterUKFTest.cxx | 172 +++++++++++++++++- AtReconstruction/CMakeLists.txt | 1 + AtTools/AtKinematics.cxx | 9 + AtTools/AtKinematics.h | 3 + AtTools/AtPropagator.h | 2 + 7 files changed, 325 insertions(+), 66 deletions(-) create mode 100644 AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx new file mode 100644 index 000000000..4534689b7 --- /dev/null +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -0,0 +1,86 @@ +#include "TrackFitterUKF.h" + +#include + +#include +#include + +#include + +namespace kf { + +void TrackFitterUKF::SetInitialState(const ROOT::Math::XYZPoint &initialPosition, + const ROOT::Math::XYZVector &initialMomentum, const TMatrixD &initialCovariance) +{ + m_vecX[0] = initialPosition.X(); // X position + m_vecX[1] = initialPosition.Y(); // Y position + m_vecX[2] = initialPosition.Z(); // Z position + m_vecX[3] = initialMomentum.R(); // Momentum magnitude + m_vecX[4] = initialMomentum.Theta(); // Polar angle + m_vecX[5] = initialMomentum.Phi(); // Azimuthal angle + + // Copy elements from initialCovariance to m_matP + for (int i = 0; i < m_matP.rows(); ++i) { + for (int j = 0; j < m_matP.cols(); ++j) { + m_matP(i, j) = initialCovariance(i, j); + } + } +} + +std::array TrackFitterUKF::calculateProcessNoiseMean() +{ + // The process noise is the scaling factor for dedx. By definition the mean should be 1 + std::array processNoiseMean{1}; + return processNoiseMean; +} + +Matrix TrackFitterUKF::calculateProcessNoiseCovariance() +{ + assert(TF_DIM_V == 1 && "Process noise covariance is only implemented for DIM_V = 1"); + // Calculate the process noise covariance matrix + Matrix matQ{Matrix::Zero()}; + + // We need to know what the energy of the particle before/after transport. + double eIn = AtTools::Kinematics::KE(fMeanStep.fLastMom, fMeanStep.fMass); + double eOut = AtTools::Kinematics::KE(fMeanStep.fMom, fMeanStep.fMass); + + if (const auto *elossModel = fPropagator.GetELossModel()) { + double dedx_straggle = elossModel->GetdEdxStraggling(eIn, eOut); + double factor = dedx_straggle / elossModel->GetdEdx(eIn); + matQ(0, 0) = factor * factor; // Variance for the dedx straggling. + + } else { + throw std::runtime_error("Cannot calculate process noise covariance without an energy loss model"); + } + // TODO: Add multiple scattering + return matQ; +} + +Vector TrackFitterUKF::funcF(const Vector &x, + const Vector &v, + const Vector &z) +{ + // Set the state of the propagator to the current state vector + using namespace ROOT::Math; + XYZPoint fPos(x[0], x[1], x[2]); // Position from state vector + Polar3DVector fMom(x[3], x[4], x[5]); // Momentum from state vector + + fPropagator.SetState(fPos, XYZVector(fMom)); + + fPropagator.fScalingFactor = v[0]; // Set the scaling factor for energy loss + fPropagator.PropagateToMeasurementSurface(AtTools::AtMeasurementPlane(fMeasurementPlane), *fStepper); + fPropagator.fScalingFactor = 1.0; // Reset the scaling factor after propagation + + auto fState = fPropagator.GetState(); // Get the propagated state + Vector vecX{Vector::Zero()}; + vecX[0] = fState.fPos.X(); // X position + vecX[1] = fState.fPos.Y(); // Y position + vecX[2] = fState.fPos.Z(); // Z position + vecX[3] = fState.fMom.R(); // Momentum magnitude + vecX[4] = fState.fMom.Theta(); // Polar angle + vecX[5] = fState.fMom.Phi(); // Azimuthal + + return vecX; // Return the propagated state vector +} + +} // namespace kf \ No newline at end of file diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index c0d2ad4cf..42459991c 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -12,12 +12,18 @@ #ifndef TRACKFITTERUKF_H #define TRACKFITTERUKF_H +#include "AtKinematics.h" #include "AtPropagator.h" +#include + +#include +#include +#include + #include "kalman_filter.h" #include "kf_util.h" #include "unscented_kalman_filter.h" - namespace kf { /// @brief Class for fitting tracks using the Unscented Kalman Filter (UKF) algorithm. @@ -27,17 +33,31 @@ namespace kf { /// @tparam DIM_N Dimension of the measurement noise vector. template class TrackFitterUKFBase : public KalmanFilter { + public: // Augmented state vector is just the process noise and state vector. The measurement noise is not included as that // is independent of the propagation and measurement model and just adds linearly. static constexpr int32_t DIM_A{DIM_X + DIM_V}; ///< @brief Augmented state dimension static constexpr int32_t SIGMA_DIM_A{2 * DIM_A + 1}; ///< @brief Sigma points dimension for augmented state - float32_t m_kappa{0}; ///< @brief Kappa parameter for finding sigma points + +protected: + using KalmanFilter::m_vecX; // from Base KalmanFilter class + using KalmanFilter::m_matP; // from Base KalmanFilter class + + float32_t m_weight0; /// @brief unscented transform weight 0 for mean + float32_t m_weighti; /// @brief unscented transform weight i for none mean samples + float32_t m_kappa{0}; ///< @brief Kappa parameter for finding sigma points + + Vector m_vecXa{Vector::Zero()}; /// @brief augmented state vector (incl. process + /// and measurement noise means) + Matrix m_matPa{Matrix::Zero()}; /// @brief augmented state covariance (incl. + /// process and measurement noise covariances) // Add variables to track the covariances of the process and measurement noise. Matrix m_matQ; // @brief Process noise covariance matrix Matrix m_matR; // @brief Measurement noise covariance matrix +public: Matrix m_matSigmaXa{Matrix::Zero()}; ///< @brief Sigma points matrix TrackFitterUKFBase() @@ -56,13 +76,6 @@ class TrackFitterUKFBase : public KalmanFilter { updateWeights(); // Update the weights based on the new kappa value } - // This code uses two different conventions for managing noise. - // The state vector noise is set in the updateAugmentedStateAndCovariance() method, while - // the noise vectors for the process and measurement models are set in the setCovarianceQ() and - // setCovarianceR() methods. This is an odd choice. We will be moving everything into a common - // structure where updateAugmentedStateAndCovariance() handle all covariance updates that are actually - // part of the augmented state vector. - /// /// @brief adding process noise covariance Q to the augmented state covariance /// matPa in the middle element of the diagonal. @@ -232,18 +245,7 @@ class TrackFitterUKFBase : public KalmanFilter { m_matP -= matK * matPzz * matK.transpose(); } -private: - using KalmanFilter::m_vecX; // from Base KalmanFilter class - using KalmanFilter::m_matP; // from Base KalmanFilter class - - float32_t m_weight0; /// @brief unscented transform weight 0 for mean - float32_t m_weighti; /// @brief unscented transform weight i for none mean samples - - Vector m_vecXa{Vector::Zero()}; /// @brief augmented state vector (incl. process - /// and measurement noise means) - Matrix m_matPa{Matrix::Zero()}; /// @brief augmented state covariance (incl. - /// process and measurement noise covariances) - +protected: /// /// @brief algorithm to calculate the weights used to draw the sigma points /// @@ -276,6 +278,10 @@ class TrackFitterUKFBase : public KalmanFilter { // cholesky factorization to get matrix Pxx square-root Eigen::LLT> lltOfPa(matPa); if (lltOfPa.info() != Eigen::Success) { + LOG(error) << "Cholesky decomposition failed, matrix is not positive definite."; + for (int32_t i{0}; i < STATE_DIM; ++i) { + LOG(error) << "Pxx[" << i << "]: " << matPa(i, i); + } throw std::runtime_error("Cholesky decomposition failed, matrix is not positive definite."); } Matrix matSa{lltOfPa.matrixL()}; // sqrt(P_{a}) @@ -375,16 +381,17 @@ class TrackFitterUKFBase : public KalmanFilter { // Define template dimension variables for clarity and reuse -class TrackFitterUKF : public TrackFitterUKFBase<6, 1, 3, 3> { +class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { protected: - static constexpr int32_t TRACKFITTER_DIM_X = 6; - static constexpr int32_t TRACKFITTER_DIM_Z = 1; - static constexpr int32_t TRACKFITTER_DIM_V = 3; - static constexpr int32_t TRACKFITTER_DIM_N = 3; + static constexpr int32_t TF_DIM_X = 6; + static constexpr int32_t TF_DIM_Z = 3; + static constexpr int32_t TF_DIM_V = 1; + static constexpr int32_t TF_DIM_N = 3; - AtTools::AtPropagator fPropagator; ///< @brief Propagator for the track fitter + AtTools::AtPropagator fPropagator; ///< @brief Propagator for the track fitter + std::unique_ptr fStepper{nullptr}; AtTools::AtPropagator::StepState fMeanStep; /// Holds the step information for POCA propagation of mean state - + ROOT::Math::Plane3D fMeasurementPlane; ///< Holds the measurement plane for the track fitter public: /** * @brief Constructor for the TrackFitterUKF class. @@ -396,46 +403,37 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 1, 3, 3> { * kf::TrackFitterUKF trackFitterUKF(std::move(propagator)); * ``` */ - TrackFitterUKF(AtTools::AtPropagator &&propagator) : TrackFitterUKFBase(), fPropagator(std::move(propagator)) {} - - template - void predictUKF(PredictionModelCallback predictionModelFunc, const Vector &vecZ) + TrackFitterUKF(AtTools::AtPropagator &&propagator, std::unique_ptr &&stepper) + : TrackFitterUKFBase(), fPropagator(std::move(propagator)), fStepper(std::move(stepper)) { - // First we need to propagate the mean state vector to the next measurement point. - AtTools::AtRK4Stepper stepper; // Use RK4 stepper for propagation - fPropagator.PropagateToMeasurementSurface(AtTools::AtMeasurementPoint(vecZ), stepper); - fMeanStep = fPropagator.GetState(); // Get the mean step information from the propagator } -protected: - std::array calculateProcessNoiseMean() override - { - // The process noise is the scaling factor for dedx. By definition the mean should be 1 - std::array processNoiseMean{1}; - return processNoiseMean; - } + void SetInitialState(const ROOT::Math::XYZPoint &initialPosition, const ROOT::Math::XYZVector &initialMomentum, + const TMatrixD &initialCovariance); - Matrix calculateProcessNoiseCovariance() override - { - assert(TRACKFITTER_DIM_V == 1 && "Process noise covariance is only implemented for DIM_V = 1"); - // Calculate the process noise covariance matrix - Matrix matQ{Matrix::Zero()}; + kf::Vector + funcF(const kf::Vector &x, const kf::Vector &v, const kf::Vector &z); - // We need to know what the energy of the particle before/after transport. - double eIn = 0; - double eOut = 0; + void predictUKF(const Vector &z) + { + // First we need to propagate the mean state vector to the next measurement point. + fPropagator.PropagateToMeasurementSurface(AtTools::AtMeasurementPoint(z), *fStepper); + fMeanStep = fPropagator.GetState(); // Get the mean step information from the propagator - if (const auto *elossModel = fPropagator.GetELossModel()) { - double dedx_straggle = elossModel->GetdEdxStraggling(eIn, eOut); - double factor = dedx_straggle / elossModel->GetdEdx(eIn); - matQ(0, 0) = factor * factor; // Variance for the dedx straggling. + // Now we can construct the reference plane. + using namespace ROOT::Math; + fMeasurementPlane = Plane3D(fMeanStep.fMom.Unit(), + XYZPoint(z)); // Create a plane using the momentum direction and position - } else { - throw std::runtime_error("Cannot calculate process noise covariance without an energy loss model"); - } - // TODO: Add multiple scattering - return matQ; + auto callback = [this](const kf::Vector &x_, const kf::Vector &v_, + const kf::Vector &z_) { return funcF(x_, v_, z_); }; + TrackFitterUKFBase::predictUKF(callback, z); } + +protected: + std::array calculateProcessNoiseMean() override; + + Matrix calculateProcessNoiseCovariance() override; }; } // namespace kf diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index b43064d0a..4ca0f983c 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -2,6 +2,11 @@ #include "AtELossTable.h" #include "AtKinematics.h" +#include "AtPropagator.h" + +#include + +#include "TMatrixD.h" #include "Math/Vector3D.h" #include "gtest/gtest.h" @@ -221,6 +226,28 @@ std::string getEnergyPath() } } // namespace +namespace kf { +class TrackFitterUKFTest : public TrackFitterUKF { +public: + Vector vecXa() { return m_vecXa; } + Matrix matPa() { return m_matPa; } + Matrix matSigmaXa() { return m_matSigmaXa; } + AtTools::AtPropagator &getPropagator() { return fPropagator; } + AtTools::AtPropagator::StepState &getState() { return fMeanStep; } + + void calcSigmaPoints() { m_matSigmaXa = calculateSigmaPoints(m_vecXa, m_matPa); } + void calcMeanAndCovFromSigma(const Matrix &sigmaXa, Vector &meanXa, + Matrix &covXa) + { + calculateWeightedMeanAndCovariance(sigmaXa, meanXa, covXa); + } + + TrackFitterUKFTest(AtTools::AtPropagator &&propagator, std::unique_ptr &&stepper) + : TrackFitterUKF(std::move(propagator), std::move(stepper)) + { + } +}; +} // namespace kf class TrackFitterUKFFixture : public testing::Test { public: using XYZVector = ROOT::Math::XYZVector; @@ -234,20 +261,23 @@ class TrackFitterUKFFixture : public testing::Test { static constexpr size_t DIM_Z{3}; // Measurement vector is (x,y,z) static constexpr size_t DIM_N{3}; // Measurement noise vector - XYZVector fBField{0, 0, 2.85}; // B-field in tesla - XYZVector fEField{0, 0, 0}; // E-field in V/m - static constexpr double fC = 299792458; // m/s + XYZVector fBField{0, 0, 2.85}; // B-field in tesla + XYZVector fEField{0, 0, 0}; // E-field in V/m + static constexpr double fC = 299792458; // m/s + const double mass_p = 938.272; // Mass of proton in MeV/c^2 + const double charge_p = 1.602176634e-19; // Charge of protonble std::unique_ptr m_ukf{nullptr}; void SetUp() override { - const double mass_p = 938.272; // Mass of proton in MeV/c^2 - const double charge_p = 1.602176634e-19; // Charge of protonble auto elossModel = std::make_unique(0); elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path AtTools::AtPropagator propagator(charge_p, mass_p, std::move(elossModel)); - m_ukf = std::make_unique(std::move(propagator)); + propagator.SetBField(fBField); + propagator.SetEField(fEField); + auto stepper = std::make_unique(); // Use RK4 stepper for propagation + m_ukf = std::make_unique(std::move(propagator), std::move(stepper)); } /// @brief to propagate the state vector using the process model @@ -264,4 +294,134 @@ class TrackFitterUKFFixture : public testing::Test { TEST_F(TrackFitterUKFFixture, TestInstantiation) { assert(true); +} + +TEST_F(TrackFitterUKFFixture, TestAugmentedStateUpdate) +{ + auto testPtr = dynamic_cast(m_ukf.get()); + + // Create a state change where a proton has lost 1 MeV of energy (to rest) + auto p = AtTools::Kinematics::GetRelMomFromKE(1.0, mass_p); + AtTools::AtPropagator::StepState fInitialState; + fInitialState.fMass = mass_p; + fInitialState.fQ = charge_p; + + // Current state + fInitialState.fLastPos = {0, 0, 0}; + fInitialState.fLastMom = {p, 0, 0}; + // State at next measurement point + fInitialState.fPos = {202.78, 0, 0}; // Pulled from SRIM table + fInitialState.fMom = {0, 0, 0}; + double var = 0.01; + TMatrixD cov(6, 6); + cov.Zero(); + for (int i = 0; i < 6; ++i) { + cov(i, i) = var; // Set diagonal covariance to some small number + } + + m_ukf->SetInitialState(fInitialState.fLastPos, fInitialState.fLastMom, cov); + testPtr->getState() = fInitialState; // Set the mean state in the test class (rather than propagate) + + // Fill m_vecXa and m_matPa with the provided state + m_ukf->updateAugmentedStateAndCovariance(); + + assert(testPtr != nullptr); + + // Check the augmented state vector + double eps = 1e-4; + ASSERT_NEAR(testPtr->vecXa()[0], fInitialState.fLastPos.X(), eps); + ASSERT_NEAR(testPtr->vecXa()[1], fInitialState.fLastPos.Y(), eps); + ASSERT_NEAR(testPtr->vecXa()[2], fInitialState.fLastPos.Z(), eps); + ASSERT_NEAR(testPtr->vecXa()[3], fInitialState.fLastMom.R(), eps); + ASSERT_NEAR(testPtr->vecXa()[4], fInitialState.fPos.Theta(), eps); + ASSERT_NEAR(testPtr->vecXa()[5], fInitialState.fPos.Phi(), eps); + ASSERT_NEAR(testPtr->vecXa()[6], 1, eps); // Average energy straggling factor is 1 + + // Check the augmented covariance matrix. Nothing set but energy straggling + ASSERT_NEAR(testPtr->matPa()(0, 0), var, eps); + ASSERT_NEAR(testPtr->matPa()(1, 1), var, eps); + ASSERT_NEAR(testPtr->matPa()(2, 2), var, eps); + ASSERT_NEAR(testPtr->matPa()(3, 3), var, eps); + ASSERT_NEAR(testPtr->matPa()(4, 4), var, eps); + ASSERT_NEAR(testPtr->matPa()(5, 5), var, eps); + + double sigma = 8.74 / 202.78; // Energy straggling factor (from range straggling table) + ASSERT_NEAR(testPtr->matPa()(6, 6), sigma * sigma, eps); +} + +TEST_F(TrackFitterUKFFixture, TestSigmaPoints) +{ + auto testPtr = dynamic_cast(m_ukf.get()); + + // Create a state change where a proton has lost 1 MeV of energy (to rest) + auto p = AtTools::Kinematics::GetRelMomFromKE(1.0, mass_p); + AtTools::AtPropagator::StepState fInitialState; + fInitialState.fMass = mass_p; + fInitialState.fQ = charge_p; + + // Current state + fInitialState.fLastPos = {0, 0, 0}; + fInitialState.fLastMom = {p, 0, 0}; + // State at next measurement point + fInitialState.fPos = {202.78, 0, 0}; // Pulled from SRIM table + fInitialState.fMom = {0, 0, 0}; + double var = 0.01; + TMatrixD cov(6, 6); + cov.Zero(); + for (int i = 0; i < 6; ++i) { + cov(i, i) = var; // Set diagonal covariance to some small number + } + + m_ukf->SetInitialState(fInitialState.fLastPos, fInitialState.fLastMom, cov); + testPtr->getState() = fInitialState; // Set the mean state in the test class (rather than propagate) + + // Fill m_vecXa and m_matPa with the provided state + m_ukf->updateAugmentedStateAndCovariance(); + + testPtr->calcSigmaPoints(); + auto sigmaPoints = testPtr->matSigmaXa(); + + kf::Matrix covXa; + kf::Vector meanXa; + testPtr->calcMeanAndCovFromSigma(sigmaPoints, meanXa, covXa); + + // Verify the mean and cov match what we put in + for (int i = 0; i < kf::TrackFitterUKF::DIM_A; ++i) { + ASSERT_NEAR(meanXa[i], testPtr->vecXa()[i], FLOAT_EPSILON); + for (int j = 0; j < kf::TrackFitterUKF::DIM_A; ++j) { + ASSERT_NEAR(covXa(i, j), testPtr->matPa()(i, j), FLOAT_EPSILON); + } + } + + LOG(debug) << "Sigma points:\n" << sigmaPoints; +} + +TEST_F(TrackFitterUKFFixture, TestPredictionStep) +{ + auto testPtr = dynamic_cast(m_ukf.get()); + + // Create a state change where a proton has lost 1 MeV of energy (to rest) + auto p = AtTools::Kinematics::GetRelMomFromKE(1.0, mass_p); + AtTools::AtPropagator::StepState fInitialState; + fInitialState.fMass = mass_p; + fInitialState.fQ = charge_p; + + // Current state + fInitialState.fLastPos = {0, 0, 0}; + fInitialState.fLastMom = {p, 0, 0}; + // State at next measurement point + fInitialState.fPos = {202.78, 0, 0}; // Pulled from SRIM table + fInitialState.fMom = {0, 0, 0}; + double var = 0.01; + TMatrixD cov(6, 6); + cov.Zero(); + for (int i = 0; i < 6; ++i) { + cov(i, i) = var; // Set diagonal covariance to some small number + } + + m_ukf->SetInitialState(fInitialState.fLastPos, fInitialState.fLastMom, cov); + testPtr->getState() = fInitialState; // Set the mean state in the test class (rather than propagate) + + // Fill m_vecXa and m_matPa with the provided state + m_ukf->updateAugmentedStateAndCovariance(); } \ No newline at end of file diff --git a/AtReconstruction/CMakeLists.txt b/AtReconstruction/CMakeLists.txt index bc0de8a62..585f5ce2a 100755 --- a/AtReconstruction/CMakeLists.txt +++ b/AtReconstruction/CMakeLists.txt @@ -120,6 +120,7 @@ if(TARGET Eigen3::Eigen) set(SRCS ${SRCS} AtPatternRecognition/triplclust/src/orthogonallsq.cxx AtFitter/AtOpenKFTest.cxx + AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx ) message(STATUS "Current sources: ${SRCS}") diff --git a/AtTools/AtKinematics.cxx b/AtTools/AtKinematics.cxx index 3f9d416f7..7470effa9 100644 --- a/AtTools/AtKinematics.cxx +++ b/AtTools/AtKinematics.cxx @@ -291,6 +291,11 @@ double GetRelMom(double gamma, double mass) { return std::sqrt(gamma * gamma - 1) * mass; } +double GetRelMomFromKE(double KE, double mass) +{ + + return std::sqrt((KE + mass) * (KE + mass) - mass * mass); +} /** * Get the mass in MeV of a fragment of mass in amu (or A) @@ -317,4 +322,8 @@ double KE(ROOT::Math::XYZVector mom, double mass) { return std::sqrt(mom.Mag2() + mass * mass) - mass; // Kinetic energy in MeV } +double KE(double mom, double mass) +{ + return std::sqrt(mom * mom + mass * mass) - mass; +} } // namespace AtTools::Kinematics diff --git a/AtTools/AtKinematics.h b/AtTools/AtKinematics.h index 622f347d9..ce09b7a44 100644 --- a/AtTools/AtKinematics.h +++ b/AtTools/AtKinematics.h @@ -85,6 +85,9 @@ double GetSpeed(double p, double mass); /// Calculate the speed of a particle gi * @returns Kinetic energy in MeV */ double KE(ROOT::Math::XYZVector mom, double mass); +double KE(double mom, double mass); + +double GetRelMomFromKE(double KE, double mass); /** * Calculate the velocity vector from momentum and mass diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 153d4fa88..6713e81fd 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -194,6 +194,8 @@ class AtStepper { virtual AtPropagator::StepState Step(const AtPropagator::StepState &state) const = 0; virtual double GetInitialStep() const { return 1e-4; } /// Default initial step size in m + virtual ~AtStepper() = default; + protected: static constexpr double fReltoSImom = 1.60218e-13 / 299792458; // Conversion factor from MeV/c to kg m/s (SI units) }; From 9573b645907bbcf11b8e634d019faa53ae503f02 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 24 Jul 2025 19:59:22 +0200 Subject: [PATCH 45/75] Seemingly working forward step --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 41 ++++- .../OpenKF/kalman_filter/TrackFitterUKF.h | 23 ++- .../kalman_filter/TrackFitterUKFTest.cxx | 145 +++++++++++++++--- AtTools/AtELossTable.cxx | 2 +- AtTools/AtPropagator.cxx | 8 +- 5 files changed, 189 insertions(+), 30 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index 4534689b7..8c3824eca 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -12,12 +12,13 @@ namespace kf { void TrackFitterUKF::SetInitialState(const ROOT::Math::XYZPoint &initialPosition, const ROOT::Math::XYZVector &initialMomentum, const TMatrixD &initialCovariance) { - m_vecX[0] = initialPosition.X(); // X position - m_vecX[1] = initialPosition.Y(); // Y position - m_vecX[2] = initialPosition.Z(); // Z position - m_vecX[3] = initialMomentum.R(); // Momentum magnitude - m_vecX[4] = initialMomentum.Theta(); // Polar angle - m_vecX[5] = initialMomentum.Phi(); // Azimuthal angle + fPropagator.SetState(initialPosition, initialMomentum); // Set the initial state in the propagator + m_vecX[0] = initialPosition.X(); // X position + m_vecX[1] = initialPosition.Y(); // Y position + m_vecX[2] = initialPosition.Z(); // Z position + m_vecX[3] = initialMomentum.R(); // Momentum magnitude + m_vecX[4] = initialMomentum.Theta(); // Polar angle + m_vecX[5] = initialMomentum.Phi(); // Azimuthal angle // Copy elements from initialCovariance to m_matP for (int i = 0; i < m_matP.rows(); ++i) { @@ -27,6 +28,19 @@ void TrackFitterUKF::SetInitialState(const ROOT::Math::XYZPoint &initialPosition } } +void TrackFitterUKF::SetMeasCov(const TMatrixD &measCov) +{ + if (measCov.GetNrows() != TF_DIM_Z || measCov.GetNcols() != TF_DIM_Z) { + throw std::runtime_error("Measurement covariance matrix must be of size " + std::to_string(TF_DIM_Z) + "x" + + std::to_string(TF_DIM_Z)); + } + for (int i = 0; i < TF_DIM_Z; ++i) { + for (int j = 0; j < TF_DIM_Z; ++j) { + m_matR(i, j) = measCov(i, j); // Copy measurement covariance to m_matR + } + } +} + std::array TrackFitterUKF::calculateProcessNoiseMean() { // The process noise is the scaling factor for dedx. By definition the mean should be 1 @@ -83,4 +97,19 @@ Vector TrackFitterUKF::funcF(const Vector TrackFitterUKF::funcH(const Vector &x) +{ + // Measurement function to convert state vector to measurement vector + using namespace ROOT::Math; + Vector vecZ; + XYZPoint fPos(x[0], x[1], x[2]); // Position from state vector + + // Calculate the measurement vector based on the position and momentum + vecZ[0] = fPos.X(); // X coordinate + vecZ[1] = fPos.Y(); // Y coordinate + vecZ[2] = fPos.Z(); // Z coordinate + + return vecZ; // Return the measurement vector +} + } // namespace kf \ No newline at end of file diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index 42459991c..a5227a2da 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -411,11 +411,15 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { void SetInitialState(const ROOT::Math::XYZPoint &initialPosition, const ROOT::Math::XYZVector &initialMomentum, const TMatrixD &initialCovariance); + void SetMeasCov(const TMatrixD &measCov); + kf::Vector funcF(const kf::Vector &x, const kf::Vector &v, const kf::Vector &z); + kf::Vector funcH(const kf::Vector &x); - void predictUKF(const Vector &z) + void predictUKF(const ROOT::Math::XYZPoint &z) { + // First we need to propagate the mean state vector to the next measurement point. fPropagator.PropagateToMeasurementSurface(AtTools::AtMeasurementPoint(z), *fStepper); fMeanStep = fPropagator.GetState(); // Get the mean step information from the propagator @@ -424,10 +428,23 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { using namespace ROOT::Math; fMeasurementPlane = Plane3D(fMeanStep.fMom.Unit(), XYZPoint(z)); // Create a plane using the momentum direction and position - + Vector zVec; // Initialize the measurement vector + zVec[0] = z.X(); + zVec[1] = z.Y(); + zVec[2] = z.Z(); auto callback = [this](const kf::Vector &x_, const kf::Vector &v_, const kf::Vector &z_) { return funcF(x_, v_, z_); }; - TrackFitterUKFBase::predictUKF(callback, z); + TrackFitterUKFBase::predictUKF(callback, zVec); + } + + void correctUKF(const ROOT::Math::XYZPoint &z) + { + Vector zVec; // Initialize the measurement vector + zVec[0] = z.X(); + zVec[1] = z.Y(); + zVec[2] = z.Z(); + auto callback = [this](const kf::Vector &x_) { return funcH(x_); }; + TrackFitterUKFBase::correctUKF(callback, zVec); } protected: diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index 4ca0f983c..9709f4797 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -234,6 +234,7 @@ class TrackFitterUKFTest : public TrackFitterUKF { Matrix matSigmaXa() { return m_matSigmaXa; } AtTools::AtPropagator &getPropagator() { return fPropagator; } AtTools::AtPropagator::StepState &getState() { return fMeanStep; } + Vector getStateVector() { return m_vecX; } void calcSigmaPoints() { m_matSigmaXa = calculateSigmaPoints(m_vecXa, m_matPa); } void calcMeanAndCovFromSigma(const Matrix &sigmaXa, Vector &meanXa, @@ -268,10 +269,12 @@ class TrackFitterUKFFixture : public testing::Test { const double charge_p = 1.602176634e-19; // Charge of protonble std::unique_ptr m_ukf{nullptr}; + AtTools::AtELossModel *fElossModel{nullptr}; void SetUp() override { auto elossModel = std::make_unique(0); + fElossModel = elossModel.get(); // Store the model for later use elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path AtTools::AtPropagator propagator(charge_p, mass_p, std::move(elossModel)); propagator.SetBField(fBField); @@ -398,30 +401,136 @@ TEST_F(TrackFitterUKFFixture, TestSigmaPoints) TEST_F(TrackFitterUKFFixture, TestPredictionStep) { + using namespace AtTools; + using namespace ROOT::Math; + + // Set the conditions for simulated data auto testPtr = dynamic_cast(m_ukf.get()); + fElossModel->SetDensity(3.3084e-05); + testPtr->getPropagator().SetEField({0, 0, 0}); // No electric field + testPtr->getPropagator().SetBField({0, 0, 2.85}); // Magnetic field - // Create a state change where a proton has lost 1 MeV of energy (to rest) - auto p = AtTools::Kinematics::GetRelMomFromKE(1.0, mass_p); - AtTools::AtPropagator::StepState fInitialState; - fInitialState.fMass = mass_p; - fInitialState.fQ = charge_p; + // Attempt to predict the state of a proton from simulation + XYZPoint startPos(-3.40046e-05, -1.49863e-05, 0.10018); // Start position in cm + startPos *= 10; // Convert to mm + XYZVector startMom(0.00935463, -0.0454279, 0.00826042); // Start momentum in GeV/c + startMom *= 1e3; // Convert to MeV/c + + double sigma_pos = 10; // Position uncertainty of 10 mm + double sigma_mom = 0.1 * startMom.R(); // Momentum uncertainty of 10% MeV/c + double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree + double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree - // Current state - fInitialState.fLastPos = {0, 0, 0}; - fInitialState.fLastMom = {p, 0, 0}; - // State at next measurement point - fInitialState.fPos = {202.78, 0, 0}; // Pulled from SRIM table - fInitialState.fMom = {0, 0, 0}; - double var = 0.01; TMatrixD cov(6, 6); cov.Zero(); - for (int i = 0; i < 6; ++i) { - cov(i, i) = var; // Set diagonal covariance to some small number + for (int i = 0; i < 3; ++i) { + + cov(i, i) = sigma_pos * sigma_pos; // Set diagonal covariance to some small number } + cov(3, 3) = sigma_mom * sigma_mom; // Momentum uncertainty + cov(4, 4) = sigma_theta * sigma_theta; // Angular uncertainty + cov(5, 5) = sigma_phi * sigma_phi; // Angular uncertainty + + m_ukf->SetInitialState(startPos, startMom, cov); + + XYZPoint point({-1.4895, -4.8787, 1.01217}); // measurement point in cm + point *= 10; // Convert to mm + m_ukf->predictUKF(point); + + // Check the predicted state vector before correction + auto stateVec = testPtr->getStateVector(); + XYZPoint predictedPos(stateVec[0], stateVec[1], stateVec[2]); + Polar3DVector predictedMom(stateVec[3], stateVec[4], stateVec[5]); + LOG(info) << "Measurement point: " << point; + LOG(info) << "Mean position: " << testPtr->getState().fLastPos; + LOG(info) << "Predicted position (sigma): " << predictedPos; + + LOG(info) << "Mean momentum: " << testPtr->getState().fLastMom; + LOG(info) << "Predicted momentum (sigma): " << XYZPoint(predictedMom); + + LOG(info) << "Initial covariance matrix:\n"; + cov.Print(); + LOG(info) << "Predicted covariance matrix:\n" << testPtr->matP(); +} - m_ukf->SetInitialState(fInitialState.fLastPos, fInitialState.fLastMom, cov); - testPtr->getState() = fInitialState; // Set the mean state in the test class (rather than propagate) +TEST_F(TrackFitterUKFFixture, TestPredictionAndCorrectStep) +{ + using namespace AtTools; + using namespace ROOT::Math; - // Fill m_vecXa and m_matPa with the provided state - m_ukf->updateAugmentedStateAndCovariance(); + // Set the conditions for simulated data + auto testPtr = dynamic_cast(m_ukf.get()); + fElossModel->SetDensity(3.3084e-05); + testPtr->getPropagator().SetEField({0, 0, 0}); // No electric field + testPtr->getPropagator().SetBField({0, 0, 2.85}); // Magnetic field + + // Attempt to predict the state of a proton from simulation + XYZPoint startPos(-3.40046e-05, -1.49863e-05, 0.10018); // Start position in cm + startPos *= 10; // Convert to mm + XYZVector startMom(0.00935463, -0.0454279, 0.00826042); // Start momentum in GeV/c + startMom *= 1e3; // Convert to MeV/c + + double sigma_pos = 1; // Position uncertainty of 10 mm + double sigma_mom = 0.01 * startMom.R(); // Momentum uncertainty of 10% MeV/c + double sigma_theta = 5 * M_PI / 180; // Angular uncertainty of 1 degree + double sigma_phi = 5 * M_PI / 180; // Angular uncertainty of 1 degree + + TMatrixD cov(6, 6); + cov.Zero(); + for (int i = 0; i < 3; ++i) { + + cov(i, i) = sigma_pos * sigma_pos; // Set diagonal covariance to some small number + } + cov(3, 3) = sigma_mom * sigma_mom; // Momentum uncertainty + cov(4, 4) = sigma_theta * sigma_theta; // Angular uncertainty + cov(5, 5) = sigma_phi * sigma_phi; // Angular uncertainty + + m_ukf->SetInitialState(startPos, startMom, cov); + + XYZPoint point({-1.4895, -4.8787, 1.01217}); // measurement point in cm + point *= 10; // Convert to mm + TMatrixD cov_meas(3, 3); + cov_meas.Zero(); + for (int i = 0; i < 3; ++i) { + cov_meas(i, i) = sigma_pos * sigma_pos; + } + m_ukf->SetMeasCov(cov_meas); // Set measurement noise covariance + + /** Make prediction */ + m_ukf->predictUKF(point); + + // Check the predicted state vector before correction + auto stateVec = testPtr->getStateVector(); + XYZPoint predictedPos(stateVec[0], stateVec[1], stateVec[2]); + Polar3DVector predictedMom(stateVec[3], stateVec[4], stateVec[5]); + + LOG(info) << "Prediction step complete: " << std::endl; + + LOG(info) << "Measurement point: " << point; + LOG(info) << "Mean position: " << testPtr->getState().fLastPos; + LOG(info) << "Predicted position (sigma): " << predictedPos; + + LOG(info) << "Mean momentum: " << testPtr->getState().fLastMom; + LOG(info) << "Predicted momentum (sigma): " << XYZPoint(predictedMom); + + LOG(info) << "Initial covariance matrix:\n"; + cov.Print(); + LOG(info) << "Predicted covariance matrix:\n" << testPtr->matP(); + + // Now correct the state with the measurement + m_ukf->correctUKF(point); + auto correctedStateVec = testPtr->getStateVector(); + XYZPoint correctedPos(correctedStateVec[0], correctedStateVec[1], correctedStateVec[2]); + Polar3DVector correctedMom(correctedStateVec[3], correctedStateVec[4], correctedStateVec[5]); + + LOG(info) << "Correction complete: " << std::endl; + + LOG(info) << "Measurement point: " << point; + LOG(info) << "Predicted position (sigma): " << predictedPos; + LOG(info) << "Corrected position: " << correctedPos; + + LOG(info) << "Predicted momentum (sigma): " << XYZPoint(predictedMom); + LOG(info) << "Corrected momentum: " << XYZPoint(correctedMom); + + LOG(info) << "Corrected covariance matrix:\n" << testPtr->matP(); } \ No newline at end of file diff --git a/AtTools/AtELossTable.cxx b/AtTools/AtELossTable.cxx index 8f093a118..33159c92d 100644 --- a/AtTools/AtELossTable.cxx +++ b/AtTools/AtELossTable.cxx @@ -26,7 +26,7 @@ void AtELossTable::SetDensity(double density) elem = fScaling / elem; // scale dx/de by the density } for (auto &elem : r_var) { - LOG(info) << "Scaling range variance by " << fScaling * fScaling; + LOG(debug) << "Scaling range variance by " << fScaling * fScaling; elem = elem / (fScaling * fScaling); // scale range variance by the density } LoadTable(x, y); diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index da36cca07..8b458ed6d 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -79,11 +79,15 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur fState.h = stepper.GetInitialStep(); // Set the initial step size auto KE_initial = Kinematics::KE(fState.fMom, fState.fMass); + if (KE_initial < fStopTol) { + LOG(warning) << "Initial kinetic energy is below stopping threshold, cannot propagate."; + return; // Cannot propagate if the initial kinetic energy is below the stopping threshold + } stepper.fDeriv = [this](const XYZPoint &pos, const XYZVector &mom) { return this->Derivatives(pos, mom); }; while (true) { - LOG(info) << "Position: " << GetPosition().X() / 10 << ", " << GetPosition().Y() / 10 << ", " - << GetPosition().Z() / 10; + LOG(debug) << "Position: " << GetPosition().X() / 10 << ", " << GetPosition().Y() / 10 << ", " + << GetPosition().Z() / 10; LOG(debug) << "Momentum: " << GetMomentum().X() << ", " << GetMomentum().Y() << ", " << GetMomentum().Z(); auto result = stepper.Step(fState); From 5e8471c2108f7355d8d52254cf2aef9c99fef56a Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Sat, 26 Jul 2025 22:35:05 +0200 Subject: [PATCH 46/75] Add full test track. Decomp failing --- .../OpenKF/kalman_filter/TrackFitterUKF.h | 21 ++- AtReconstruction/AtReconstructionLinkDef.h | 2 + macro/tests/UKF/UKFSingleTrack.C | 175 ++++++++++++++++++ 3 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 macro/tests/UKF/UKFSingleTrack.C diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index a5227a2da..d8011f8d3 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -275,14 +275,21 @@ class TrackFitterUKFBase : public KalmanFilter { { const float32_t scalarMultiplier{std::sqrt(STATE_DIM + m_kappa)}; // sqrt(n + \kappa) - // cholesky factorization to get matrix Pxx square-root Eigen::LLT> lltOfPa(matPa); if (lltOfPa.info() != Eigen::Success) { - LOG(error) << "Cholesky decomposition failed, matrix is not positive definite."; - for (int32_t i{0}; i < STATE_DIM; ++i) { - LOG(error) << "Pxx[" << i << "]: " << matPa(i, i); + LOG(error) << "Cholesky decomposition failed, matrix is not positive definite. Attempting recovery..."; + // Add a small value to the diagonal to regularize the matrix + Matrix matPaReg = matPa; + matPaReg = (matPaReg + matPaReg.transpose()) * 0.5; // Ensure symmetry + matPaReg += Matrix::Identity() * 1e-6; // Regularization value + lltOfPa.compute(matPaReg); + if (lltOfPa.info() != Eigen::Success) { + for (int32_t i{0}; i < STATE_DIM; ++i) { + LOG(error) << "\n" << matPaReg; + } + throw std::runtime_error( + "Cholesky decomposition failed, matrix is not positive definite even after regularization."); } - throw std::runtime_error("Cholesky decomposition failed, matrix is not positive definite."); } Matrix matSa{lltOfPa.matrixL()}; // sqrt(P_{a}) @@ -412,6 +419,10 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { const TMatrixD &initialCovariance); void SetMeasCov(const TMatrixD &measCov); + std::array GetStateVector() const + { + return {m_vecX[0], m_vecX[1], m_vecX[2], m_vecX[3], m_vecX[4], m_vecX[5]}; + } kf::Vector funcF(const kf::Vector &x, const kf::Vector &v, const kf::Vector &z); diff --git a/AtReconstruction/AtReconstructionLinkDef.h b/AtReconstruction/AtReconstructionLinkDef.h index f9f287f94..7b1780c62 100755 --- a/AtReconstruction/AtReconstructionLinkDef.h +++ b/AtReconstruction/AtReconstructionLinkDef.h @@ -41,6 +41,8 @@ #pragma link C++ namespace kf; #pragma link C++ namespace kf::util; +#pragma link C++ class kf::TrackFitterUKFBase - !; +#pragma link C++ class kf::TrackFitterUKF - !; /* Classes that depend on Genfit2 */ #pragma link C++ class genfit::AtSpacepointMeasurement + ; diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C new file mode 100644 index 000000000..402643318 --- /dev/null +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -0,0 +1,175 @@ +std::string getEnergyPath() +{ + auto env = std::getenv("VMCWORKDIR"); + if (env == nullptr) { + return "../../resources/energy_loss/HinH.txt"; // Default path assuming cwd is build/AtTools + } + return std::string(env) + "/resources/energy_loss/HinH.txt"; // Use environment variable +} + +const double mass_p = 938.272; // Mass of proton in MeV/c^2 +const double charge_p = 1.602176634e-19; // Charge of proton + +// Simulated (measurement) hits +std::vector x, y, z, Eloss; + +void LoadHits() +{ + std::ifstream infile("hits.txt"); + double xi, yi, zi, Ei; + int i = 0; + double eLoss = 0; + + // Save first point. + eLoss = Ei * 1e3; // Initialize energy loss + x.push_back(xi * 10); // Convert to mm + y.push_back(yi * 10); // Convert to mm + z.push_back(zi * 10); // Convert to mm + + while (infile >> xi >> yi >> zi >> Ei) { + Ei *= 1e3; // Convert to MeV + + if (i++ % 5 != 0) { + eLoss += Ei; + continue; // Skip every 5th point + } + + double dx = x.back() - xi; + double dy = y.back() - yi; + double dz = z.back() - zi; + double distance = std::sqrt(dx * dx + dy * dy + dz * dz); + + x.push_back(xi * 10); + y.push_back(yi * 10); + z.push_back(zi * 10); + Eloss.push_back(eLoss); + eLoss = 0; // Reset energy loss for the next segment + } + + std::cout << "Finished loading hits. Total points: " << x.size() << std::endl; +} + +// This test should plot the trajectory of a particle in a magnetic field using +// the output from GEANT and the AtPropagator class. +void UKFSingleTrack() +{ + LoadHits(); // Load hits from file + + std::cout << " Creating the UKF class" << std::endl; + using namespace AtTools; + + std::vector x2, y2, z2, Eloss2; + + // Setup the Propagator for UKF + auto elossModel = std::make_unique(0); + elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path + elossModel->SetDensity(3.553e-5); // Set density in g/cm^3 for 300 torr H2 + AtTools::AtPropagator propagator(charge_p, mass_p, std::move(elossModel)); + propagator.SetEField({0, 0, 0}); // No electric field + propagator.SetBField({0, 0, 2.85}); // Magnetic field + + // Setup stepper for UKF + auto stepper = std::make_unique(); + + // Setup UKF + kf::TrackFitterUKF ukf(std::move(propagator), std::move(stepper)); + + XYZPoint startPos(-3.40046e-05, -1.49863e-05, 0.10018); // Start position in cm + startPos *= 10; // Convert to mm + XYZVector startMom(0.00935463, -0.0454279, 0.00826042); // Start momentum in GeV/c + startMom *= 1e3; // Convert to MeV/c + + // Initial uncertainties + double sigma_pos = 5; // Position uncertainty of 10 mm + double sigma_mom = 0.01 * startMom.R(); // Momentum uncertainty of 10% MeV/c + double sigma_theta = 5 * M_PI / 180; // Angular uncertainty of 1 degree + double sigma_phi = 5 * M_PI / 180; // Angular uncertainty of 1 degree + + TMatrixD cov(6, 6); + cov.Zero(); + for (int i = 0; i < 3; ++i) { + + cov(i, i) = sigma_pos * sigma_pos; // Set diagonal covariance to some small number + } + cov(3, 3) = sigma_mom * sigma_mom; // Momentum uncertainty + cov(4, 4) = sigma_theta * sigma_theta; // Angular uncertainty + cov(5, 5) = sigma_phi * sigma_phi; // Angular uncertainty + + // Set the initial state + std::cout << "Setting initial state" << std::endl; + + ukf.SetInitialState(startPos, startMom, cov); + TMatrixD cov_meas(3, 3); + cov_meas.Zero(); + for (int i = 0; i < 3; ++i) { + cov_meas(i, i) = sigma_pos * sigma_pos; + } + // Create the covariance for measurement points. Assume constant + + x2.push_back(startPos.X()); + y2.push_back(startPos.Y()); + z2.push_back(startPos.Z()); + + ROOT::Math::XYZVector lastMom = ROOT::Math::XYZVector(startMom.X(), startMom.Y(), startMom.Z()); + + for (size_t i = 0; i < x.size(); ++i) { + std::cout << "Processing hit " << i << " of " << x.size() << std::endl; + XYZPoint point(x[i], y[i], z[i]); // measurement point in mm + ukf.SetMeasCov(cov_meas); // Set measurement noise covariance + + ukf.predictUKF(point); + ukf.correctUKF(point); + + auto state = ukf.GetStateVector(); + ROOT::Math::XYZPoint pos(state[0], state[1], state[2]); + ROOT::Math::Polar3DVector momPolar(state[3], state[4], state[5]); + ROOT::Math::XYZVector mom(momPolar); + + std::cout << "Predicted position: " << pos << std::endl; + std::cout << "Predicted momentum: " << mom << std::endl; + std::cout << "Measurement point: " << point << std::endl; + + auto KE_in = Kinematics::KE(lastMom, mass_p); + auto KE_out = Kinematics::KE(mom, mass_p); + lastMom = mom; + + x2.push_back(pos.X()); + y2.push_back(pos.Y()); + z2.push_back(pos.Z()); + Eloss2.push_back((KE_in - KE_out)); + } + + TGraph2D *track = new TGraph2D(x.size(), x.data(), y.data(), z.data()); + track->SetTitle("Particle Track;X [mm];Y [mm];Z [mm]"); + track->SetMarkerStyle(20); + track->SetMarkerSize(0.8); + + TGraph2D *track2 = new TGraph2D(x2.size(), x2.data(), y2.data(), z2.data()); + track2->SetTitle("Propagated Particle Track;X [mm];Y [mm];Z [mm]"); + track2->SetMarkerStyle(21); + track2->SetMarkerSize(0.8); + track2->SetMarkerColor(kRed); + + TCanvas *c1 = new TCanvas("c1", "Particle Track", 800, 600); + track->Draw("P"); + track2->Draw("PSAME"); + + TGraph *elossGraph = new TGraph(Eloss.size()); + for (size_t i = 0; i < Eloss.size(); ++i) { + elossGraph->SetPoint(i, i, Eloss[i]); + } + elossGraph->SetTitle("Energy Loss per Hit;Hit Number;Energy Loss [MeV]"); + elossGraph->SetMarkerStyle(20); + + TGraph *eloss2Graph = new TGraph(Eloss2.size()); + for (size_t i = 0; i < Eloss2.size(); ++i) { + eloss2Graph->SetPoint(i, i, Eloss2[i]); + } + eloss2Graph->SetTitle("Propagated Energy Loss per Hit;Hit Number;Energy Loss [MeV]"); + eloss2Graph->SetMarkerStyle(21); + eloss2Graph->SetMarkerColor(kRed); + + TCanvas *c2 = new TCanvas("c2", "Energy Loss per Hit", 800, 600); + elossGraph->Draw("AP"); + // eloss2Graph->Draw("PSAME"); +} \ No newline at end of file From 479bb8471957be8423681cf98887be4dfd6ddb5a Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Sat, 26 Jul 2025 23:01:07 +0200 Subject: [PATCH 47/75] Fix decomposition, but UKF seems suspicious. It seems to be propagating initially in the wrong direction. It gets "dragged" back to following the points, but that seems to blow up the momentum uncertainty (which makes sense if the initial momentum is somehow in the wrong direction, and the measurement points need to force it to change its mind). I think that causes it to fail somewhere around point 23, becuase of sigma points are sampling such a large distribution due to the momentum uncertainty that they start stopping in the gas (or have a negative momentum). Checks for physicality need to be added (step size, momentum values, etc.) --- .../AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h | 9 ++++++--- macro/tests/UKF/UKFSingleTrack.C | 13 ++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index d8011f8d3..7714bc4dc 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -284,9 +284,12 @@ class TrackFitterUKFBase : public KalmanFilter { matPaReg += Matrix::Identity() * 1e-6; // Regularization value lltOfPa.compute(matPaReg); if (lltOfPa.info() != Eigen::Success) { - for (int32_t i{0}; i < STATE_DIM; ++i) { - LOG(error) << "\n" << matPaReg; - } + LOG(error) << "Cholesky decomposition failed even after regularization. Attempting again"; + matPaReg += Matrix::Identity() * 1e-3; // Increase regularization value + lltOfPa.compute(matPaReg); + } + if (lltOfPa.info() != Eigen::Success) { + LOG(error) << "\n" << matPaReg; throw std::runtime_error( "Cholesky decomposition failed, matrix is not positive definite even after regularization."); } diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index 402643318..faa7071b8 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -21,6 +21,7 @@ void LoadHits() double eLoss = 0; // Save first point. + infile >> xi >> yi >> zi >> Ei; eLoss = Ei * 1e3; // Initialize energy loss x.push_back(xi * 10); // Convert to mm y.push_back(yi * 10); // Convert to mm @@ -47,6 +48,7 @@ void LoadHits() } std::cout << "Finished loading hits. Total points: " << x.size() << std::endl; + std::cout << x[0] << " " << x[1] << " " << x[2] << std::endl; } // This test should plot the trajectory of a particle in a magnetic field using @@ -82,8 +84,8 @@ void UKFSingleTrack() // Initial uncertainties double sigma_pos = 5; // Position uncertainty of 10 mm double sigma_mom = 0.01 * startMom.R(); // Momentum uncertainty of 10% MeV/c - double sigma_theta = 5 * M_PI / 180; // Angular uncertainty of 1 degree - double sigma_phi = 5 * M_PI / 180; // Angular uncertainty of 1 degree + double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree + double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree TMatrixD cov(6, 6); cov.Zero(); @@ -112,13 +114,17 @@ void UKFSingleTrack() ROOT::Math::XYZVector lastMom = ROOT::Math::XYZVector(startMom.X(), startMom.Y(), startMom.Z()); - for (size_t i = 0; i < x.size(); ++i) { + // Skip the first point since it is the initial state. + // Stop when things break. + for (size_t i = 1; i < 21; ++i) { std::cout << "Processing hit " << i << " of " << x.size() << std::endl; XYZPoint point(x[i], y[i], z[i]); // measurement point in mm ukf.SetMeasCov(cov_meas); // Set measurement noise covariance ukf.predictUKF(point); + std::cout << std::endl << "Prediction step complete." << std::endl; ukf.correctUKF(point); + std::cout << std::endl << "Correction step complete." << std::endl; auto state = ukf.GetStateVector(); ROOT::Math::XYZPoint pos(state[0], state[1], state[2]); @@ -127,6 +133,7 @@ void UKFSingleTrack() std::cout << "Predicted position: " << pos << std::endl; std::cout << "Predicted momentum: " << mom << std::endl; + std::cout << "Measurement point: " << point << std::endl; auto KE_in = Kinematics::KE(lastMom, mass_p); From ab3b6425af9a0311945d7df21bd9f06a9c0ff579 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Sat, 26 Jul 2025 23:23:04 +0200 Subject: [PATCH 48/75] Fix issue with initial COV being too high. Filter not follows measurement points. --- AtTools/AtPropagator.cxx | 10 +++++----- macro/tests/UKF/UKFSingleTrack.C | 11 +++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 8b458ed6d..d3ee01989 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -75,7 +75,7 @@ void AtPropagator::PropagateOneStep(AtStepper &stepper) void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &surface, AtStepper &stepper) { - LOG(info) << "Propagating to measurement surface"; + LOG(info) << "Propagating to measurement surface from: " << fState.fPos; fState.h = stepper.GetInitialStep(); // Set the initial step size auto KE_initial = Kinematics::KE(fState.fMom, fState.fMass); @@ -109,9 +109,9 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur LOG(info) << "Distance to plane: " << approach << " mm"; LOG(info) << "Final step size: " << finalH << " mm"; - LOG(info) << "Current position: " << fState.fLastPos.X() << ", " << fState.fLastPos.Y() << ", " + LOG(info) << "Position before surface: " << fState.fLastPos.X() << ", " << fState.fLastPos.Y() << ", " << fState.fLastPos.Z(); - LOG(info) << "Current momentum: " << fState.fLastMom.X() << ", " << fState.fLastMom.Y() << ", " + LOG(info) << "Momentum before surface: " << fState.fLastMom.X() << ", " << fState.fLastMom.Y() << ", " << fState.fLastMom.Z(); finalH = approach * 1e-3; // Convert to meters for the RK4 step @@ -187,16 +187,16 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur double distanceToSurface = surface.Distance(fState.fPos); double KE_final = Kinematics::KE(fState.fMom, fState.fMass); - LOG(info) << "Initial KE" << KE_initial << " MeV"; + LOG(info) << "Initial KE: " << KE_initial << " MeV"; LOG(info) << "Final KE: " << KE_final << " MeV"; auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV - LOG(info) << "------- End of RK4 interation ---------"; LOG(info) << "Particle stopped: " << particleStopped; LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; LOG(info) << "Distance to surface: " << distanceToSurface << " mm"; LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; LOG(info) << "Scaling factor: " << fScalingFactor; LOG(info) << "Final Position: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " << fState.fPos.Z(); + LOG(info) << "------- End of RK4 interation ---------" << std::endl; // If we reached the measurement surface, we should project the position onto the surface if (reachedMeasurementPoint) { diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index faa7071b8..71d37742e 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -79,10 +79,13 @@ void UKFSingleTrack() XYZPoint startPos(-3.40046e-05, -1.49863e-05, 0.10018); // Start position in cm startPos *= 10; // Convert to mm XYZVector startMom(0.00935463, -0.0454279, 0.00826042); // Start momentum in GeV/c - startMom *= 1e3; // Convert to MeV/c + startMom *= 1e3; + + XYZPoint nextPos(x[1], y[1], z[1]); + startMom = startMom.R() * (nextPos - startPos).Unit(); // Set momentum direction towards the first hit // Initial uncertainties - double sigma_pos = 5; // Position uncertainty of 10 mm + double sigma_pos = 1; // Position uncertainty of 10 mm double sigma_mom = 0.01 * startMom.R(); // Momentum uncertainty of 10% MeV/c double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree @@ -115,8 +118,8 @@ void UKFSingleTrack() ROOT::Math::XYZVector lastMom = ROOT::Math::XYZVector(startMom.X(), startMom.Y(), startMom.Z()); // Skip the first point since it is the initial state. - // Stop when things break. - for (size_t i = 1; i < 21; ++i) { + // Stop when things break (point 21). + for (size_t i = 1; i < x.size() && i < 100; ++i) { std::cout << "Processing hit " << i << " of " << x.size() << std::endl; XYZPoint point(x[i], y[i], z[i]); // measurement point in mm ukf.SetMeasCov(cov_meas); // Set measurement noise covariance From 3c516c265574aaf74838325270588f84b4b1a903 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Mon, 28 Jul 2025 23:49:01 +0200 Subject: [PATCH 49/75] Move most propagator logging to debug --- AtTools/AtPropagator.cxx | 71 ++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index d3ee01989..5838ac706 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -75,7 +75,7 @@ void AtPropagator::PropagateOneStep(AtStepper &stepper) void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &surface, AtStepper &stepper) { - LOG(info) << "Propagating to measurement surface from: " << fState.fPos; + LOG(debug) << "Propagating to measurement surface from: " << fState.fPos; fState.h = stepper.GetInitialStep(); // Set the initial step size auto KE_initial = Kinematics::KE(fState.fMom, fState.fMass); @@ -103,16 +103,16 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur if (reachedMeasurementPoint && !particleStopped && !momentumReversed) { // We reached the measurement surface, so we should figure out how far we are from the measurement point - LOG(info) << "------ Reached measurement surface ------"; + LOG(debug) << "------ Reached measurement surface ------"; double finalH = (fState.fLastPos - fState.fPos).R(); // Distance traveled in the last step double approach = surface.Distance(fState.fLastPos); - LOG(info) << "Distance to plane: " << approach << " mm"; - LOG(info) << "Final step size: " << finalH << " mm"; - LOG(info) << "Position before surface: " << fState.fLastPos.X() << ", " << fState.fLastPos.Y() << ", " - << fState.fLastPos.Z(); - LOG(info) << "Momentum before surface: " << fState.fLastMom.X() << ", " << fState.fLastMom.Y() << ", " - << fState.fLastMom.Z(); + LOG(debug) << "Distance to plane: " << approach << " mm"; + LOG(debug) << "Final step size: " << finalH << " mm"; + LOG(debug) << "Position before surface: " << fState.fLastPos.X() << ", " << fState.fLastPos.Y() << ", " + << fState.fLastPos.Z(); + LOG(debug) << "Momentum before surface: " << fState.fLastMom.X() << ", " << fState.fLastMom.Y() << ", " + << fState.fLastMom.Z(); finalH = approach * 1e-3; // Convert to meters for the RK4 step fState.h = finalH; // Set the step size to the distance to the surface @@ -139,10 +139,10 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur double deltaE = KE_last - fStopTol; deltaE = std::max(deltaE, 0.0); // Ensure we don't have negative energy loss - LOG(info) << "Last KE: " << KE_last << " MeV"; - LOG(info) << "Energy to loose to stop: " << deltaE << " MeV"; + LOG(debug) << "Last KE: " << KE_last << " MeV"; + LOG(debug) << "Energy to loose to stop: " << deltaE << " MeV"; double h_Stop = deltaE / fELossModel->GetdEdx(KE_last); // Distance to stop in mm - LOG(info) << "Estimated distance to stop: " << h_Stop << " mm"; + LOG(debug) << "Estimated distance to stop: " << h_Stop << " mm"; fState.h = h_Stop * 1e-3; // Convert to meters for the RK4 step fState.fPos = fState.fLastPos; // Set position to last position @@ -155,9 +155,9 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur auto origH = fState.h; // Save original step size fState = result; // Update the internal state fState.h = origH; // Restore original step size - LOG(info) << "Propagated to stopping point: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " - << fState.fPos.Z(); - LOG(info) << "Energy after stopping: " << Kinematics::KE(fState.fMom, fState.fMass) << " MeV"; + LOG(debug) << "Propagated to stopping point: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " + << fState.fPos.Z(); + LOG(debug) << "Energy after stopping: " << Kinematics::KE(fState.fMom, fState.fMass) << " MeV"; while (surface.fClipToSurface) { fScalingFactor = 0; // Turn off energy loss. @@ -168,7 +168,7 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur reachedMeasurementPoint = true; break; } - LOG(info) << "Propagating to surface after stopping with step size: " << h << " mm"; + LOG(debug) << "Propagating to surface after stopping with step size: " << h << " mm"; fState.h = h * 1e-3; // Convert to meters for the RK4 step result = stepper.Step(fState); if (!result) { @@ -176,8 +176,8 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur return; // Abort propagation if step failed } fState = result; // Update the internal state - LOG(info) << "New position after adjusting step size: " << fState.fPos.X() << ", " << fState.fPos.Y() - << ", " << fState.fPos.Z(); + LOG(debug) << "New position after adjusting step size: " << fState.fPos.X() << ", " << fState.fPos.Y() + << ", " << fState.fPos.Z(); } fState.fLastMom = fState.fMom; fState.fMom = XYZVector(0, 0, 0); // Set momentum to zero since we stopped @@ -187,23 +187,24 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur double distanceToSurface = surface.Distance(fState.fPos); double KE_final = Kinematics::KE(fState.fMom, fState.fMass); - LOG(info) << "Initial KE: " << KE_initial << " MeV"; - LOG(info) << "Final KE: " << KE_final << " MeV"; + LOG(debug) << "Initial KE: " << KE_initial << " MeV"; + LOG(debug) << "Final KE: " << KE_final << " MeV"; auto calc_eLoss = KE_initial - KE_final; // Energy loss in MeV - LOG(info) << "Particle stopped: " << particleStopped; - LOG(info) << "Reached measurement point: " << reachedMeasurementPoint; - LOG(info) << "Distance to surface: " << distanceToSurface << " mm"; - LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; - LOG(info) << "Scaling factor: " << fScalingFactor; - LOG(info) << "Final Position: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " << fState.fPos.Z(); - LOG(info) << "------- End of RK4 interation ---------" << std::endl; + LOG(debug) << "Particle stopped: " << particleStopped; + LOG(debug) << "Reached measurement point: " << reachedMeasurementPoint; + LOG(debug) << "Distance to surface: " << distanceToSurface << " mm"; + LOG(debug) << "Calculated energy loss: " << calc_eLoss << " MeV"; + LOG(debug) << "Scaling factor: " << fScalingFactor; + LOG(debug) << "Final Position: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " << fState.fPos.Z(); + LOG(debug) << "------- End of RK4 interation ---------" << std::endl; // If we reached the measurement surface, we should project the position onto the surface if (reachedMeasurementPoint) { fState.fPos = surface.ProjectToSurface(fState.fPos); } - LOG(info) << "Projected on surface: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " << fState.fPos.Z(); - LOG(info) << "Final Momentum: " << fState.fMom.X() << ", " << fState.fMom.Y() << ", " << fState.fMom.Z(); + LOG(debug) << "Projected on surface: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " + << fState.fPos.Z(); + LOG(debug) << "Final Momentum: " << fState.fMom.X() << ", " << fState.fMom.Y() << ", " << fState.fMom.Z(); return; } } // End of loop over RK4 integration @@ -211,7 +212,7 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &surface, double eLoss, AtStepper &stepper) { - LOG(info) << "Propagating to surface with eLoss: " << eLoss; + LOG(debug) << "Propagating to surface with eLoss: " << eLoss; if (eLoss == 0) { LOG(warn) << "No energy loss specified, propagating without energy loss adjustment."; @@ -243,15 +244,15 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur double KE_final = Kinematics::KE(fState.fMom, fState.fMass); calc_eLoss = KE_initial - KE_final; // Energy loss in MeV fScalingFactor *= eLoss / calc_eLoss; - LOG(info) << "Desired energy loss: " << eLoss << " MeV"; - LOG(info) << "Calculated energy loss: " << calc_eLoss << " MeV"; - LOG(info) << "Difference: " << calc_eLoss - eLoss << " MeV"; - LOG(info) << "New scaling factor: " << fScalingFactor; - LOG(info) << "Condition: " << (std::abs(calc_eLoss - eLoss) > 1e-4); + LOG(debug) << "Desired energy loss: " << eLoss << " MeV"; + LOG(debug) << "Calculated energy loss: " << calc_eLoss << " MeV"; + LOG(debug) << "Difference: " << calc_eLoss - eLoss << " MeV"; + LOG(debug) << "New scaling factor: " << fScalingFactor; + LOG(debug) << "Condition: " << (std::abs(calc_eLoss - eLoss) > 1e-4); } // End loop over energy loss convergence - LOG(info) << "Energy loss converged after " << iterations << " iterations."; + LOG(debug) << "Energy loss converged after " << iterations << " iterations."; fScalingFactor = 1; // Reset scaling factor after convergence } From 2b070b938e4cdcc3c2df7d129e8087dbf3294a4e Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Mon, 28 Jul 2025 23:49:15 +0200 Subject: [PATCH 50/75] Fix bug in straggling. Fix issue with stragling being calculated over last timestep rather then the whole propagation from previous point to next measurement POCA. Add additional functions to UKF to disable straggling. Add functions to pull intermediate results from UKF. Add some additional CATIMA straggling functions (pull from calculation, not just range tables that are pre-calculated). --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 44 +++++++++++ .../OpenKF/kalman_filter/TrackFitterUKF.h | 22 +++++- AtTools/AtELossCATIMA.cxx | 21 ++++++ AtTools/AtELossCATIMA.h | 1 + AtTools/AtELossCATIMATest.cxx | 17 +++++ CMakeLists.txt | 7 +- macro/tests/UKF/UKFSingleTrack.C | 73 +++++++++++++++++-- 7 files changed, 174 insertions(+), 11 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index 8c3824eca..1c8012494 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -28,6 +28,33 @@ void TrackFitterUKF::SetInitialState(const ROOT::Math::XYZPoint &initialPosition } } +TMatrixD TrackFitterUKF::GetStateCovariance() const +{ + TMatrixD cov(m_matP.rows(), m_matP.cols()); + for (int i = 0; i < m_matP.rows(); ++i) { + for (int j = 0; j < m_matP.cols(); ++j) { + cov(i, j) = m_matP(i, j); // Copy covariance matrix to TMatrixD + } + } + return cov; +} + +std::array TrackFitterUKF::GetAugStateVector() const +{ + return {m_vecXa[0], m_vecXa[1], m_vecXa[2], m_vecXa[3], + m_vecXa[4], m_vecXa[5], m_vecXa[6]}; // Return the augmented state vector as an array +} +TMatrixD TrackFitterUKF::GetAugStateCovariance() const +{ + TMatrixD cov(m_matPa.rows(), m_matPa.cols()); + for (int i = 0; i < m_matPa.rows(); ++i) { + for (int j = 0; j < m_matPa.cols(); ++j) { + cov(i, j) = m_matPa(i, j); // Copy augmented covariance matrix to TMatrixD + } + } + return cov; +} + void TrackFitterUKF::SetMeasCov(const TMatrixD &measCov) { if (measCov.GetNrows() != TF_DIM_Z || measCov.GetNcols() != TF_DIM_Z) { @@ -58,10 +85,27 @@ Matrix TrackFitterUKF::calcu double eIn = AtTools::Kinematics::KE(fMeanStep.fLastMom, fMeanStep.fMass); double eOut = AtTools::Kinematics::KE(fMeanStep.fMom, fMeanStep.fMass); + if (!fEnableEnStraggling) { + // If energy straggling is disabled, we set the process noise to zero. + matQ(0, 0) = 0.0; + return matQ; + } + if (const auto *elossModel = fPropagator.GetELossModel()) { double dedx_straggle = elossModel->GetdEdxStraggling(eIn, eOut); double factor = dedx_straggle / elossModel->GetdEdx(eIn); + if (factor > fMaxStragglingFactor) { + LOG(warn) << "Process noise factor for energy straggling is greater than " << fMaxStragglingFactor + << ". To maintain stability, we will " + "use a factor of " + << fMaxStragglingFactor << "."; + factor = fMaxStragglingFactor; + } matQ(0, 0) = factor * factor; // Variance for the dedx straggling. + LOG(info) << "Calculating process noise for straggling between " << eIn << " MeV and " << eOut << " MeV over " + << elossModel->GetRange(eIn, eOut) << " mm."; + LOG(info) << "Process noise covariance for energy straggling: " << matQ(0, 0) << " (factor: " << factor + << ", dedx_straggle: " << dedx_straggle << ", dEdx: " << elossModel->GetdEdx(eIn) << ")"; } else { throw std::runtime_error("Cannot calculate process noise covariance without an energy loss model"); diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index 7714bc4dc..1e38f505e 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -403,6 +403,9 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { AtTools::AtPropagator::StepState fMeanStep; /// Holds the step information for POCA propagation of mean state ROOT::Math::Plane3D fMeasurementPlane; ///< Holds the measurement plane for the track fitter public: + bool fEnableEnStraggling{true}; ///< @brief Flag to enable/disable energy straggling + double fMaxStragglingFactor{1. / 3.}; ///< @brief Maximum straggling factor for energy loss + /** * @brief Constructor for the TrackFitterUKF class. * @param propagator The propagator to be used for the track fitting, must be passed as an rvalue reference. @@ -426,6 +429,9 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { { return {m_vecX[0], m_vecX[1], m_vecX[2], m_vecX[3], m_vecX[4], m_vecX[5]}; } + TMatrixD GetStateCovariance() const; + TMatrixD GetAugStateCovariance() const; + std::array GetAugStateVector() const; kf::Vector funcF(const kf::Vector &x, const kf::Vector &v, const kf::Vector &z); @@ -433,13 +439,25 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { void predictUKF(const ROOT::Math::XYZPoint &z) { + using namespace ROOT::Math; // First we need to propagate the mean state vector to the next measurement point. + XYZPoint startingPosition{m_vecX[0], m_vecX[1], m_vecX[2]}; // Get the starting position from the state vector + Polar3DVector startingMomentum{m_vecX[3], m_vecX[4], + m_vecX[5]}; // Get the starting momentum from the state vector + + LOG(info) << "Propagating reference state from position: " << startingPosition + << " with momentum: " << XYZVector(startingMomentum); + + fPropagator.SetState(startingPosition, XYZVector(startingMomentum)); fPropagator.PropagateToMeasurementSurface(AtTools::AtMeasurementPoint(z), *fStepper); - fMeanStep = fPropagator.GetState(); // Get the mean step information from the propagator + fMeanStep = fPropagator.GetState(); // Get the mean step information from the propagator + fMeanStep.fLastPos = startingPosition; // Store the last position + fMeanStep.fLastMom = startingMomentum; // Store the last momentum + + LOG(info) << "Propagated to position: " << fMeanStep.fPos << " with momentum: " << fMeanStep.fMom; // Now we can construct the reference plane. - using namespace ROOT::Math; fMeasurementPlane = Plane3D(fMeanStep.fMom.Unit(), XYZPoint(z)); // Create a plane using the momentum direction and position Vector zVec; // Initialize the measurement vector diff --git a/AtTools/AtELossCATIMA.cxx b/AtTools/AtELossCATIMA.cxx index 3f85a981d..2518d8402 100644 --- a/AtTools/AtELossCATIMA.cxx +++ b/AtTools/AtELossCATIMA.cxx @@ -129,6 +129,27 @@ double AtELossCATIMA::GetdEdxStraggling(double energyIni, double energyFin) cons auto factor = dE_st / (energyIni - energyFin); return factor * dedx_min; } +double AtELossCATIMA::GetdEdxStragglingCATIMA(double energy, double intDistance) const +{ + if (fProjectile == nullptr || fMaterial == nullptr) { + LOG(error) << "Projectile or material not set. dEdx straggling is 0."; + return 0; + } + catima::Result result = catima::calculate(*fProjectile, *fMaterial, energy / fProjectileMassAmu); + double dEdxi = result.dEdxi; // MeV/(g/cm^2) + + intDistance *= 10 * fDensity; // Convert to g/cm^2. + + auto oldT = fProjectile->T; + fProjectile->T = energy / fProjectileMassAmu; // Set the projectile's + auto dE_st = catima::domega2dx(*fProjectile, *fMaterial) / dEdxi; // domega in MeV/(g/cm^2) + fProjectile->T = oldT; // Restore the projectile's T + + dE_st /= intDistance; // Get the variance in stopping power (MeV/(g/cm^2)) + dE_st *= fDensity * fDensity; // Convert to MeV^2/cm^2 + dE_st *= 100; // Convert to MeV^2/mm^2 + return std::sqrt(dE_st); // Returns the factor for dEdx straggling +} std::vector> AtELossCATIMA::GetBraggCurve(double energy, double rangeStepSize, double totalFractionELoss) const diff --git a/AtTools/AtELossCATIMA.h b/AtTools/AtELossCATIMA.h index bf63862e7..33f4c61c7 100644 --- a/AtTools/AtELossCATIMA.h +++ b/AtTools/AtELossCATIMA.h @@ -45,6 +45,7 @@ class AtELossCATIMA : public AtELossModel { virtual double GetRangeVariance(double energy) const override; virtual double GetElossStraggling(double energyIni, double energyFin) const override; virtual double GetdEdxStraggling(double energyIni, double energyFin) const override; + double GetdEdxStragglingCATIMA(double energy, double distance) const; virtual std::vector> GetBraggCurve(double energy, double rangeStepSize = 0, double totalFractionELoss = 0.001) const override; diff --git a/AtTools/AtELossCATIMATest.cxx b/AtTools/AtELossCATIMATest.cxx index 9a438ae60..d7d89dfae 100644 --- a/AtTools/AtELossCATIMATest.cxx +++ b/AtTools/AtELossCATIMATest.cxx @@ -70,6 +70,7 @@ TEST_F(AtELossCATIMATestFixture, TestEnergyLossStragglingDistance) { double expectedSigma = 0.0084 * mass; // Expected sigma from LISE double eloss_straggling = model.GetElossStragglingDistance(1.0, 50.0); // 1 MeV over 10 mm + LOG(info) << "Straggling over 5 cm: " << eloss_straggling; ASSERT_NEAR(eloss_straggling, expectedSigma, 0.1 * expectedSigma); expectedSigma = 0.0376 * mass; @@ -94,6 +95,22 @@ TEST_F(AtELossCATIMATestFixture, TestdEdxStraggling) ASSERT_NEAR(e_min, e_min_dedx, 0.01 * e_min); // Check minimum energy loss } +TEST_F(AtELossCATIMATestFixture, TestdEdxStragglingCalc) +{ + double intDistance = 10; // 1 mm integration distance. + auto Eout = model.GetEnergy(5.0, intDistance); // Initialize the model with a known energy + double dedx_straggling = model.GetdEdxStraggling(5.0, Eout); // Get over small range + double dedx_st_calc = model.GetdEdxStragglingCATIMA(5.0, intDistance); + + LOG(info) << "Energy after " << intDistance << " mm: " << Eout << " MeV (" << Eout / mass << " MeV/u)"; + LOG(info) << "Energy loss: " << 5.0 - Eout << " MeV"; + LOG(info) << "Straggling factor: " << dedx_straggling * intDistance / (5.0 - Eout); + LOG(info) << "Straggling factor: " << dedx_straggling / model.GetdEdx(5.0); + LOG(info) << "Energy straggling calc: " << dedx_st_calc * intDistance << " MeV"; + LOG(info) << "Energy straggling range: " << dedx_straggling * intDistance << " MeV"; + ASSERT_NEAR(dedx_straggling, dedx_st_calc, 1e-4); // Check dEdx straggling +} + TEST(AtELossCATIMATest, LISE_Match) { // Create the vector with the gas components for H2. diff --git a/CMakeLists.txt b/CMakeLists.txt index 55a74e19e..1ae91b0e3 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,8 +138,11 @@ if(NOT CATIMA_FOUND) set(APPS OFF CACHE BOOL "Disable CATIMA applications" FORCE) set(GENERATE_DATA OFF CACHE BOOL "Disable CATIMA data generator" FORCE) - FetchContent_MakeAvailable(catima) - +FetchContent_GetProperties(catima) + if (NOT catima_POPULATED) + FetchContent_Populate(catima) + add_subdirectory(${catima_SOURCE_DIR} ${catima_BINARY_DIR}) + endif() # Suppress shadow warnings for CATIMA target if(TARGET catima) get_target_property(catima_compile_options catima COMPILE_OPTIONS) diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index 71d37742e..a6a82cd5a 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -12,7 +12,7 @@ const double charge_p = 1.602176634e-19; // Charge of proton // Simulated (measurement) hits std::vector x, y, z, Eloss; - +int pointsToCluster = 5; void LoadHits() { std::ifstream infile("hits.txt"); @@ -22,15 +22,15 @@ void LoadHits() // Save first point. infile >> xi >> yi >> zi >> Ei; - eLoss = Ei * 1e3; // Initialize energy loss + eLoss = Ei; // Initialize energy loss x.push_back(xi * 10); // Convert to mm y.push_back(yi * 10); // Convert to mm z.push_back(zi * 10); // Convert to mm while (infile >> xi >> yi >> zi >> Ei) { - Ei *= 1e3; // Convert to MeV + // Ei *= 1e3; // Convert to MeV - if (i++ % 5 != 0) { + if (++i % pointsToCluster != 0) { eLoss += Ei; continue; // Skip every 5th point } @@ -60,13 +60,20 @@ void UKFSingleTrack() std::cout << " Creating the UKF class" << std::endl; using namespace AtTools; - std::vector x2, y2, z2, Eloss2; + std::vector x2, y2, z2, Eloss2, p2, sigmap2, lambda2, sigmalambda2, residual; // Setup the Propagator for UKF auto elossModel = std::make_unique(0); elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path elossModel->SetDensity(3.553e-5); // Set density in g/cm^3 for 300 torr H2 - AtTools::AtPropagator propagator(charge_p, mass_p, std::move(elossModel)); + + auto elossModel2 = std::make_unique(3.553e-5); + elossModel2->SetProjectile(1, 1, 1); + std::vector> mat; + mat.push_back({1, 1, 1}); + elossModel2->SetMaterial(mat); + + AtTools::AtPropagator propagator(charge_p, mass_p, std::move(elossModel2)); propagator.SetEField({0, 0, 0}); // No electric field propagator.SetBField({0, 0, 2.85}); // Magnetic field @@ -89,6 +96,7 @@ void UKFSingleTrack() double sigma_mom = 0.01 * startMom.R(); // Momentum uncertainty of 10% MeV/c double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree + ukf.fEnableEnStraggling = true; // Enable energy straggling TMatrixD cov(6, 6); cov.Zero(); @@ -114,6 +122,9 @@ void UKFSingleTrack() x2.push_back(startPos.X()); y2.push_back(startPos.Y()); z2.push_back(startPos.Z()); + p2.push_back(startMom.R()); + sigmap2.push_back(sigma_mom); + residual.push_back(0); // Initial residual is zero ROOT::Math::XYZVector lastMom = ROOT::Math::XYZVector(startMom.X(), startMom.Y(), startMom.Z()); @@ -125,11 +136,15 @@ void UKFSingleTrack() ukf.SetMeasCov(cov_meas); // Set measurement noise covariance ukf.predictUKF(point); + auto augState = ukf.GetAugStateVector(); + auto augCov = ukf.GetAugStateCovariance(); std::cout << std::endl << "Prediction step complete." << std::endl; ukf.correctUKF(point); std::cout << std::endl << "Correction step complete." << std::endl; auto state = ukf.GetStateVector(); + auto cov = ukf.GetStateCovariance(); + ROOT::Math::XYZPoint pos(state[0], state[1], state[2]); ROOT::Math::Polar3DVector momPolar(state[3], state[4], state[5]); ROOT::Math::XYZVector mom(momPolar); @@ -143,10 +158,17 @@ void UKFSingleTrack() auto KE_out = Kinematics::KE(mom, mass_p); lastMom = mom; + double residualValue = (point - pos).R(); + x2.push_back(pos.X()); y2.push_back(pos.Y()); z2.push_back(pos.Z()); Eloss2.push_back((KE_in - KE_out)); + p2.push_back(mom.R()); + sigmap2.push_back(std::sqrt(cov(3, 3))); // Propagate momentum uncertainty + lambda2.push_back(augState[6]); // Energy straggling factor + sigmalambda2.push_back(std::sqrt(augCov(6, 6))); // Propagate energy straggling uncertainty + residual.push_back(residualValue); // Store the residual for this hit } TGraph2D *track = new TGraph2D(x.size(), x.data(), y.data(), z.data()); @@ -179,7 +201,44 @@ void UKFSingleTrack() eloss2Graph->SetMarkerStyle(21); eloss2Graph->SetMarkerColor(kRed); + TGraphErrors *pGraph = new TGraphErrors(p2.size()); + for (size_t i = 0; i < p2.size(); ++i) { + pGraph->SetPoint(i, i, p2[i] * 1e-3 * pointsToCluster / 5.); + pGraph->SetPointError(i, 0, + sigmap2[i] * 5 * 1e-3 * pointsToCluster / + 5.); // Error bars from sigmap2, converted to GeV/c + } + pGraph->SetTitle("Momentum per Hit;Hit Number;Momentum [MeV/c]"); + pGraph->SetMarkerStyle(20); + pGraph->SetMarkerColor(kBlue); + + TGraphErrors *lambdaGraph = new TGraphErrors(lambda2.size()); + for (size_t i = 0; i < lambda2.size(); ++i) { + lambdaGraph->SetPoint(i, i, lambda2[i] * 0.03 * pointsToCluster / 5.); + lambdaGraph->SetPointError(i, 0, sigmalambda2[i] * 0.03 * pointsToCluster / 5.); + std::cout << "Lambda: " << lambda2[i] << ", Error: " << sigmalambda2[i] << std::endl; + } + lambdaGraph->SetTitle("Lambda per Hit (scaled);Hit Number;Lambda [scaled]"); + lambdaGraph->SetMarkerStyle(22); + lambdaGraph->SetMarkerColor(kGreen + 2); + + TGraph *residualGraph = new TGraph(residual.size()); + for (size_t i = 0; i < residual.size(); ++i) { + residualGraph->SetPoint(i, i, residual[i] * .1); + } + residualGraph->SetTitle("Residual per Hit;Hit Number;Residual [mm]"); + residualGraph->SetMarkerStyle(23); + residualGraph->SetMarkerColor(kMagenta); + TCanvas *c2 = new TCanvas("c2", "Energy Loss per Hit", 800, 600); elossGraph->Draw("AP"); - // eloss2Graph->Draw("PSAME"); + eloss2Graph->Draw("PSAME"); + pGraph->Draw("PSAME"); + // lambdaGraph->Draw("PSAME"); + residualGraph->Draw("PSAME"); + + double sumEloss = std::accumulate(Eloss.begin(), Eloss.end(), 0.0); + double sumEloss2 = std::accumulate(Eloss2.begin(), Eloss2.end(), 0.0); + std::cout << "Sum of Eloss: " << sumEloss << std::endl; + std::cout << "Sum of Eloss2: " << sumEloss2 << std::endl; } \ No newline at end of file From 796d89365b8e42faf78e181a34748f14b82ab740 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 29 Jul 2025 00:33:41 +0200 Subject: [PATCH 51/75] Cleanup UKF doc and headers --- .../OpenKF/kalman_filter/TrackFitterUKF.h | 305 +++++++++--------- .../OpenKF/kalman_filter/kalman_filter.h | 13 +- 2 files changed, 156 insertions(+), 162 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index 1e38f505e..f525661ec 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -26,49 +26,54 @@ #include "unscented_kalman_filter.h" namespace kf { -/// @brief Class for fitting tracks using the Unscented Kalman Filter (UKF) algorithm. -/// @tparam DIM_X Dimension of the state vector. -/// @tparam DIM_Z Dimension of the measurement vector. -/// @tparam DIM_V Dimension of the process noise vector. -/// @tparam DIM_N Dimension of the measurement noise vector. +/** @brief Class for fitting tracks using the Unscented Kalman Filter (UKF) algorithm. + * + * Serves as a templated base class for UKF calculations. It hold no physics information, just + * the machinery that underlies the UKF formalism. It is a modified version of the UKF provided + * by OpenKF, that has been expanded to allow for more hooks into the method. + * + * Templated because I believe Eigen can do quite a bit of operation for small matrices like we have here if + * the size is known at compile time. Worth checking that though. + * + * @tparam DIM_X Dimension of the state vector. + * @tparam DIM_Z Dimension of the measurement vector. + * @tparam DIM_V Dimension of the process noise vector. + * @tparam DIM_N Dimension of the measurement noise vector. + */ template -class TrackFitterUKFBase : public KalmanFilter { - +class TrackFitterUKFBase { public: // Augmented state vector is just the process noise and state vector. The measurement noise is not included as that // is independent of the propagation and measurement model and just adds linearly. static constexpr int32_t DIM_A{DIM_X + DIM_V}; ///< @brief Augmented state dimension static constexpr int32_t SIGMA_DIM_A{2 * DIM_A + 1}; ///< @brief Sigma points dimension for augmented state + Matrix m_matSigmaXa{Matrix::Zero()}; ///< @brief Sigma points matrix protected: - using KalmanFilter::m_vecX; // from Base KalmanFilter class - using KalmanFilter::m_matP; // from Base KalmanFilter class - - float32_t m_weight0; /// @brief unscented transform weight 0 for mean - float32_t m_weighti; /// @brief unscented transform weight i for none mean samples - float32_t m_kappa{0}; ///< @brief Kappa parameter for finding sigma points - - Vector m_vecXa{Vector::Zero()}; /// @brief augmented state vector (incl. process - /// and measurement noise means) - Matrix m_matPa{Matrix::Zero()}; /// @brief augmented state covariance (incl. - /// process and measurement noise covariances) - - // Add variables to track the covariances of the process and measurement noise. - Matrix m_matQ; // @brief Process noise covariance matrix - Matrix m_matR; // @brief Measurement noise covariance matrix + Vector m_vecX{Vector::Zero()}; /// @brief estimated state vector + Matrix m_matP{Matrix::Zero()}; /// @brief state covariance matrix + + /// @brief Augmented state vector (incl. process and measurement noise means) + Vector m_vecXa{Vector::Zero()}; + /// @brief augmented state covariance (incl. process and measurement noise covariances) + Matrix m_matPa{Matrix::Zero()}; + + // Process and measurement noise covariance matrices + /// Process noise covariance matrix (Q) + Matrix m_matQ{Matrix::Zero()}; + /// Measurement noise covariance matrix (R) + Matrix m_matR{Matrix::Zero()}; + + /// Unscented transform weight for the mean sigma point + float32_t m_weight0; + /// Unscented transform weight for the other sigma points + float32_t m_weighti; + /// Kappa parameter for sigma point calculation + float32_t m_kappa{3 - DIM_A}; public: - Matrix m_matSigmaXa{Matrix::Zero()}; ///< @brief Sigma points matrix - - TrackFitterUKFBase() - : KalmanFilter(), m_kappa(3 - DIM_A), m_matQ(Matrix::Zero()), - m_matR(Matrix::Zero()) - { - // 1. calculate weights - updateWeights(); - } - - ~TrackFitterUKFBase() {} + TrackFitterUKFBase() { updateWeights(); } + ~TrackFitterUKFBase() = default; void setKappa(float32_t kappa) { @@ -76,96 +81,43 @@ class TrackFitterUKFBase : public KalmanFilter { updateWeights(); // Update the weights based on the new kappa value } - /// - /// @brief adding process noise covariance Q to the augmented state covariance - /// matPa in the middle element of the diagonal. - /// - void setCovarianceQ(const Matrix &matQ) - { - m_matQ = matQ; // Store the process noise covariance matrix - } - - /// - /// @brief set the measurement noise covariance R to be used in the update step - /// - void setCovarianceR(const Matrix &matR) - { - m_matR = matR; // Store the measurement noise covariance matrix - } - - /// Add state vector (m_vecX) to the augment state vector (m_vecXa) and also - /// add covariance matrix (m_matP) to the augment covariance (m_matPa). - void updateAugWithState() - { - // Copy state vector to augmented state vector - for (int32_t i{0}; i < DIM_X; ++i) { - m_vecXa[i] = m_vecX[i]; - } - - // Copy state covariance matrix to augmented covariance matrix - for (int32_t i{0}; i < DIM_X; ++i) { - for (int32_t j{0}; j < DIM_X; ++j) { - m_matPa(i, j) = m_matP(i, j); - } - } - } - - virtual std::array calculateProcessNoiseMean() - { - // Calculate the expectation value of the process noise using the current value of the state vector m_vecX - std::array processNoiseMean{0}; - - // TODO: Set the mean energy loss based on the momentum and particle type. Probably best to track stopping power? - return processNoiseMean; - } - - virtual Matrix calculateProcessNoiseCovariance() - { - // Calculate the process noise covariance matrix - Matrix matQ{Matrix::Zero()}; - matQ = m_matQ; // Use the stored process noise covariance matrix - - // TODO: Set the process noise covariance for angular straggle and energy loss. - return matQ; - } - - void updateAugWithProcessNoise() - { - auto processNoiseMean = calculateProcessNoiseMean(); - m_matQ = calculateProcessNoiseCovariance(); - - // Add the mean process noise to the augmented state vector - for (int32_t i{0}; i < DIM_V; ++i) { - m_vecXa[DIM_X + i] = processNoiseMean[i]; - } + /** + * @brief Set process noise covariance Q to be used in the prediction step. + */ + void setCovarianceQ(const Matrix &matQ) { m_matQ = matQ; } + /** + * @brief Set the measurement noise covariance R to be used in the update step. + */ + void setCovarianceR(const Matrix &matR) { m_matR = matR; } + virtual Vector &vecX() { return m_vecX; } + virtual const Vector &vecX() const { return m_vecX; } - // Add process noise covariance to the augmented covariance matrix - const int32_t S_IDX{DIM_X}; - const int32_t L_IDX{S_IDX + DIM_V}; + virtual Matrix &matP() { return m_matP; } + virtual const Matrix &matP() const { return m_matP; } - for (int32_t i{S_IDX}; i < L_IDX; ++i) { - for (int32_t j{S_IDX}; j < L_IDX; ++j) { - m_matPa(i, j) = m_matQ(i - S_IDX, j - S_IDX); - } - } - } - - /// - /// @brief update the augmented state vector and covariance matrix - /// This functions fully updates the augmented state vector (m_vecXa) and covariance matrix (m_matPa) - /// by setting both the state vector and process noise components. - /// + /** + * @brief update the augmented state vector and covariance matrix + * + * This function fully updates the augmented state vector and covariance matrix using + * the state and process noise. + */ void updateAugmentedStateAndCovariance() { updateAugWithState(); updateAugWithProcessNoise(); } - /// - /// @brief state prediction step of the unscented Kalman filter (UKF). - /// @param predictionModelFunc callback to the prediction/process model - /// function - /// + /** + * @brief state prediction step of the unscented Kalman filter (UKF). + * @param predictionModelFunc callback to the prediction/process model function. + * @param vecZ actual measurement vector.` + * + * A template is used here for performace reasons since there is only really a single prediction + * model used. Honestly it probably does not make a difference compared to an std::function, but + * this was not changed from OpenKF. + * + * This modifies the state vector and state covariance. + */ template void predictUKF(PredictionModelCallback predictionModelFunc, const Vector &vecZ) { @@ -199,11 +151,11 @@ class TrackFitterUKFBase : public KalmanFilter { calculateWeightedMeanAndCovariance(sigmaXx, m_vecX, m_matP); } - /// - /// @brief measurement correction step of the unscented Kalman filter (UKF). - /// @param measurementModelFunc callback to the measurement model function - /// @param vecZ actual measurement vector. - /// + /** + * @brief measurement correction step of the unscented Kalman filter (UKF). + * @param measurementModelFunc callback to the measurement model function + * @param vecZ actual measurement vector. + */ template void correctUKF(MeasurementModelCallback measurementModelFunc, const Vector &vecZ) { @@ -235,7 +187,6 @@ class TrackFitterUKFBase : public KalmanFilter { // Add in the measurement noise covariance matrix to the measurement covariance matrix. matPzz += m_matR; // Add measurement noise covariance - // TODO: calculate cross correlation const Matrix matPxz{calculateCrossCorrelation(sigmaXx, m_vecX, sigmaZ, vecZhat)}; // kalman gain @@ -246,9 +197,9 @@ class TrackFitterUKFBase : public KalmanFilter { } protected: - /// - /// @brief algorithm to calculate the weights used to draw the sigma points - /// + /** + * @brief Set the weights used to calculate sigma points. + */ void updateWeights() { static_assert(DIM_A > 0, "DIM_A is Zero which leads to numerical issue."); @@ -258,17 +209,59 @@ class TrackFitterUKFBase : public KalmanFilter { m_weight0 = m_kappa / denoTerm; m_weighti = 0.5F / denoTerm; } + /** + * @brief Add state vector and state covariance matrix to the augmented state vector covariance matrix. + */ + void updateAugWithState() + { + // Copy state vector to augmented state vector + for (int32_t i{0}; i < DIM_X; ++i) { + m_vecXa[i] = m_vecX[i]; + } + + // Copy state covariance matrix to augmented covariance matrix + for (int32_t i{0}; i < DIM_X; ++i) { + for (int32_t j{0}; j < DIM_X; ++j) { + m_matPa(i, j) = m_matP(i, j); + } + } + } + + virtual std::array calculateProcessNoiseMean() { return std::array{0}; } + + virtual Matrix calculateProcessNoiseCovariance() { return m_matQ; } + + void updateAugWithProcessNoise() + { + auto processNoiseMean = calculateProcessNoiseMean(); + m_matQ = calculateProcessNoiseCovariance(); + + // Add the mean process noise to the augmented state vector + for (int32_t i{0}; i < DIM_V; ++i) { + m_vecXa[DIM_X + i] = processNoiseMean[i]; + } + + // Add process noise covariance to the augmented covariance matrix + const int32_t S_IDX{DIM_X}; + const int32_t L_IDX{S_IDX + DIM_V}; + + for (int32_t i{S_IDX}; i < L_IDX; ++i) { + for (int32_t j{S_IDX}; j < L_IDX; ++j) { + m_matPa(i, j) = m_matQ(i - S_IDX, j - S_IDX); + } + } + } - /// - /// @brief algorithm to calculate the deterministic sigma points for - /// the unscented transformation - /// - /// @param vecX mean of the normally distributed state - /// @param matPxx covariance of the normally distributed state - /// @param STATE_DIM dimension of the vector used to calculate the sigma points - /// @param SIGMA_DIM number of sigma points required (default is 2 * STATE_DIM + 1) - /// @return matrix of sigma points where each column is a sigma point - /// + /** + * @brief Algorithm to calculate the deterministic sigma points for + * the unscented transformation. + * + * @param vecX Mean of the normally distributed state. + * @param matPxx Covariance of the normally distributed state. + * @param STATE_DIM Dimension of the vector used to calculate the sigma points. + * @param SIGMA_DIM Number of sigma points required (default is 2 * STATE_DIM + 1). + * @return Matrix of sigma points where each column is a sigma point. + */ template Matrix calculateSigmaPoints(const Vector &vecXa, const Matrix &matPa) @@ -321,14 +314,12 @@ class TrackFitterUKFBase : public KalmanFilter { return sigmaXa; } - /// - /// @brief calculate the weighted mean and covariance given a set of sigma - /// points - /// @param[in] sigmaX matrix of (probably posterior) sigma points where each column contain single - /// sigma point. - /// @param[out] vecX output weighted mean of the sigma points - /// @param[out] matPxx output weighted covariance of the sigma points - /// + /** + * @brief Calculate the weighted mean and covariance given a set of sigma points. + * @param[in] sigmaX Matrix of (probably posterior) sigma points where each column contains a single sigma point. + * @param[out] vecX Output weighted mean of the sigma points. + * @param[out] matPxx Output weighted covariance of the sigma points. + */ template void calculateWeightedMeanAndCovariance(const Matrix &sigmaX, Vector &vecX, Matrix &matPxx) @@ -356,17 +347,17 @@ class TrackFitterUKFBase : public KalmanFilter { } } - /// - /// @brief calculate the cross-correlation given two sets sigma points X and Y - /// and their means x and y - /// @param sigmaX first matrix of sigma points where each column contain - /// single sigma point - /// @param vecX mean of the first set of sigma points - /// @param sigmaY second matrix of sigma points where each column contain - /// single sigma point - /// @param vecY mean of the second set of sigma points - /// @return matPxy, the cross-correlation matrix - /// + /** + * @brief calculate the cross-correlation given two sets sigma points X and Y + * and their means x and y + * @param sigmaX first matrix of sigma points where each column contain + * single sigma point + * @param vecX mean of the first set of sigma points + * @param sigmaY second matrix of sigma points where each column contain + * single sigma point + * @param vecY mean of the second set of sigma points + * @return matPxy, the cross-correlation matrix + */ template Matrix calculateCrossCorrelation(const Matrix &sigmaX, const Vector &vecX, const Matrix &sigmaY, const Vector &vecY) @@ -389,8 +380,11 @@ class TrackFitterUKFBase : public KalmanFilter { } }; -// Define template dimension variables for clarity and reuse - +/** + * @brief Class for fitting tracks using the Unscented Kalman Filter (UKF) algorithm. + * + * UKF specialized for fitting tracks. This class is where all of the physics is + */ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { protected: static constexpr int32_t TF_DIM_X = 6; @@ -402,6 +396,7 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { std::unique_ptr fStepper{nullptr}; AtTools::AtPropagator::StepState fMeanStep; /// Holds the step information for POCA propagation of mean state ROOT::Math::Plane3D fMeasurementPlane; ///< Holds the measurement plane for the track fitter + public: bool fEnableEnStraggling{true}; ///< @brief Flag to enable/disable energy straggling double fMaxStragglingFactor{1. / 3.}; ///< @brief Maximum straggling factor for energy loss diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/kalman_filter.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/kalman_filter.h index 90651eb99..66694183e 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/kalman_filter.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/kalman_filter.h @@ -18,10 +18,13 @@ namespace kf { template class KalmanFilter { -public: - KalmanFilter() {} - ~KalmanFilter() {} +protected: + Vector m_vecX{Vector::Zero()}; /// @brief estimated state vector + Matrix m_matP{Matrix::Zero()}; /// @brief state covariance matrix +public: + KalmanFilter() = default; + ~KalmanFilter() = default; virtual Vector &vecX() { return m_vecX; } virtual const Vector &vecX() const { return m_vecX; } @@ -88,10 +91,6 @@ class KalmanFilter { m_vecX = m_vecX + matKk * (vecZ - measurementModelFunc(m_vecX)); m_matP = (matI - matKk * matJcobH) * m_matP; } - -protected: - Vector m_vecX{Vector::Zero()}; /// @brief estimated state vector - Matrix m_matP{Matrix::Zero()}; /// @brief state covariance matrix }; } // namespace kf From bd62e903f7bc3de98047de2d44b927fb5aebb335 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 29 Jul 2025 02:02:33 +0200 Subject: [PATCH 52/75] Save the information needed for smoothing --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 91 +++++++++++++++++++ .../OpenKF/kalman_filter/TrackFitterUKF.h | 84 ++++++----------- 2 files changed, 120 insertions(+), 55 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index 1c8012494..407a09edb 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -8,10 +8,31 @@ #include namespace kf { +void TrackFitterUKF::Reset() +{ + // Reset the state vector and covariance matrix + m_vecX.setZero(); + m_matP.setZero(); + m_vecXa.setZero(); + m_matPa.setZero(); + m_matQ.setZero(); + m_matR.setZero(); + m_matSigmaXa.setZero(); + + // Clear the history vectors + m_vecXPredHist.clear(); + m_matPPredHist.clear(); + m_matCPredHist.clear(); + m_vecXHist.clear(); + m_matPHist.clear(); + fMeanStep = AtTools::AtPropagator::StepState(); // Reset the step state +} void TrackFitterUKF::SetInitialState(const ROOT::Math::XYZPoint &initialPosition, const ROOT::Math::XYZVector &initialMomentum, const TMatrixD &initialCovariance) { + // If we are setting the initial state, then we should clear the history. + Reset(); fPropagator.SetState(initialPosition, initialMomentum); // Set the initial state in the propagator m_vecX[0] = initialPosition.X(); // X position m_vecX[1] = initialPosition.Y(); // Y position @@ -26,6 +47,19 @@ void TrackFitterUKF::SetInitialState(const ROOT::Math::XYZPoint &initialPosition m_matP(i, j) = initialCovariance(i, j); } } + + // Save the initial state in our history vectors + m_vecXHist.push_back(m_vecX); + m_matPHist.push_back(m_matP); + m_vecXPredHist.push_back(m_vecX); + m_matPPredHist.push_back(m_matP); + m_matCPredHist.push_back(Matrix::Zero()); // Cross-correlation is not defined for the first point + + // We need to calculate the sigma points for the initial state + updateAugmentedStateAndCovariance(); // Update the augmented state vector and covariance matrix + m_matSigmaXa = calculateSigmaPoints(m_vecXa, m_matPa); // Calculate the sigma points for the initial state + // Now we grab the sigma points only for the state. + m_matSigmaXPred = m_matSigmaXa.block(0, 0, TF_DIM_X, SIGMA_DIM_A); // Extract the state sigma points } TMatrixD TrackFitterUKF::GetStateCovariance() const @@ -156,4 +190,61 @@ Vector TrackFitterUKF::funcH(const Vector zVec; // Initialize the measurement vector + zVec[0] = z.X(); + zVec[1] = z.Y(); + zVec[2] = z.Z(); + auto callback = [this](const kf::Vector &x_, const kf::Vector &v_, + const kf::Vector &z_) { return funcF(x_, v_, z_); }; + TrackFitterUKFBase::predictUKF(callback, zVec); + + // Now we need to store the predicted state and covariance for smoothing later. + m_vecXPredHist.push_back(m_vecX); // Store the predicted state vector + m_matPPredHist.push_back(m_matP); // Store the predicted covariance matrix + + // Get the sigma points belonging to the predicted state + Matrix sigmaXx{m_matSigmaXa.block(0, 0, TF_DIM_X, SIGMA_DIM_A)}; + + // Calculate the cross-corelation between the filtered state at k and predicted state at k+1 + auto matCPred = + calculateCrossCorrelation(m_matSigmaXPred, m_vecXHist.back(), sigmaXx, m_vecXPredHist.back()); + m_matCPredHist.push_back(matCPred); // Store the cross-correlation matrix +} + +void TrackFitterUKF::correctUKF(const ROOT::Math::XYZPoint &z) +{ + Vector zVec; // Initialize the measurement vector + zVec[0] = z.X(); + zVec[1] = z.Y(); + zVec[2] = z.Z(); + auto callback = [this](const kf::Vector &x_) { return funcH(x_); }; + TrackFitterUKFBase::correctUKF(callback, zVec); + + // After correction we need to save the filtered state + m_vecXHist.push_back(m_vecX); // Store the filtered state vector + m_matPHist.push_back(m_matP); // Store the filtered covariance matrix +} + } // namespace kf \ No newline at end of file diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index f525661ec..3a6237688 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -358,19 +358,20 @@ class TrackFitterUKFBase { * @param vecY mean of the second set of sigma points * @return matPxy, the cross-correlation matrix */ - template - Matrix calculateCrossCorrelation(const Matrix &sigmaX, const Vector &vecX, - const Matrix &sigmaY, const Vector &vecY) + template + Matrix + calculateCrossCorrelation(const Matrix &sigmaX, const Vector &vecX, + const Matrix &sigmaY, const Vector &vecY) { - Vector devXi{util::getColumnAt(0, sigmaX) - vecX}; // X[:, 0] - \bar{ x } - Vector devYi{util::getColumnAt(0, sigmaY) - vecY}; // Y[:, 0] - \bar{ y } + Vector devXi{util::getColumnAt(0, sigmaX) - vecX}; // X[:, 0] - \bar{ x } + Vector devYi{util::getColumnAt(0, sigmaY) - vecY}; // Y[:, 0] - \bar{ y } // P_0 = W[0, 0] (X[:, 0] - \bar{x}) (Y[:, 0] - \bar{y})^T - Matrix matPxy{m_weight0 * (devXi * devYi.transpose())}; + Matrix matPxy{m_weight0 * (devXi * devYi.transpose())}; for (int32_t i{1}; i < SIGMA_DIM; ++i) { - devXi = util::getColumnAt(i, sigmaX) - vecX; // X[:, i] - \bar{x} - devYi = util::getColumnAt(i, sigmaY) - vecY; // Y[:, i] - \bar{y} + devXi = util::getColumnAt(i, sigmaX) - vecX; // X[:, i] - \bar{x} + devYi = util::getColumnAt(i, sigmaY) - vecY; // Y[:, i] - \bar{y} matPxy += m_weighti * (devXi * devYi.transpose()); // y += W[0, i] (Y[:, i] - // \bar{y}) (Y[:, i] - \bar{y})^T @@ -397,6 +398,19 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { AtTools::AtPropagator::StepState fMeanStep; /// Holds the step information for POCA propagation of mean state ROOT::Math::Plane3D fMeasurementPlane; ///< Holds the measurement plane for the track fitter + // vectors to hold the information needed for smoothing the UKF + std::vector> m_vecXPredHist; /// @brief History of predicted state vectors at k+1 + std::vector> m_matPPredHist; /// @brief History of predicted state covariances at k+1 + /// History of cross correlation between filtered state at k and predicted at k+1 + std::vector> m_matCPredHist; + /// History of filtered (after correction) state vectors at k + std::vector> m_vecXHist; + /// History of filtered (after correction) state covariances at k + std::vector> m_matPHist; + + /// The sigma points after propagation for the last prediction step. + Matrix m_matSigmaXPred{Matrix::Zero()}; + public: bool fEnableEnStraggling{true}; ///< @brief Flag to enable/disable energy straggling double fMaxStragglingFactor{1. / 3.}; ///< @brief Maximum straggling factor for energy loss @@ -415,7 +429,7 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { : TrackFitterUKFBase(), fPropagator(std::move(propagator)), fStepper(std::move(stepper)) { } - + void Reset(); void SetInitialState(const ROOT::Math::XYZPoint &initialPosition, const ROOT::Math::XYZVector &initialMomentum, const TMatrixD &initialCovariance); @@ -428,56 +442,16 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { TMatrixD GetAugStateCovariance() const; std::array GetAugStateVector() const; - kf::Vector - funcF(const kf::Vector &x, const kf::Vector &v, const kf::Vector &z); - kf::Vector funcH(const kf::Vector &x); - - void predictUKF(const ROOT::Math::XYZPoint &z) - { - using namespace ROOT::Math; - - // First we need to propagate the mean state vector to the next measurement point. - XYZPoint startingPosition{m_vecX[0], m_vecX[1], m_vecX[2]}; // Get the starting position from the state vector - Polar3DVector startingMomentum{m_vecX[3], m_vecX[4], - m_vecX[5]}; // Get the starting momentum from the state vector - - LOG(info) << "Propagating reference state from position: " << startingPosition - << " with momentum: " << XYZVector(startingMomentum); - - fPropagator.SetState(startingPosition, XYZVector(startingMomentum)); - fPropagator.PropagateToMeasurementSurface(AtTools::AtMeasurementPoint(z), *fStepper); - fMeanStep = fPropagator.GetState(); // Get the mean step information from the propagator - fMeanStep.fLastPos = startingPosition; // Store the last position - fMeanStep.fLastMom = startingMomentum; // Store the last momentum - - LOG(info) << "Propagated to position: " << fMeanStep.fPos << " with momentum: " << fMeanStep.fMom; - - // Now we can construct the reference plane. - fMeasurementPlane = Plane3D(fMeanStep.fMom.Unit(), - XYZPoint(z)); // Create a plane using the momentum direction and position - Vector zVec; // Initialize the measurement vector - zVec[0] = z.X(); - zVec[1] = z.Y(); - zVec[2] = z.Z(); - auto callback = [this](const kf::Vector &x_, const kf::Vector &v_, - const kf::Vector &z_) { return funcF(x_, v_, z_); }; - TrackFitterUKFBase::predictUKF(callback, zVec); - } - - void correctUKF(const ROOT::Math::XYZPoint &z) - { - Vector zVec; // Initialize the measurement vector - zVec[0] = z.X(); - zVec[1] = z.Y(); - zVec[2] = z.Z(); - auto callback = [this](const kf::Vector &x_) { return funcH(x_); }; - TrackFitterUKFBase::correctUKF(callback, zVec); - } + void predictUKF(const ROOT::Math::XYZPoint &z); + void correctUKF(const ROOT::Math::XYZPoint &z); protected: std::array calculateProcessNoiseMean() override; - Matrix calculateProcessNoiseCovariance() override; + + kf::Vector + funcF(const kf::Vector &x, const kf::Vector &v, const kf::Vector &z); + kf::Vector funcH(const kf::Vector &x); }; } // namespace kf From 322e738f3bb3ab43170c2f987ec660ebcbec7b7d Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 29 Jul 2025 11:30:44 +0200 Subject: [PATCH 53/75] Refactor LLT decomp into function --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 49 ++++++++++++- .../OpenKF/kalman_filter/TrackFitterUKF.h | 69 ++++++++++++------- macro/tests/UKF/UKFSingleTrack.C | 13 ++++ 3 files changed, 105 insertions(+), 26 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index 407a09edb..36981c980 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -218,6 +218,18 @@ void TrackFitterUKF::predictUKF(const ROOT::Math::XYZPoint &z) zVec[2] = z.Z(); auto callback = [this](const kf::Vector &x_, const kf::Vector &v_, const kf::Vector &z_) { return funcF(x_, v_, z_); }; + + updateAugmentedStateAndCovariance(); + + // Calculate the sigma points for the augmented state vector and save in a matrix where each column is a sigma + // point. + auto sigmaPoints = calculateSigmaPoints(m_vecXa, m_matPa); + + // Pull out the sigma points for the state vector and process noise in two different matrices. + Matrix sigmaXxPrior{ + sigmaPoints.block(0, 0, TF_DIM_X, SIGMA_DIM_A)}; // Sigma points for state vector + m_matSigmaXPred = sigmaXxPrior; // Store the sigma points for the state vector + TrackFitterUKFBase::predictUKF(callback, zVec); // Now we need to store the predicted state and covariance for smoothing later. @@ -228,8 +240,7 @@ void TrackFitterUKF::predictUKF(const ROOT::Math::XYZPoint &z) Matrix sigmaXx{m_matSigmaXa.block(0, 0, TF_DIM_X, SIGMA_DIM_A)}; // Calculate the cross-corelation between the filtered state at k and predicted state at k+1 - auto matCPred = - calculateCrossCorrelation(m_matSigmaXPred, m_vecXHist.back(), sigmaXx, m_vecXPredHist.back()); + auto matCPred = calculateCrossCorrelation(sigmaXxPrior, m_vecXHist.back(), sigmaXx, m_vecXPredHist.back()); m_matCPredHist.push_back(matCPred); // Store the cross-correlation matrix } @@ -247,4 +258,38 @@ void TrackFitterUKF::correctUKF(const ROOT::Math::XYZPoint &z) m_matPHist.push_back(m_matP); // Store the filtered covariance matrix } +void TrackFitterUKF::smoothUKF() +{ + + // Smoothing is done by iterating backwards over the history of predicted states and covariances. + // Here i = k+1 + m_vecXSmooth.resize(m_vecXPredHist.size()); + m_matPSmooth.resize(m_matPPredHist.size()); + m_vecXSmooth.back() = m_vecXHist.back(); // The last smoothed state is the last corrected state + m_matPSmooth.back() = m_matPHist.back(); // The last smoothed covariance is the last corrected covariance + for (size_t i = m_vecXPredHist.size() - 1; i > 0; --i) { + + // Get the predicted state and covariance at step i + const auto &xPred = m_vecXPredHist[i]; // m_{k+1}^- + const auto &pPred = m_matPPredHist[i]; // P_{k+1}^- + const auto &ccor = m_matCPredHist[i]; // C_{k+1} + + // Get the filtered state and covariance at step i-1 + const auto &xFilt = m_vecXHist[i - 1]; // m_{k} + const auto &pFilt = m_matPHist[i - 1]; // P_{k} + + // Get the smoothed state and covariance at step i + auto &xSmooth = m_vecXSmooth[i]; // m^s_{k+1} + auto &pSmooth = m_matPSmooth[i]; // P^s_{k+1} + + auto llt = calculateCholesky(pPred); // Perform Cholesky decomposition + auto D = ccor * pPred.inverse(); // D = C_{k+1} * (P_{k+1}^-)^{-1} + + std::cout << "D matrix at step " << i << ":\n" << D << "\n"; + m_vecXSmooth[i - 1] = xFilt + D * (xSmooth - xPred); // m^s_{k} = m_{k} + D * (m^s_{k+1} - m_{k+1}^-) + m_matPSmooth[i - 1] = + pFilt + D * (pSmooth - pPred) * D.transpose(); // P^s_{k} = P_{k} + D * (P^s_{k+1} - P_{k+1}^-) * D^T + } +} + } // namespace kf \ No newline at end of file diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index 3a6237688..4617e7000 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -252,6 +252,31 @@ class TrackFitterUKFBase { } } + template + Eigen::LLT> calculateCholesky(const Matrix &matP) + { + Eigen::LLT> lltOfP(matP); + if (lltOfP.info() != Eigen::Success) { + LOG(error) << "Cholesky decomposition failed, matrix is not positive definite. Attempting recovery..."; + // Add a small value to the diagonal to regularize the matrix + Matrix matPReg = matP; + matPReg = (matPReg + matPReg.transpose()) * 0.5; // Ensure symmetry + matPReg += Matrix::Identity() * 1e-6; // Regularization value + lltOfP.compute(matPReg); + if (lltOfP.info() != Eigen::Success) { + LOG(error) << "Cholesky decomposition failed even after regularization. Attempting again"; + matPReg += Matrix::Identity() * 1e-3; // Increase regularization value + lltOfP.compute(matPReg); + } + if (lltOfP.info() != Eigen::Success) { + LOG(error) << "\n" << matPReg; + throw std::runtime_error( + "Cholesky decomposition failed, matrix is not positive definite even after regularization."); + } + } + return lltOfP; // Return the Cholesky decomposition of the covariance matrix + } + /** * @brief Algorithm to calculate the deterministic sigma points for * the unscented transformation. @@ -268,25 +293,8 @@ class TrackFitterUKFBase { { const float32_t scalarMultiplier{std::sqrt(STATE_DIM + m_kappa)}; // sqrt(n + \kappa) - Eigen::LLT> lltOfPa(matPa); - if (lltOfPa.info() != Eigen::Success) { - LOG(error) << "Cholesky decomposition failed, matrix is not positive definite. Attempting recovery..."; - // Add a small value to the diagonal to regularize the matrix - Matrix matPaReg = matPa; - matPaReg = (matPaReg + matPaReg.transpose()) * 0.5; // Ensure symmetry - matPaReg += Matrix::Identity() * 1e-6; // Regularization value - lltOfPa.compute(matPaReg); - if (lltOfPa.info() != Eigen::Success) { - LOG(error) << "Cholesky decomposition failed even after regularization. Attempting again"; - matPaReg += Matrix::Identity() * 1e-3; // Increase regularization value - lltOfPa.compute(matPaReg); - } - if (lltOfPa.info() != Eigen::Success) { - LOG(error) << "\n" << matPaReg; - throw std::runtime_error( - "Cholesky decomposition failed, matrix is not positive definite even after regularization."); - } - } + Eigen::LLT> lltOfPa = calculateCholesky(matPa); + Matrix matSa{lltOfPa.matrixL()}; // sqrt(P_{a}) matSa *= scalarMultiplier; // sqrt( (n + \kappa) * P_{a} ) @@ -398,15 +406,23 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { AtTools::AtPropagator::StepState fMeanStep; /// Holds the step information for POCA propagation of mean state ROOT::Math::Plane3D fMeasurementPlane; ///< Holds the measurement plane for the track fitter + using EigenVectorDimX = std::vector, Eigen::aligned_allocator>>; + using VectorEigenMatDimX = + std::vector, Eigen::aligned_allocator>>; // vectors to hold the information needed for smoothing the UKF - std::vector> m_vecXPredHist; /// @brief History of predicted state vectors at k+1 - std::vector> m_matPPredHist; /// @brief History of predicted state covariances at k+1 + EigenVectorDimX m_vecXPredHist; /// @brief History of predicted state vectors at k+1 + VectorEigenMatDimX m_matPPredHist; /// @brief History of predicted state covariances at k+1 /// History of cross correlation between filtered state at k and predicted at k+1 - std::vector> m_matCPredHist; + VectorEigenMatDimX m_matCPredHist; /// History of filtered (after correction) state vectors at k - std::vector> m_vecXHist; + EigenVectorDimX m_vecXHist; /// History of filtered (after correction) state covariances at k - std::vector> m_matPHist; + VectorEigenMatDimX m_matPHist; + + /// Smoothed state vector and covariance + EigenVectorDimX m_vecXSmooth; + /// Smoothed state covariance + VectorEigenMatDimX m_matPSmooth; /// The sigma points after propagation for the last prediction step. Matrix m_matSigmaXPred{Matrix::Zero()}; @@ -441,9 +457,14 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { TMatrixD GetStateCovariance() const; TMatrixD GetAugStateCovariance() const; std::array GetAugStateVector() const; + const EigenVectorDimX &GetSmoothedStates() const { return m_vecXSmooth; }; + const VectorEigenMatDimX &GetSmoothedCovariances() const { return m_matPSmooth; }; + const EigenVectorDimX &GetFilteredStates() const { return m_vecXHist; }; + const VectorEigenMatDimX &GetFilteredCovariances() const { return m_matPHist; }; void predictUKF(const ROOT::Math::XYZPoint &z); void correctUKF(const ROOT::Math::XYZPoint &z); + void smoothUKF(); protected: std::array calculateProcessNoiseMean() override; diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index a6a82cd5a..565f72492 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -171,6 +171,19 @@ void UKFSingleTrack() residual.push_back(residualValue); // Store the residual for this hit } + // At this point we have the full trajectory of the particle + ukf.smoothUKF(); // Perform smoothing + auto smoothedStates = ukf.GetSmoothedStates(); + auto smoothedCovariances = ukf.GetSmoothedCovariances(); + auto filteredStates = ukf.GetFilteredStates(); + auto filteredCovariances = ukf.GetFilteredCovariances(); + for (int i = 0; i < smoothedStates.size(); ++i) { + auto &state = smoothedStates[i]; + auto &filteredState = filteredStates[i]; + std::cout << "Smoothed state " << i << ": " << state.transpose() << std::endl; + std::cout << "Filtered state " << i << ": " << filteredState.transpose() << std::endl; + } + TGraph2D *track = new TGraph2D(x.size(), x.data(), y.data(), z.data()); track->SetTitle("Particle Track;X [mm];Y [mm];Z [mm]"); track->SetMarkerStyle(20); From 841a7cdb6b3b3932a66d4e4e846eb7a791b18b0a Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 29 Jul 2025 12:32:22 +0200 Subject: [PATCH 54/75] Adjust weights to use method that should protect PD of COV better --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 6 +++-- .../OpenKF/kalman_filter/TrackFitterUKF.h | 24 +++++++++++------ .../kalman_filter/TrackFitterUKFTest.cxx | 2 +- macro/tests/UKF/UKFSingleTrack.C | 26 +++++++++++++++++++ 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index 36981c980..bdb68edc6 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -268,6 +268,7 @@ void TrackFitterUKF::smoothUKF() m_vecXSmooth.back() = m_vecXHist.back(); // The last smoothed state is the last corrected state m_matPSmooth.back() = m_matPHist.back(); // The last smoothed covariance is the last corrected covariance for (size_t i = m_vecXPredHist.size() - 1; i > 0; --i) { + LOG(info) << "Smoothing step " << i << " of " << m_vecXPredHist.size() - 1; // Get the predicted state and covariance at step i const auto &xPred = m_vecXPredHist[i]; // m_{k+1}^- @@ -283,9 +284,10 @@ void TrackFitterUKF::smoothUKF() auto &pSmooth = m_matPSmooth[i]; // P^s_{k+1} auto llt = calculateCholesky(pPred); // Perform Cholesky decomposition - auto D = ccor * pPred.inverse(); // D = C_{k+1} * (P_{k+1}^-)^{-1} + auto D = ccor * llt.solve(Matrix::Identity()); + // auto D = ccor * pPred.inverse(); // D = C_{k+1} * (P_{k+1}^-)^{-1} - std::cout << "D matrix at step " << i << ":\n" << D << "\n"; + // std::cout << "D matrix at step " << i << ":\n" << D << "\n"; m_vecXSmooth[i - 1] = xFilt + D * (xSmooth - xPred); // m^s_{k} = m_{k} + D * (m^s_{k+1} - m_{k+1}^-) m_matPSmooth[i - 1] = pFilt + D * (pSmooth - pPred) * D.transpose(); // P^s_{k} = P_{k} + D * (P^s_{k+1} - P_{k+1}^-) * D^T diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index 4617e7000..e0f032614 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -64,8 +64,10 @@ class TrackFitterUKFBase { /// Measurement noise covariance matrix (R) Matrix m_matR{Matrix::Zero()}; - /// Unscented transform weight for the mean sigma point - float32_t m_weight0; + /// Unscented transform weight for the mean sigma point in mean + float32_t m_weightM0; + /// Unscented transform weight for the mean sigma point in covariance + float32_t m_weightC0; /// Unscented transform weight for the other sigma points float32_t m_weighti; /// Kappa parameter for sigma point calculation @@ -204,9 +206,15 @@ class TrackFitterUKFBase { { static_assert(DIM_A > 0, "DIM_A is Zero which leads to numerical issue."); - const float32_t denoTerm{m_kappa + static_cast(DIM_A)}; + constexpr float alpha = 1.0; // Scaling parameter, set to 1 to match orig + constexpr float beta = 2.0; // Optimal for Gaussian distributions - m_weight0 = m_kappa / denoTerm; + float lambda{alpha * alpha * (DIM_A + m_kappa) - DIM_A}; // Lambda parameter for sigma points + // lambda = m_kappa + const float32_t denoTerm{lambda + static_cast(DIM_A)}; + + m_weightM0 = lambda / denoTerm; + m_weightC0 = m_weightM0 + (1.0F - alpha * alpha + beta); // Weight for the mean sigma point in covariance m_weighti = 0.5F / denoTerm; } /** @@ -333,7 +341,7 @@ class TrackFitterUKFBase { Matrix &matPxx) { // 1. calculate mean of the sigma points - vecX = m_weight0 * util::getColumnAt(0, sigmaX); + vecX = m_weightM0 * util::getColumnAt(0, sigmaX); for (int32_t i{1}; i < SIGMA_DIM; ++i) { vecX += m_weighti * util::getColumnAt(i, sigmaX); // y += W[0, i] Y[:, i] } @@ -342,8 +350,8 @@ class TrackFitterUKFBase { // \bar{y}) (Y[:, i] - \bar{y})^T Vector devXi{util::getColumnAt(0, sigmaX) - vecX}; // Y[:, 0] - \bar{ y } - matPxx = m_weight0 * devXi * devXi.transpose(); // P_0 = W[0, 0] (Y[:, 0] - \bar{y}) (Y[:, 0] - - // \bar{y})^T + matPxx = m_weightC0 * devXi * devXi.transpose(); // P_0 = W[0, 0] (Y[:, 0] - \bar{y}) (Y[:, 0] - + // \bar{y})^T for (int32_t i{1}; i < SIGMA_DIM; ++i) { devXi = util::getColumnAt(i, sigmaX) - vecX; // Y[:, i] - \bar{y} @@ -375,7 +383,7 @@ class TrackFitterUKFBase { Vector devYi{util::getColumnAt(0, sigmaY) - vecY}; // Y[:, 0] - \bar{ y } // P_0 = W[0, 0] (X[:, 0] - \bar{x}) (Y[:, 0] - \bar{y})^T - Matrix matPxy{m_weight0 * (devXi * devYi.transpose())}; + Matrix matPxy{m_weightC0 * (devXi * devYi.transpose())}; for (int32_t i{1}; i < SIGMA_DIM; ++i) { devXi = util::getColumnAt(i, sigmaX) - vecX; // X[:, i] - \bar{x} diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index 9709f4797..4f502d9f1 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -188,7 +188,7 @@ TEST_F(TrackFitterUKFExampleTest, PredictionAndCorrection) // [ 0.00651178 -0.0046378 0.13023154 -0.00210188] // [-0.00465059 0.01344241 -0.00210188 0.1333886 ]] - ASSERT_NEAR(m_ukf.vecX()[0], 2.4758845F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[0], 2.47414017F, FLOAT_EPSILON); ASSERT_NEAR(m_ukf.vecX()[1], 0.53327217F, FLOAT_EPSILON); ASSERT_NEAR(m_ukf.vecX()[2], 0.21649734F, FLOAT_EPSILON); ASSERT_NEAR(m_ukf.vecX()[3], -0.21214576F, FLOAT_EPSILON); diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index 565f72492..92a852ef7 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -61,6 +61,7 @@ void UKFSingleTrack() using namespace AtTools; std::vector x2, y2, z2, Eloss2, p2, sigmap2, lambda2, sigmalambda2, residual; + std::vector xSmooth, ySmooth, zSmooth, pSmooth, sigmapSmooth; // Setup the Propagator for UKF auto elossModel = std::make_unique(0); @@ -182,6 +183,11 @@ void UKFSingleTrack() auto &filteredState = filteredStates[i]; std::cout << "Smoothed state " << i << ": " << state.transpose() << std::endl; std::cout << "Filtered state " << i << ": " << filteredState.transpose() << std::endl; + xSmooth.push_back(state[0]); + ySmooth.push_back(state[1]); + zSmooth.push_back(state[2]); + pSmooth.push_back(state[3]); + sigmapSmooth.push_back(std::sqrt(smoothedCovariances[i](3, 3))); // Momentum uncertainty } TGraph2D *track = new TGraph2D(x.size(), x.data(), y.data(), z.data()); @@ -195,9 +201,29 @@ void UKFSingleTrack() track2->SetMarkerSize(0.8); track2->SetMarkerColor(kRed); + TGraph2D *smoothedTrack = new TGraph2D(xSmooth.size(), xSmooth.data(), ySmooth.data(), zSmooth.data()); + smoothedTrack->SetTitle("Smoothed Particle Track;X [mm];Y [mm];Z [mm]"); + smoothedTrack->SetMarkerStyle(22); + smoothedTrack->SetMarkerSize(0.8); + smoothedTrack->SetMarkerColor(kGreen + 2); + TCanvas *c1 = new TCanvas("c1", "Particle Track", 800, 600); + + // Set axis ranges based on track and track2 + double xmin = std::min(*std::min_element(x.begin(), x.end()), *std::min_element(x2.begin(), x2.end())); + double xmax = std::max(*std::max_element(x.begin(), x.end()), *std::max_element(x2.begin(), x2.end())); + double ymin = std::min(*std::min_element(y.begin(), y.end()), *std::min_element(y2.begin(), y2.end())); + double ymax = std::max(*std::max_element(y.begin(), y.end()), *std::max_element(y2.begin(), y2.end())); + double zmin = std::min(*std::min_element(z.begin(), z.end()), *std::min_element(z2.begin(), z2.end())); + double zmax = std::max(*std::max_element(z.begin(), z.end()), *std::max_element(z2.begin(), z2.end())); + + track->GetXaxis()->SetLimits(xmin, xmax); + track->GetYaxis()->SetLimits(ymin, ymax); + track->GetZaxis()->SetLimits(zmin, zmax); + track->Draw("P"); track2->Draw("PSAME"); + smoothedTrack->Draw("PSAME"); TGraph *elossGraph = new TGraph(Eloss.size()); for (size_t i = 0; i < Eloss.size(); ++i) { From d73785f90097dc648a7c22d11b1df0c7b55e24f6 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 29 Jul 2025 13:05:37 +0200 Subject: [PATCH 55/75] Add function to ensure PD and only save PD matrices Smoother is more stable, but it still fail part way through, so we are still poorly conditioned somewhere. --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 3 ++ .../OpenKF/kalman_filter/TrackFitterUKF.h | 53 +++++++++++++++---- .../kalman_filter/TrackFitterUKFTest.cxx | 1 + 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index bdb68edc6..eee2d4288 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -223,7 +223,9 @@ void TrackFitterUKF::predictUKF(const ROOT::Math::XYZPoint &z) // Calculate the sigma points for the augmented state vector and save in a matrix where each column is a sigma // point. + LOG(info) << "Calculating sigma points for prediction step."; auto sigmaPoints = calculateSigmaPoints(m_vecXa, m_matPa); + LOG(info) << "Finished calculating sigma points for prediction step."; // Pull out the sigma points for the state vector and process noise in two different matrices. Matrix sigmaXxPrior{ @@ -231,6 +233,7 @@ void TrackFitterUKF::predictUKF(const ROOT::Math::XYZPoint &z) m_matSigmaXPred = sigmaXxPrior; // Store the sigma points for the state vector TrackFitterUKFBase::predictUKF(callback, zVec); + LOG(info) << "Finished prediction step."; // Now we need to store the predicted state and covariance for smoothing later. m_vecXPredHist.push_back(m_vecX); // Store the predicted state vector diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index e0f032614..afd8a23e3 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -107,6 +107,7 @@ class TrackFitterUKFBase { { updateAugWithState(); updateAugWithProcessNoise(); + ensurePD(m_matPa); // Ensure the augmented covariance matrix is positive definite } /** @@ -127,7 +128,9 @@ class TrackFitterUKFBase { // Calculate the sigma points for the augmented state vector and save in a matrix where each column is a sigma // point. + LOG(info) << "Calculating sigma points for prediction step2."; m_matSigmaXa = calculateSigmaPoints(m_vecXa, m_matPa); + LOG(info) << "Finished calculating sigma points for prediction step2."; // Pull out the sigma points for the state vector and process noise in two different matrices. Matrix sigmaXx{m_matSigmaXa.block(0, 0, DIM_X, SIGMA_DIM_A)}; // Sigma points for state vector @@ -260,31 +263,58 @@ class TrackFitterUKFBase { } } + /** + * @brief Calculate Cholesky decomposition of a covariance matrix and update the matrix so it is PD. + * + * Modifies the input matrix to ensure it is symmetric and positive definite. + * If the decomposition fails, it attempts to regularize the matrix by adding a small value + * to the diagonal and retrying the decomposition. + */ template Eigen::LLT> calculateCholesky(const Matrix &matP) { Eigen::LLT> lltOfP(matP); if (lltOfP.info() != Eigen::Success) { - LOG(error) << "Cholesky decomposition failed, matrix is not positive definite. Attempting recovery..."; + throw std::runtime_error("Cholesky decomposition failed, matrix is not positive definite."); + } + + return lltOfP; // Return the Cholesky decomposition of the covariance matrix + } + + template + Eigen::LLT> ensurePD(Matrix &matP) + { + Eigen::LLT> lltOfP(matP); + if (lltOfP.info() != Eigen::Success) { + LOG(warn) << "Cholesky decomposition failed, matrix is not positive definite. Attempting recovery..."; // Add a small value to the diagonal to regularize the matrix - Matrix matPReg = matP; - matPReg = (matPReg + matPReg.transpose()) * 0.5; // Ensure symmetry - matPReg += Matrix::Identity() * 1e-6; // Regularization value - lltOfP.compute(matPReg); - if (lltOfP.info() != Eigen::Success) { - LOG(error) << "Cholesky decomposition failed even after regularization. Attempting again"; - matPReg += Matrix::Identity() * 1e-3; // Increase regularization value - lltOfP.compute(matPReg); + int i = 0; + while (lltOfP.info() != Eigen::Success && i < 3) { + LOG(debug) << "Attempting to regularize covariance matrix, iteration: " << i; + + symmetrize(matP); // Ensure symmetry before regularization + matP += Matrix::Identity() * std::pow(10, -6 + i); // Regularization value + lltOfP.compute(matP); + i = i + 1; + // Check if the regularized matrix is now positive definite } if (lltOfP.info() != Eigen::Success) { - LOG(error) << "\n" << matPReg; + LOG(error) << "\n" << matP; throw std::runtime_error( "Cholesky decomposition failed, matrix is not positive definite even after regularization."); - } + } else + LOG(warn) << "Cholesky decomposition succeeded after regularization of order " << i - 1; } return lltOfP; // Return the Cholesky decomposition of the covariance matrix } + template + void symmetrize(Matrix &matP) + { + // Ensure the matrix is symmetric + matP = (matP + matP.transpose()) * 0.5; + } + /** * @brief Algorithm to calculate the deterministic sigma points for * the unscented transformation. @@ -361,6 +391,7 @@ class TrackFitterUKFBase { matPxx += Pi; // y += W[0, i] (Y[:, i] - \bar{y}) (Y[:, i] - \bar{y})^T } + ensurePD(matPxx); // Ensure the covariance matrix is positive definite } /** diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index 4f502d9f1..50ad94688 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -432,6 +432,7 @@ TEST_F(TrackFitterUKFFixture, TestPredictionStep) cov(5, 5) = sigma_phi * sigma_phi; // Angular uncertainty m_ukf->SetInitialState(startPos, startMom, cov); + LOG(info) << "Finished setting initial state"; XYZPoint point({-1.4895, -4.8787, 1.01217}); // measurement point in cm point *= 10; // Convert to mm From 22c624e38b356521281b802e9b7a25d3732006f3 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 29 Jul 2025 14:10:31 +0200 Subject: [PATCH 56/75] Remove debug. Add diagnostic plots --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 3 - .../OpenKF/kalman_filter/TrackFitterUKF.h | 3 +- macro/tests/UKF/UKFSingleTrack.C | 62 ++++++++++++++++--- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index eee2d4288..bdb68edc6 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -223,9 +223,7 @@ void TrackFitterUKF::predictUKF(const ROOT::Math::XYZPoint &z) // Calculate the sigma points for the augmented state vector and save in a matrix where each column is a sigma // point. - LOG(info) << "Calculating sigma points for prediction step."; auto sigmaPoints = calculateSigmaPoints(m_vecXa, m_matPa); - LOG(info) << "Finished calculating sigma points for prediction step."; // Pull out the sigma points for the state vector and process noise in two different matrices. Matrix sigmaXxPrior{ @@ -233,7 +231,6 @@ void TrackFitterUKF::predictUKF(const ROOT::Math::XYZPoint &z) m_matSigmaXPred = sigmaXxPrior; // Store the sigma points for the state vector TrackFitterUKFBase::predictUKF(callback, zVec); - LOG(info) << "Finished prediction step."; // Now we need to store the predicted state and covariance for smoothing later. m_vecXPredHist.push_back(m_vecX); // Store the predicted state vector diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index afd8a23e3..c12f95062 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -128,9 +128,7 @@ class TrackFitterUKFBase { // Calculate the sigma points for the augmented state vector and save in a matrix where each column is a sigma // point. - LOG(info) << "Calculating sigma points for prediction step2."; m_matSigmaXa = calculateSigmaPoints(m_vecXa, m_matPa); - LOG(info) << "Finished calculating sigma points for prediction step2."; // Pull out the sigma points for the state vector and process noise in two different matrices. Matrix sigmaXx{m_matSigmaXa.block(0, 0, DIM_X, SIGMA_DIM_A)}; // Sigma points for state vector @@ -284,6 +282,7 @@ class TrackFitterUKFBase { template Eigen::LLT> ensurePD(Matrix &matP) { + symmetrize(matP); Eigen::LLT> lltOfP(matP); if (lltOfP.info() != Eigen::Success) { LOG(warn) << "Cholesky decomposition failed, matrix is not positive definite. Attempting recovery..."; diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index 92a852ef7..4d7007cd4 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -61,7 +61,7 @@ void UKFSingleTrack() using namespace AtTools; std::vector x2, y2, z2, Eloss2, p2, sigmap2, lambda2, sigmalambda2, residual; - std::vector xSmooth, ySmooth, zSmooth, pSmooth, sigmapSmooth; + std::vector xSmooth, ySmooth, zSmooth, pSmooth, sigmapSmooth, residualSmooth, eLossSmooth; // Setup the Propagator for UKF auto elossModel = std::make_unique(0); @@ -188,6 +188,17 @@ void UKFSingleTrack() zSmooth.push_back(state[2]); pSmooth.push_back(state[3]); sigmapSmooth.push_back(std::sqrt(smoothedCovariances[i](3, 3))); // Momentum uncertainty + residualSmooth.push_back( + (XYZPoint(state[0], state[1], state[2]) - XYZPoint(filteredState[0], filteredState[1], filteredState[2])).R()); + if (i > 0) { + auto lastMom = smoothedStates[i - 1][3]; + auto mom = smoothedStates[i][3]; + auto KE_in = Kinematics::KE(lastMom, mass_p); + auto KE_out = Kinematics::KE(mom, mass_p); + eLossSmooth.push_back(KE_in - KE_out); // Energy loss between smoothed states + } else { + eLossSmooth.push_back(0); // First point has no previous state to compare + } } TGraph2D *track = new TGraph2D(x.size(), x.data(), y.data(), z.data()); @@ -239,13 +250,19 @@ void UKFSingleTrack() eloss2Graph->SetTitle("Propagated Energy Loss per Hit;Hit Number;Energy Loss [MeV]"); eloss2Graph->SetMarkerStyle(21); eloss2Graph->SetMarkerColor(kRed); + TGraph *elossSmoothGraph = new TGraph(eLossSmooth.size()); + for (size_t i = 0; i < eLossSmooth.size(); ++i) { + elossSmoothGraph->SetPoint(i, i, eLossSmooth[i]); + } + elossSmoothGraph->SetTitle("Smoothed Energy Loss per Hit;Hit Number;Energy Loss [MeV]"); + elossSmoothGraph->SetMarkerStyle(22); + elossSmoothGraph->SetMarkerColor(kGreen + 2); TGraphErrors *pGraph = new TGraphErrors(p2.size()); for (size_t i = 0; i < p2.size(); ++i) { - pGraph->SetPoint(i, i, p2[i] * 1e-3 * pointsToCluster / 5.); + pGraph->SetPoint(i, i, p2[i]); pGraph->SetPointError(i, 0, - sigmap2[i] * 5 * 1e-3 * pointsToCluster / - 5.); // Error bars from sigmap2, converted to GeV/c + sigmap2[i] * 5); // Error bars from sigmap2, converted to GeV/c } pGraph->SetTitle("Momentum per Hit;Hit Number;Momentum [MeV/c]"); pGraph->SetMarkerStyle(20); @@ -265,16 +282,45 @@ void UKFSingleTrack() for (size_t i = 0; i < residual.size(); ++i) { residualGraph->SetPoint(i, i, residual[i] * .1); } - residualGraph->SetTitle("Residual per Hit;Hit Number;Residual [mm]"); + residualGraph->SetTitle("Residual per Hit;Hit Number;Residual [cm]"); residualGraph->SetMarkerStyle(23); residualGraph->SetMarkerColor(kMagenta); + TGraphErrors *pSmoothGraph = new TGraphErrors(pSmooth.size()); + for (size_t i = 0; i < pSmooth.size(); ++i) { + pSmoothGraph->SetPoint(i, i, pSmooth[i]); + pSmoothGraph->SetPointError(i, 0, + sigmapSmooth[i] * 5); // Error bars from sigmapSmooth, converted to GeV/c + } + pSmoothGraph->SetTitle("Smoothed Momentum per Hit;Hit Number;Momentum [MeV/c]"); + pSmoothGraph->SetMarkerStyle(22); + pSmoothGraph->SetMarkerColor(kGreen + 2); + + TGraph *residualSmoothGraph = new TGraph(residualSmooth.size()); + for (size_t i = 0; i < residualSmooth.size(); ++i) { + residualSmoothGraph->SetPoint(i, i, residualSmooth[i] * .1); + } + residualSmoothGraph->SetTitle("Smoothed Residual per Hit;Hit Number;Residual [cm]"); + residualSmoothGraph->SetMarkerStyle(24); + residualSmoothGraph->SetMarkerColor(kOrange + 7); + TCanvas *c2 = new TCanvas("c2", "Energy Loss per Hit", 800, 600); elossGraph->Draw("AP"); eloss2Graph->Draw("PSAME"); - pGraph->Draw("PSAME"); - // lambdaGraph->Draw("PSAME"); - residualGraph->Draw("PSAME"); + elossSmoothGraph->Draw("PSAME"); + // pGraph->Draw("PSAME"); + // lambdaGraph->Draw("PSAME"); + // residualGraph->Draw("PSAME"); + // pSmoothGraph->Draw("PSAME"); + // residualSmoothGraph->Draw("PSAME"); + + TCanvas *c3 = new TCanvas("c3", "Momentum at Hit (error bars 5X)", 800, 600); + pGraph->Draw("AP"); + pSmoothGraph->Draw("PSAME"); + + TCanvas *c4 = new TCanvas("c4", "Residual at Hit", 800, 600); + residualGraph->Draw("AP"); + residualSmoothGraph->Draw("PSAME"); double sumEloss = std::accumulate(Eloss.begin(), Eloss.end(), 0.0); double sumEloss2 = std::accumulate(Eloss2.begin(), Eloss2.end(), 0.0); From 56bc6218b5e3663712d448d3269c9a4a3629974a Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 29 Jul 2025 15:40:57 +0200 Subject: [PATCH 57/75] Parameter tuning --- .../OpenKF/kalman_filter/TrackFitterUKF.h | 45 +++++++++---------- .../kalman_filter/TrackFitterUKFTest.cxx | 4 +- macro/tests/UKF/AtPropagator.C | 9 +++- macro/tests/UKF/UKFSingleTrack.C | 8 +++- 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index c12f95062..fc35231f8 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -70,17 +70,29 @@ class TrackFitterUKFBase { float32_t m_weightC0; /// Unscented transform weight for the other sigma points float32_t m_weighti; - /// Kappa parameter for sigma point calculation - float32_t m_kappa{3 - DIM_A}; + /// Lambda parameter for sigma point calculation + float32_t m_lambda{0}; public: - TrackFitterUKFBase() { updateWeights(); } + TrackFitterUKFBase() { setParameters(1, 2, 0); } ~TrackFitterUKFBase() = default; - void setKappa(float32_t kappa) + /** + * @brief Set the weights used to calculate sigma points. + */ + void setParameters(float alpha, float beta, float kappa) { - m_kappa = kappa; // Set the kappa parameter for sigma point calculation - updateWeights(); // Update the weights based on the new kappa value + static_assert(DIM_A > 0, "DIM_A is Zero which leads to numerical issue."); + + m_lambda = alpha * alpha * (DIM_A + kappa) - DIM_A; + float32_t denoTerm = m_lambda + static_cast(DIM_A); + + m_weightM0 = m_lambda / denoTerm; + m_weightC0 = m_weightM0 + (1.0F - alpha * alpha + beta); // Weight for the mean sigma point in covariance + m_weighti = 0.5F / denoTerm; + LOG(info) << "Mean weight " << m_weightM0; + LOG(info) << "Cov weight: " << m_weightC0; + LOG(info) << "Shared weight: " << m_weighti; } /** @@ -200,24 +212,6 @@ class TrackFitterUKFBase { } protected: - /** - * @brief Set the weights used to calculate sigma points. - */ - void updateWeights() - { - static_assert(DIM_A > 0, "DIM_A is Zero which leads to numerical issue."); - - constexpr float alpha = 1.0; // Scaling parameter, set to 1 to match orig - constexpr float beta = 2.0; // Optimal for Gaussian distributions - - float lambda{alpha * alpha * (DIM_A + m_kappa) - DIM_A}; // Lambda parameter for sigma points - // lambda = m_kappa - const float32_t denoTerm{lambda + static_cast(DIM_A)}; - - m_weightM0 = lambda / denoTerm; - m_weightC0 = m_weightM0 + (1.0F - alpha * alpha + beta); // Weight for the mean sigma point in covariance - m_weighti = 0.5F / denoTerm; - } /** * @brief Add state vector and state covariance matrix to the augmented state vector covariance matrix. */ @@ -328,7 +322,7 @@ class TrackFitterUKFBase { Matrix calculateSigmaPoints(const Vector &vecXa, const Matrix &matPa) { - const float32_t scalarMultiplier{std::sqrt(STATE_DIM + m_kappa)}; // sqrt(n + \kappa) + const float32_t scalarMultiplier{std::sqrt(STATE_DIM + m_lambda)}; // sqrt(n + \kappa) Eigen::LLT> lltOfPa = calculateCholesky(matPa); @@ -447,6 +441,7 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { using EigenVectorDimX = std::vector, Eigen::aligned_allocator>>; using VectorEigenMatDimX = std::vector, Eigen::aligned_allocator>>; + // vectors to hold the information needed for smoothing the UKF EigenVectorDimX m_vecXPredHist; /// @brief History of predicted state vectors at k+1 VectorEigenMatDimX m_matPPredHist; /// @brief History of predicted state covariances at k+1 diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index 50ad94688..8ce7dfc3d 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -77,6 +77,7 @@ TEST_F(TrackFitterUKFExampleTest, Prediction) m_ukf.setCovarianceQ(Q); m_ukf.setCovarianceR(R); + m_ukf.setParameters(1, 0, 3 - m_ukf.DIM_A); // Set kappa to match the original implementation m_ukf.predictUKF(funcF, z); @@ -138,6 +139,7 @@ TEST_F(TrackFitterUKFExampleTest, PredictionAndCorrection) m_ukf.setCovarianceQ(Q); m_ukf.setCovarianceR(R); + m_ukf.setParameters(1, 0, 3 - m_ukf.DIM_A); // Set kappa to match the original implementation m_ukf.predictUKF(funcF, z); @@ -188,7 +190,7 @@ TEST_F(TrackFitterUKFExampleTest, PredictionAndCorrection) // [ 0.00651178 -0.0046378 0.13023154 -0.00210188] // [-0.00465059 0.01344241 -0.00210188 0.1333886 ]] - ASSERT_NEAR(m_ukf.vecX()[0], 2.47414017F, FLOAT_EPSILON); + ASSERT_NEAR(m_ukf.vecX()[0], 2.4758845F, FLOAT_EPSILON); ASSERT_NEAR(m_ukf.vecX()[1], 0.53327217F, FLOAT_EPSILON); ASSERT_NEAR(m_ukf.vecX()[2], 0.21649734F, FLOAT_EPSILON); ASSERT_NEAR(m_ukf.vecX()[3], -0.21214576F, FLOAT_EPSILON); diff --git a/macro/tests/UKF/AtPropagator.C b/macro/tests/UKF/AtPropagator.C index 80774a23c..a20fef05a 100644 --- a/macro/tests/UKF/AtPropagator.C +++ b/macro/tests/UKF/AtPropagator.C @@ -33,7 +33,14 @@ void AtPropagator() auto elossModel = std::make_unique(0); elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path elossModel->SetDensity(3.553e-5); // Set density in g/cm^3 for 300 torr H2 - AtTools::AtPropagator propagator(charge, mass, std::move(elossModel)); + + auto elossModel2 = std::make_unique(3.553e-5); + elossModel2->SetProjectile(1, 1, 1); + std::vector> mat; + mat.push_back({1, 1, 1}); + elossModel2->SetMaterial(mat); + + AtTools::AtPropagator propagator(charge, mass, std::move(elossModel2)); propagator.SetEField({0, 0, 0}); // No electric field propagator.SetBField({0, 0, 2.85}); // Magnetic field AtTools::AtRK4Stepper stepper; diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index 4d7007cd4..d6639004e 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -88,6 +88,7 @@ void UKFSingleTrack() startPos *= 10; // Convert to mm XYZVector startMom(0.00935463, -0.0454279, 0.00826042); // Start momentum in GeV/c startMom *= 1e3; + double beginMom = startMom.R(); // Initial momentum in MeV/c XYZPoint nextPos(x[1], y[1], z[1]); startMom = startMom.R() * (nextPos - startPos).Unit(); // Set momentum direction towards the first hit @@ -98,6 +99,7 @@ void UKFSingleTrack() double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree ukf.fEnableEnStraggling = true; // Enable energy straggling + ukf.setParameters(1e-2, 2, 0); // Set kappa to match the original implementation TMatrixD cov(6, 6); cov.Zero(); @@ -200,6 +202,9 @@ void UKFSingleTrack() eLossSmooth.push_back(0); // First point has no previous state to compare } } + LOG(info) << "Initial smoothed momentum: " << smoothedStates[0][3]; + LOG(info) << "Starting momentum: " << beginMom; + LOG(info) << "Error in momentum reconstruction: " << (smoothedStates[0][3] - beginMom) / beginMom * 100 << "%"; TGraph2D *track = new TGraph2D(x.size(), x.data(), y.data(), z.data()); track->SetTitle("Particle Track;X [mm];Y [mm];Z [mm]"); @@ -272,7 +277,7 @@ void UKFSingleTrack() for (size_t i = 0; i < lambda2.size(); ++i) { lambdaGraph->SetPoint(i, i, lambda2[i] * 0.03 * pointsToCluster / 5.); lambdaGraph->SetPointError(i, 0, sigmalambda2[i] * 0.03 * pointsToCluster / 5.); - std::cout << "Lambda: " << lambda2[i] << ", Error: " << sigmalambda2[i] << std::endl; + // std::cout << "Lambda: " << lambda2[i] << ", Error: " << sigmalambda2[i] << std::endl; } lambdaGraph->SetTitle("Lambda per Hit (scaled);Hit Number;Lambda [scaled]"); lambdaGraph->SetMarkerStyle(22); @@ -326,4 +331,5 @@ void UKFSingleTrack() double sumEloss2 = std::accumulate(Eloss2.begin(), Eloss2.end(), 0.0); std::cout << "Sum of Eloss: " << sumEloss << std::endl; std::cout << "Sum of Eloss2: " << sumEloss2 << std::endl; + std::cout << "Initial energy: " << Kinematics::KE(beginMom, mass_p) << " MeV" << std::endl; } \ No newline at end of file From 06919b8a4b54f96259bf12af8c3fff0337e2b161 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 29 Jul 2025 18:03:10 +0200 Subject: [PATCH 58/75] Add ability to build CATIMA offline if source is already downloaded. --- CMakeLists.txt | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ae91b0e3..3d9fff690 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,20 +126,37 @@ find_package2(PUBLIC CATIMA) find_package2(PUBLIC Eigen3 3.3 NO_MODULE) # If catima was not found, then download and build it locally +set(CATIMA_LOCAL_SOURCE "${CMAKE_BINARY_DIR}/_deps/catima-src") +set(CATIMA_USE_LOCAL ON) +if(NOT EXISTS "${CATIMA_LOCAL_SOURCE}/CMakeLists.txt" AND ${CATIMA_USE_LOCAL}) + message(WARNING "Local cmake requested, but the source could not be found at ${CATIMA_LOCAL_SOURCE}.") + message(INFO EXISTS "${CATIMA_LOCAL_SOURCE}/CMakeLists.txt" ) + SET(CATIMA_USE_LOCAL OFF) +endif() + if(NOT CATIMA_FOUND) message(STATUS "CATIMA not found, downloading and building locally") - FetchContent_Declare(catima - GIT_REPOSITORY https://github.com/hrosiak/catima.git - GIT_TAG master) + if(${CATIMA_USE_LOCAL}) + FetchContent_Declare(catima + SOURCE_DIR ${CATIMA_LOCAL_DIR}) + else() + FetchContent_Declare(catima + GIT_REPOSITORY https://github.com/hrosiak/catima.git + GIT_TAG master + SOURCE_DIR ${CATIMA_LOCAL_DIR}) + endif() + # Set CATIMA build options before making it available set(TESTS OFF CACHE BOOL "Disable CATIMA tests" FORCE) set(EXAMPLES OFF CACHE BOOL "Disable CATIMA examples" FORCE) set(APPS OFF CACHE BOOL "Disable CATIMA applications" FORCE) set(GENERATE_DATA OFF CACHE BOOL "Disable CATIMA data generator" FORCE) + FetchContent_GetProperties(catima) if (NOT catima_POPULATED) + message(STATUS "Populating CATIMA") FetchContent_Populate(catima) add_subdirectory(${catima_SOURCE_DIR} ${catima_BINARY_DIR}) endif() From 520786fc1ceb67027b4df104fff6ba42142d62b9 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 29 Jul 2025 19:03:17 +0200 Subject: [PATCH 59/75] Increase stability of filter Ensures each update to a covariance matrix maintains PD. --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 3 ++- .../AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h | 14 +++++++++----- macro/tests/UKF/UKFSingleTrack.C | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index bdb68edc6..0752750e7 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -285,7 +285,8 @@ void TrackFitterUKF::smoothUKF() auto llt = calculateCholesky(pPred); // Perform Cholesky decomposition auto D = ccor * llt.solve(Matrix::Identity()); - // auto D = ccor * pPred.inverse(); // D = C_{k+1} * (P_{k+1}^-)^{-1} + // auto D = llt.solve(ccor.transpose()).transpose(); + // auto D = ccor * pPred.inverse(); // D = C_{k+1} * (P_{k+1}^-)^{-1} // std::cout << "D matrix at step " << i << ":\n" << D << "\n"; m_vecXSmooth[i - 1] = xFilt + D * (xSmooth - xPred); // m^s_{k} = m_{k} + D * (m^s_{k+1} - m_{k+1}^-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index fc35231f8..43388f379 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -195,20 +195,24 @@ class TrackFitterUKFBase { // calculate the mean measurement vector and covariance matrix // from the sigma points. - Vector vecZhat; - Matrix matPzz; + Vector vecZhat; // Predicted measurement vector + Matrix matPzz; // Measurement covariance matrix calculateWeightedMeanAndCovariance(sigmaZ, vecZhat, matPzz); // Add in the measurement noise covariance matrix to the measurement covariance matrix. - matPzz += m_matR; // Add measurement noise covariance + matPzz += m_matR; // Add measurement noise covariance so we gen the innovation covariance matrix. + ensurePD(matPzz); // Ensure the covariance matrix is positive definite const Matrix matPxz{calculateCrossCorrelation(sigmaXx, m_vecX, sigmaZ, vecZhat)}; // kalman gain - const Matrix matK{matPxz * matPzz.inverse()}; + auto llt = calculateCholesky(matPzz); + const Matrix matK = llt.solve(matPxz.transpose()).transpose(); + // Matrix matK = {matPxz * llt.solve(Matrix::Identity())}; m_vecX += matK * (vecZ - vecZhat); m_matP -= matK * matPzz * matK.transpose(); + ensurePD(m_matP); // Ensure the covariance matrix is positive definite } protected: @@ -279,7 +283,7 @@ class TrackFitterUKFBase { symmetrize(matP); Eigen::LLT> lltOfP(matP); if (lltOfP.info() != Eigen::Success) { - LOG(warn) << "Cholesky decomposition failed, matrix is not positive definite. Attempting recovery..."; + LOG(warn) << "Cholesky decomposition failed while ensuring PD. Attempting recovery..."; // Add a small value to the diagonal to regularize the matrix int i = 0; while (lltOfP.info() != Eigen::Success && i < 3) { diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index d6639004e..b524138ed 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -99,7 +99,7 @@ void UKFSingleTrack() double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree ukf.fEnableEnStraggling = true; // Enable energy straggling - ukf.setParameters(1e-2, 2, 0); // Set kappa to match the original implementation + ukf.setParameters(1e-1, 2, 0); // Set kappa to match the original implementation TMatrixD cov(6, 6); cov.Zero(); From d9247ca0958eeb71c67195b476828a989453a51e Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 29 Jul 2025 19:21:01 +0200 Subject: [PATCH 60/75] Optimize Kalman gain calculation --- .../AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index 43388f379..e5cacd4fb 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -207,11 +207,13 @@ class TrackFitterUKFBase { // kalman gain auto llt = calculateCholesky(matPzz); - const Matrix matK = llt.solve(matPxz.transpose()).transpose(); - // Matrix matK = {matPxz * llt.solve(Matrix::Identity())}; + // const Matrix matK = llt.solve(matPxz.transpose()).transpose(); + Matrix matK = {matPxz * llt.solve(Matrix::Identity())}; + // Matrix matK = matPxz * matPzz.inverse(); By far the worst method for filter stability m_vecX += matK * (vecZ - vecZhat); m_matP -= matK * matPzz * matK.transpose(); + // m_matP -= matPxz * matK.transpose(); ensurePD(m_matP); // Ensure the covariance matrix is positive definite } From 42994ef75d92a4d449670f437fa3cff94ec6968a Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 29 Jul 2025 19:30:29 +0200 Subject: [PATCH 61/75] Finish optimizing of smoothing gain --- .../AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index 0752750e7..0f754ebcd 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -283,10 +283,9 @@ void TrackFitterUKF::smoothUKF() auto &xSmooth = m_vecXSmooth[i]; // m^s_{k+1} auto &pSmooth = m_matPSmooth[i]; // P^s_{k+1} - auto llt = calculateCholesky(pPred); // Perform Cholesky decomposition - auto D = ccor * llt.solve(Matrix::Identity()); - // auto D = llt.solve(ccor.transpose()).transpose(); - // auto D = ccor * pPred.inverse(); // D = C_{k+1} * (P_{k+1}^-)^{-1} + auto llt = calculateCholesky(pPred); + // auto D = ccor * llt.solve(Matrix::Identity()); + auto D = ccor * pPred.inverse(); // D = C_{k+1} * (P_{k+1}^-)^{-1} // std::cout << "D matrix at step " << i << ":\n" << D << "\n"; m_vecXSmooth[i - 1] = xFilt + D * (xSmooth - xPred); // m^s_{k} = m_{k} + D * (m^s_{k+1} - m_{k+1}^-) From 075ab927ed3672fd3bc767bd6f0a27c76451644a Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 30 Jul 2025 12:03:17 +0200 Subject: [PATCH 62/75] Zero missing parts of filter --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index 0f754ebcd..3fbb31e27 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -18,6 +18,7 @@ void TrackFitterUKF::Reset() m_matQ.setZero(); m_matR.setZero(); m_matSigmaXa.setZero(); + m_matSigmaXPred.setZero(); // Clear the history vectors m_vecXPredHist.clear(); @@ -25,6 +26,8 @@ void TrackFitterUKF::Reset() m_matCPredHist.clear(); m_vecXHist.clear(); m_matPHist.clear(); + m_vecXSmooth.clear(); + m_matPSmooth.clear(); fMeanStep = AtTools::AtPropagator::StepState(); // Reset the step state } @@ -136,10 +139,10 @@ Matrix TrackFitterUKF::calcu factor = fMaxStragglingFactor; } matQ(0, 0) = factor * factor; // Variance for the dedx straggling. - LOG(info) << "Calculating process noise for straggling between " << eIn << " MeV and " << eOut << " MeV over " - << elossModel->GetRange(eIn, eOut) << " mm."; - LOG(info) << "Process noise covariance for energy straggling: " << matQ(0, 0) << " (factor: " << factor - << ", dedx_straggle: " << dedx_straggle << ", dEdx: " << elossModel->GetdEdx(eIn) << ")"; + LOG(debug) << "Calculating process noise for straggling between " << eIn << " MeV and " << eOut << " MeV over " + << elossModel->GetRange(eIn, eOut) << " mm."; + LOG(debug) << "Process noise covariance for energy straggling: " << matQ(0, 0) << " (factor: " << factor + << ", dedx_straggle: " << dedx_straggle << ", dEdx: " << elossModel->GetdEdx(eIn) << ")"; } else { throw std::runtime_error("Cannot calculate process noise covariance without an energy loss model"); @@ -198,8 +201,8 @@ void TrackFitterUKF::predictUKF(const ROOT::Math::XYZPoint &z) XYZPoint startingPosition{m_vecX[0], m_vecX[1], m_vecX[2]}; // Get the starting position from the state vector Polar3DVector startingMomentum{m_vecX[3], m_vecX[4], m_vecX[5]}; // Get the starting momentum from the state vector - LOG(info) << "Propagating reference state from position: " << startingPosition - << " with momentum: " << XYZVector(startingMomentum); + LOG(debug) << "Propagating reference state from position: " << startingPosition + << " with momentum: " << XYZVector(startingMomentum); fPropagator.SetState(startingPosition, XYZVector(startingMomentum)); fPropagator.PropagateToMeasurementSurface(AtTools::AtMeasurementPoint(z), *fStepper); @@ -207,7 +210,7 @@ void TrackFitterUKF::predictUKF(const ROOT::Math::XYZPoint &z) fMeanStep.fLastPos = startingPosition; // Store the last position fMeanStep.fLastMom = startingMomentum; // Store the last momentum - LOG(info) << "Propagated to position: " << fMeanStep.fPos << " with momentum: " << fMeanStep.fMom; + LOG(debug) << "Propagated to position: " << fMeanStep.fPos << " with momentum: " << fMeanStep.fMom; // Now we can construct the reference plane. fMeasurementPlane = Plane3D(fMeanStep.fMom.Unit(), @@ -268,7 +271,7 @@ void TrackFitterUKF::smoothUKF() m_vecXSmooth.back() = m_vecXHist.back(); // The last smoothed state is the last corrected state m_matPSmooth.back() = m_matPHist.back(); // The last smoothed covariance is the last corrected covariance for (size_t i = m_vecXPredHist.size() - 1; i > 0; --i) { - LOG(info) << "Smoothing step " << i << " of " << m_vecXPredHist.size() - 1; + LOG(debug) << "Smoothing step " << i << " of " << m_vecXPredHist.size() - 1; // Get the predicted state and covariance at step i const auto &xPred = m_vecXPredHist[i]; // m_{k+1}^- From 534bfe10ea7feb3384e109e52f9e673f79a428f3 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 30 Jul 2025 12:04:04 +0200 Subject: [PATCH 63/75] Add test for many tracks --- macro/tests/UKF/TestManyTracks.C | 200 +++++++++++++++++++++++++++++++ macro/tests/UKF/UKFSingleTrack.C | 9 +- 2 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 macro/tests/UKF/TestManyTracks.C diff --git a/macro/tests/UKF/TestManyTracks.C b/macro/tests/UKF/TestManyTracks.C new file mode 100644 index 000000000..f025b74b1 --- /dev/null +++ b/macro/tests/UKF/TestManyTracks.C @@ -0,0 +1,200 @@ +std::string getEnergyPath() +{ + auto env = std::getenv("VMCWORKDIR"); + if (env == nullptr) { + return "../../resources/energy_loss/HinH.txt"; // Default path assuming cwd is build/AtTools + } + return std::string(env) + "/resources/energy_loss/HinH.txt"; // Use environment variable +} +using ROOT::Math::XYZPoint; +using ROOT::Math::XYZVector; + +const double mass_p = 938.272; // Mass of proton in MeV/c^2 +const double charge_p = 1.602176634e-19; // Charge of proton + +// Vectors to store the simulated points to compare to +std::vector x_sim, y_sim, z_sim, Eloss_sim; +TH1F *hMom = nullptr; +TH1F *hMomError = nullptr; +TCanvas *c1 = new TCanvas(); +TCanvas *c2 = new TCanvas(); + +// Parameters for model +const int pointsToCluster = 5; +const double sigma_pos = 1; // Position uncertainty of 10 mm +const double sigma_mom = 0.01; // Momentum uncertainty in percentage +const double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree +const double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree +const double gasDensity = 3.553e-5; // g/cm^3 +const double fAlpha = 1e-1; +const double fBeta = 2; +const double fKappa = 0; + +// Global variables +kf::TrackFitterUKF *ukf = nullptr; + +// Functions in file +/// Loads hits from an input file +void LoadHits(); +/// Run a single UKF for the passed initial state +void SingleUKF(XYZPoint initialPos, XYZVector initialMom, TMatrixD initialCov); +/// @brief Create the ukf pointer to reuse. +void CreateUKF(); + +TMatrixD CalculateInitialCov(double p); +TMatrixD CalculatePosCov(); + +/// @brief Test many tracks, saving stat properties +/// @param n +/// @param bias +void TestManyTracks(int n, double bias = 0) +{ + LoadHits(); + CreateUKF(); + + XYZPoint fTruePos(-3.40046e-04, -1.49863e-04, 1.0018); + XYZVector fTrueMom(0.00935463, -0.0454279, 0.00826042); + fTrueMom *= 1e3; + double fSigmaMom = fTrueMom.R() * sigma_mom; + + hMom = new TH1F("hMom", "Reconstructed Momentum (MeV/c)", 100, fTrueMom.R() - 4 * fSigmaMom, + fTrueMom.R() + 4 * fSigmaMom); + hMomError = + new TH1F("hMomError", "Error (%)", 100, -4 * fSigmaMom / fTrueMom.R() * 100, 4 * fSigmaMom / fTrueMom.R() * 100); + + for (int i = 0; i < n; ++i) { + + if (i % 100 == 0) + std::cout << "On iteration " << i << std::endl; + + double pSampled = gRandom->Gaus(fTrueMom.R(), sigma_mom * fTrueMom.R()); + pSampled += bias; + + // pSampled = fTrueMom.R(); + + ROOT::Math::Polar3DVector sampledMom(pSampled, fTrueMom.Theta(), fTrueMom.Phi()); + // try { + SingleUKF(fTruePos, XYZVector(sampledMom), CalculateInitialCov(pSampled)); + //} catch (...) { + // std::cerr << "Failed to propagate iteration " << i << " with seed momentum " << pSampled << std::endl; + // continue; + //} + + auto filtState = ukf->GetFilteredStates()[0]; + auto smoothState = ukf->GetSmoothedStates()[0]; + + double pReco = smoothState[3]; + hMom->Fill(pReco); + double error = (pReco - fTrueMom.R()) / fTrueMom.R() * 100; + hMomError->Fill(error); + + std::cout << std::endl + << std::endl + << "With initial momentum " << pSampled << " reconstructed " << pReco << " Error: " << error << "%" + << std::endl + << std::endl; + } + + // Draw results + c1->cd(); + hMom->Draw("hist"); + c2->cd(); + hMomError->Draw("hist"); +} + +/*********** Function implementations **************/ +void LoadHits() +{ + if (x_sim.size() != 0) + return; + Eloss_sim.clear(); + std::ifstream infile("hits.txt"); + double xi, yi, zi, Ei; + int i = 0; + double eLoss = 0; + + // Save first point. + infile >> xi >> yi >> zi >> Ei; + eLoss = Ei; // Initialize energy loss + x_sim.push_back(xi * 10); // Convert to mm + y_sim.push_back(yi * 10); // Convert to mm + z_sim.push_back(zi * 10); // Convert to mm + + while (infile >> xi >> yi >> zi >> Ei) { + // Ei *= 1e3; // Convert to MeV + + if (++i % pointsToCluster != 0) { + eLoss += Ei; + continue; // Skip every 5th point + } + x_sim.push_back(xi * 10); + y_sim.push_back(yi * 10); + z_sim.push_back(zi * 10); + Eloss_sim.push_back(eLoss); + eLoss = 0; // Reset energy loss for the next segment + } +} + +void CreateUKF() +{ + auto elossModel2 = std::make_unique(gasDensity); + elossModel2->SetProjectile(1, 1, 1); + std::vector> mat; + mat.push_back({1, 1, 1}); + elossModel2->SetMaterial(mat); + + auto elossModel = std::make_unique(0); + elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path + elossModel->SetDensity(gasDensity); + + AtTools::AtPropagator propagator(charge_p, mass_p, std::move(elossModel)); + propagator.SetEField({0, 0, 0}); // No electric field + propagator.SetBField({0, 0, 2.85}); // Magnetic field + + // Setup stepper for UKF + auto stepper = std::make_unique(); + + // Setup UKF + ukf = new kf::TrackFitterUKF(std::move(propagator), std::move(stepper)); + ukf->setParameters(fAlpha, fBeta, fKappa); +} + +TMatrixD CalculateInitialCov(double p) +{ + TMatrixD cov(6, 6); + cov.Zero(); + for (int i = 0; i < 3; ++i) { + + cov(i, i) = sigma_pos * sigma_pos; // Set diagonal covariance to some small number + } + cov(3, 3) = p * p * sigma_mom * sigma_mom; // Momentum uncertainty + cov(4, 4) = sigma_theta * sigma_theta; // Angular uncertainty + cov(5, 5) = sigma_phi * sigma_phi; // Angular uncertainty + + return cov; +} +TMatrixD CalculatePosCov() +{ + TMatrixD cov(3, 3); + cov.Zero(); + for (int i = 0; i < 3; ++i) { + + cov(i, i) = sigma_pos * sigma_pos; // Set diagonal covariance to some small number + } + return cov; +} + +void SingleUKF(XYZPoint intitialPos, XYZVector initialMom, TMatrixD intitialCov) +{ + ukf->SetInitialState(intitialPos, initialMom, intitialCov); + + for (int i = 1; i < x_sim.size(); ++i) { + ukf->SetMeasCov(CalculatePosCov()); + XYZPoint point(x_sim[i], y_sim[i], z_sim[i]); + + ukf->predictUKF(point); + ukf->correctUKF(point); + } + + ukf->smoothUKF(); +} \ No newline at end of file diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index b524138ed..1c0ebf82e 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -12,7 +12,7 @@ const double charge_p = 1.602176634e-19; // Charge of proton // Simulated (measurement) hits std::vector x, y, z, Eloss; -int pointsToCluster = 5; +int pointsToCluster = 20; void LoadHits() { std::ifstream infile("hits.txt"); @@ -99,7 +99,7 @@ void UKFSingleTrack() double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree ukf.fEnableEnStraggling = true; // Enable energy straggling - ukf.setParameters(1e-1, 2, 0); // Set kappa to match the original implementation + ukf.setParameters(1e-1, 2, 0); // alpha, beta, kappa TMatrixD cov(6, 6); cov.Zero(); @@ -141,9 +141,7 @@ void UKFSingleTrack() ukf.predictUKF(point); auto augState = ukf.GetAugStateVector(); auto augCov = ukf.GetAugStateCovariance(); - std::cout << std::endl << "Prediction step complete." << std::endl; ukf.correctUKF(point); - std::cout << std::endl << "Correction step complete." << std::endl; auto state = ukf.GetStateVector(); auto cov = ukf.GetStateCovariance(); @@ -154,7 +152,6 @@ void UKFSingleTrack() std::cout << "Predicted position: " << pos << std::endl; std::cout << "Predicted momentum: " << mom << std::endl; - std::cout << "Measurement point: " << point << std::endl; auto KE_in = Kinematics::KE(lastMom, mass_p); @@ -183,8 +180,6 @@ void UKFSingleTrack() for (int i = 0; i < smoothedStates.size(); ++i) { auto &state = smoothedStates[i]; auto &filteredState = filteredStates[i]; - std::cout << "Smoothed state " << i << ": " << state.transpose() << std::endl; - std::cout << "Filtered state " << i << ": " << filteredState.transpose() << std::endl; xSmooth.push_back(state[0]); ySmooth.push_back(state[1]); zSmooth.push_back(state[2]); From 59f4cb40c9b044297d36d6e142ac80da144d5f57 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 30 Jul 2025 08:33:28 -0400 Subject: [PATCH 64/75] Add diagnostic and add COV correction --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 8 +++++ .../OpenKF/kalman_filter/TrackFitterUKF.h | 29 ++++++++++++++++--- macro/tests/UKF/UKFSingleTrack.C | 19 ++++++++++-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index 3fbb31e27..62fe27ba2 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -63,6 +63,9 @@ void TrackFitterUKF::SetInitialState(const ROOT::Math::XYZPoint &initialPosition m_matSigmaXa = calculateSigmaPoints(m_vecXa, m_matPa); // Calculate the sigma points for the initial state // Now we grab the sigma points only for the state. m_matSigmaXPred = m_matSigmaXa.block(0, 0, TF_DIM_X, SIGMA_DIM_A); // Extract the state sigma points + + logEigen("Initial cov", m_matP, 0); // Log the eigenvalues of the initial covariance matrix + LOG(info) << "Initial COV:" << std::endl << m_matP; } TMatrixD TrackFitterUKF::GetStateCovariance() const @@ -235,6 +238,8 @@ void TrackFitterUKF::predictUKF(const ROOT::Math::XYZPoint &z) TrackFitterUKFBase::predictUKF(callback, zVec); + // logEigen("State P-", m_matP, m_matPPredHist.size()); + // Now we need to store the predicted state and covariance for smoothing later. m_vecXPredHist.push_back(m_vecX); // Store the predicted state vector m_matPPredHist.push_back(m_matP); // Store the predicted covariance matrix @@ -256,6 +261,8 @@ void TrackFitterUKF::correctUKF(const ROOT::Math::XYZPoint &z) auto callback = [this](const kf::Vector &x_) { return funcH(x_); }; TrackFitterUKFBase::correctUKF(callback, zVec); + // logEigen("State PCorr", m_matP, m_matPHist.size()); + // After correction we need to save the filtered state m_vecXHist.push_back(m_vecX); // Store the filtered state vector m_matPHist.push_back(m_matP); // Store the filtered covariance matrix @@ -294,6 +301,7 @@ void TrackFitterUKF::smoothUKF() m_vecXSmooth[i - 1] = xFilt + D * (xSmooth - xPred); // m^s_{k} = m_{k} + D * (m^s_{k+1} - m_{k+1}^-) m_matPSmooth[i - 1] = pFilt + D * (pSmooth - pPred) * D.transpose(); // P^s_{k} = P_{k} + D * (P^s_{k+1} - P_{k+1}^-) * D^T + logEigen("State P+", m_matPSmooth[i - 1], i - 1); } } diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index e5cacd4fb..ba728cde3 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -49,6 +49,7 @@ class TrackFitterUKFBase { static constexpr int32_t SIGMA_DIM_A{2 * DIM_A + 1}; ///< @brief Sigma points dimension for augmented state Matrix m_matSigmaXa{Matrix::Zero()}; ///< @brief Sigma points matrix + int nTouch = 0; // Variable to track the number of times a matrix has a floor added. protected: Vector m_vecX{Vector::Zero()}; /// @brief estimated state vector Matrix m_matP{Matrix::Zero()}; /// @brief state covariance matrix @@ -163,7 +164,12 @@ class TrackFitterUKFBase { // Calculate the weighted mean and covariance of the sigma points for the state vector. // This will be the new state vector and covariance matrix. - calculateWeightedMeanAndCovariance(sigmaXx, m_vecX, m_matP); + calculateWeightedMeanAndCovariance(sigmaXx, m_vecX, m_matP, "P-"); + m_matP(0, 0) += 1e-4; + m_matP(1, 1) += 1e-4; + m_matP(2, 2) += 1e-4; + ensurePD(m_matP); // Ensure the covariance matrix is positive definite + logEigen("P-Post", m_matP, 0); } /** @@ -197,10 +203,11 @@ class TrackFitterUKFBase { // from the sigma points. Vector vecZhat; // Predicted measurement vector Matrix matPzz; // Measurement covariance matrix - calculateWeightedMeanAndCovariance(sigmaZ, vecZhat, matPzz); + calculateWeightedMeanAndCovariance(sigmaZ, vecZhat, matPzz, "SnoR"); // Add in the measurement noise covariance matrix to the measurement covariance matrix. matPzz += m_matR; // Add measurement noise covariance so we gen the innovation covariance matrix. + logEigen("S", matPzz, 0); ensurePD(matPzz); // Ensure the covariance matrix is positive definite const Matrix matPxz{calculateCrossCorrelation(sigmaXx, m_vecX, sigmaZ, vecZhat)}; @@ -217,6 +224,18 @@ class TrackFitterUKFBase { ensurePD(m_matP); // Ensure the covariance matrix is positive definite } + template + void logEigen(std::string tag, const T &P, int k) + { + Eigen::SelfAdjointEigenSolver es(P); + double lmin = es.eigenvalues().minCoeff(); + double lmax = es.eigenvalues().maxCoeff(); + double cond = lmax / lmin; + + LOG(info) << "k: " << k << " " << tag << " Eval: min = " << lmin << ", max = " << lmax + << ", condition number = " << cond; + } + protected: /** * @brief Add state vector and state covariance matrix to the augmented state vector covariance matrix. @@ -295,6 +314,7 @@ class TrackFitterUKFBase { matP += Matrix::Identity() * std::pow(10, -6 + i); // Regularization value lltOfP.compute(matP); i = i + 1; + nTouch++; // Check if the regularized matrix is now positive definite } if (lltOfP.info() != Eigen::Success) { @@ -367,7 +387,7 @@ class TrackFitterUKFBase { */ template void calculateWeightedMeanAndCovariance(const Matrix &sigmaX, Vector &vecX, - Matrix &matPxx) + Matrix &matPxx, std::string tag = "") { // 1. calculate mean of the sigma points vecX = m_weightM0 * util::getColumnAt(0, sigmaX); @@ -390,7 +410,8 @@ class TrackFitterUKFBase { matPxx += Pi; // y += W[0, i] (Y[:, i] - \bar{y}) (Y[:, i] - \bar{y})^T } - ensurePD(matPxx); // Ensure the covariance matrix is positive definite + logEigen(tag, matPxx, 0); // Log the eigenvalues of the covariance matrix + // ensurePD(matPxx); // Ensure the covariance matrix is positive definite } /** diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index 1c0ebf82e..32c09d472 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -12,7 +12,7 @@ const double charge_p = 1.602176634e-19; // Charge of proton // Simulated (measurement) hits std::vector x, y, z, Eloss; -int pointsToCluster = 20; +int pointsToCluster = 5; void LoadHits() { std::ifstream infile("hits.txt"); @@ -99,7 +99,7 @@ void UKFSingleTrack() double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree ukf.fEnableEnStraggling = true; // Enable energy straggling - ukf.setParameters(1e-1, 2, 0); // alpha, beta, kappa + ukf.setParameters(1e-2, 2, 0); // alpha, beta, kappa TMatrixD cov(6, 6); cov.Zero(); @@ -141,6 +141,19 @@ void UKFSingleTrack() ukf.predictUKF(point); auto augState = ukf.GetAugStateVector(); auto augCov = ukf.GetAugStateCovariance(); + auto covP = ukf.matP(); + + if (i == 1) { + LOG(info) << "P-: " << std::endl << covP; + Eigen::SelfAdjointEigenSolver es(covP); // float version + float λmin = es.eigenvalues()(0); + auto vmin = es.eigenvectors().col(0); // length-6 + + std::printf("λmin = %.3e eigvec = [", λmin); + for (int i = 0; i < 6; ++i) + std::printf(" %.2e", vmin(i)); + std::printf(" ]\n"); + } ukf.correctUKF(point); auto state = ukf.GetStateVector(); @@ -171,6 +184,8 @@ void UKFSingleTrack() residual.push_back(residualValue); // Store the residual for this hit } + LOG(info) << "After forward pass " << ukf.nTouch << " touches."; + // At this point we have the full trajectory of the particle ukf.smoothUKF(); // Perform smoothing auto smoothedStates = ukf.GetSmoothedStates(); From 596d81b435fd8021c64b38bd7e71b1e922e3e609 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 30 Jul 2025 08:43:57 -0400 Subject: [PATCH 65/75] Add controllable diagnostics --- .../AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index ba728cde3..fc59e26dc 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -49,7 +49,10 @@ class TrackFitterUKFBase { static constexpr int32_t SIGMA_DIM_A{2 * DIM_A + 1}; ///< @brief Sigma points dimension for augmented state Matrix m_matSigmaXa{Matrix::Zero()}; ///< @brief Sigma points matrix - int nTouch = 0; // Variable to track the number of times a matrix has a floor added. + // Controls and variables for running numerical diagnostics. + int nTouch{0}; // Variable to track the number of times a matrix has a floor added. + bool kLogEigen{false}; + protected: Vector m_vecX{Vector::Zero()}; /// @brief estimated state vector Matrix m_matP{Matrix::Zero()}; /// @brief state covariance matrix @@ -227,6 +230,9 @@ class TrackFitterUKFBase { template void logEigen(std::string tag, const T &P, int k) { + if (!kLogEigen) { + return; // If logging is disabled, do not log the eigenvalues. + } Eigen::SelfAdjointEigenSolver es(P); double lmin = es.eigenvalues().minCoeff(); double lmax = es.eigenvalues().maxCoeff(); From f14be203b9aa0bb8f132110ad51ff396a26c1823 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 30 Jul 2025 09:24:02 -0400 Subject: [PATCH 66/75] Rename noises --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 2 +- .../OpenKF/kalman_filter/TrackFitterUKF.h | 27 ++++++++++++++----- .../kalman_filter/TrackFitterUKFTest.cxx | 8 +++--- AtReconstruction/AtFitter/OpenKF/types.h | 2 +- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index 62fe27ba2..ffa1bde15 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -15,7 +15,7 @@ void TrackFitterUKF::Reset() m_matP.setZero(); m_vecXa.setZero(); m_matPa.setZero(); - m_matQ.setZero(); + m_matQaug.setZero(); m_matR.setZero(); m_matSigmaXa.setZero(); m_matSigmaXPred.setZero(); diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index fc59e26dc..a4508e876 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -32,6 +32,11 @@ namespace kf { * the machinery that underlies the UKF formalism. It is a modified version of the UKF provided * by OpenKF, that has been expanded to allow for more hooks into the method. * + * Introduces two different types of process noise. We have augmented process noise and + * model process noise. Augmented process noise acts during the propagator, model process + * noise can be used to describe noise in the propagation itself and allows for accounting + * for uncertainty that may arise from unmodeled dynamics. + * * Templated because I believe Eigen can do quite a bit of operation for small matrices like we have here if * the size is known at compile time. Worth checking that though. * @@ -63,8 +68,11 @@ class TrackFitterUKFBase { Matrix m_matPa{Matrix::Zero()}; // Process and measurement noise covariance matrices - /// Process noise covariance matrix (Q) - Matrix m_matQ{Matrix::Zero()}; + /// Process noise covariance matrix incorporated in propagator (Q_aug) + Matrix m_matQaug{Matrix::Zero()}; + /// Process noise covariance matrix for model noise (Q_mod) + Matrix m_matQmod{Matrix::Zero()}; + /// Measurement noise covariance matrix (R) Matrix m_matR{Matrix::Zero()}; @@ -102,11 +110,16 @@ class TrackFitterUKFBase { /** * @brief Set process noise covariance Q to be used in the prediction step. */ - void setCovarianceQ(const Matrix &matQ) { m_matQ = matQ; } + void setAugmentNoise(const Matrix &matQ) { m_matQaug = matQ; } + /** + * @brief Set the process noise covariance Q_mod to be used in the prediction step. + */ + void setModelNoise(const Matrix &matQmod) { m_matQmod = matQmod; } + /** * @brief Set the measurement noise covariance R to be used in the update step. */ - void setCovarianceR(const Matrix &matR) { m_matR = matR; } + void SetMeasurementNoise(const Matrix &matR) { m_matR = matR; } virtual Vector &vecX() { return m_vecX; } virtual const Vector &vecX() const { return m_vecX; } @@ -263,12 +276,12 @@ class TrackFitterUKFBase { virtual std::array calculateProcessNoiseMean() { return std::array{0}; } - virtual Matrix calculateProcessNoiseCovariance() { return m_matQ; } + virtual Matrix calculateProcessNoiseCovariance() { return m_matQaug; } void updateAugWithProcessNoise() { auto processNoiseMean = calculateProcessNoiseMean(); - m_matQ = calculateProcessNoiseCovariance(); + m_matQaug = calculateProcessNoiseCovariance(); // Add the mean process noise to the augmented state vector for (int32_t i{0}; i < DIM_V; ++i) { @@ -281,7 +294,7 @@ class TrackFitterUKFBase { for (int32_t i{S_IDX}; i < L_IDX; ++i) { for (int32_t j{S_IDX}; j < L_IDX; ++j) { - m_matPa(i, j) = m_matQ(i - S_IDX, j - S_IDX); + m_matPa(i, j) = m_matQaug(i - S_IDX, j - S_IDX); } } } diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx index 8ce7dfc3d..393cdca24 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKFTest.cxx @@ -75,8 +75,8 @@ TEST_F(TrackFitterUKFExampleTest, Prediction) m_ukf.vecX() = x; m_ukf.matP() = P; - m_ukf.setCovarianceQ(Q); - m_ukf.setCovarianceR(R); + m_ukf.setAugmentNoise(Q); + m_ukf.SetMeasurementNoise(R); m_ukf.setParameters(1, 0, 3 - m_ukf.DIM_A); // Set kappa to match the original implementation m_ukf.predictUKF(funcF, z); @@ -137,8 +137,8 @@ TEST_F(TrackFitterUKFExampleTest, PredictionAndCorrection) m_ukf.vecX() = x; m_ukf.matP() = P; - m_ukf.setCovarianceQ(Q); - m_ukf.setCovarianceR(R); + m_ukf.setAugmentNoise(Q); + m_ukf.SetMeasurementNoise(R); m_ukf.setParameters(1, 0, 3 - m_ukf.DIM_A); // Set kappa to match the original implementation m_ukf.predictUKF(funcF, z); diff --git a/AtReconstruction/AtFitter/OpenKF/types.h b/AtReconstruction/AtFitter/OpenKF/types.h index 215f8fc79..4111fb8dc 100644 --- a/AtReconstruction/AtFitter/OpenKF/types.h +++ b/AtReconstruction/AtFitter/OpenKF/types.h @@ -17,7 +17,7 @@ #include namespace kf { -using float32_t = float; +using float32_t = double; template using Matrix = Eigen::Matrix; From e94e3f0e292498c6978f1888d0157a50f4ed338b Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 30 Jul 2025 09:55:33 -0400 Subject: [PATCH 67/75] More refactoring --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 17 +---- .../OpenKF/kalman_filter/TrackFitterUKF.h | 70 +++++++++++-------- macro/tests/UKF/UKFSingleTrack.C | 2 +- 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index ffa1bde15..cd5444f73 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -8,24 +8,11 @@ #include namespace kf { + void TrackFitterUKF::Reset() { // Reset the state vector and covariance matrix - m_vecX.setZero(); - m_matP.setZero(); - m_vecXa.setZero(); - m_matPa.setZero(); - m_matQaug.setZero(); - m_matR.setZero(); - m_matSigmaXa.setZero(); - m_matSigmaXPred.setZero(); - - // Clear the history vectors - m_vecXPredHist.clear(); - m_matPPredHist.clear(); - m_matCPredHist.clear(); - m_vecXHist.clear(); - m_matPHist.clear(); + TrackFitterUKFBase::Reset(); m_vecXSmooth.clear(); m_matPSmooth.clear(); fMeanStep = AtTools::AtPropagator::StepState(); // Reset the step state diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index a4508e876..a86f89b06 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -59,6 +59,9 @@ class TrackFitterUKFBase { bool kLogEigen{false}; protected: + using VectorEigenVecDimX = std::vector, Eigen::aligned_allocator>>; + using VectorEigenMatDimX = std::vector, Eigen::aligned_allocator>>; + Vector m_vecX{Vector::Zero()}; /// @brief estimated state vector Matrix m_matP{Matrix::Zero()}; /// @brief state covariance matrix @@ -67,12 +70,10 @@ class TrackFitterUKFBase { /// @brief augmented state covariance (incl. process and measurement noise covariances) Matrix m_matPa{Matrix::Zero()}; - // Process and measurement noise covariance matrices /// Process noise covariance matrix incorporated in propagator (Q_aug) Matrix m_matQaug{Matrix::Zero()}; /// Process noise covariance matrix for model noise (Q_mod) Matrix m_matQmod{Matrix::Zero()}; - /// Measurement noise covariance matrix (R) Matrix m_matR{Matrix::Zero()}; @@ -85,10 +86,42 @@ class TrackFitterUKFBase { /// Lambda parameter for sigma point calculation float32_t m_lambda{0}; + // Track state over time (can be used for smoothing or diagnostics) + VectorEigenVecDimX m_vecXPredHist; /// @brief History of predicted state vectors at k+1 + VectorEigenMatDimX m_matPPredHist; /// @brief History of predicted state covariances at k+1 + /// History of cross correlation between filtered state at k and predicted at k+1 + VectorEigenMatDimX m_matCPredHist; + /// History of filtered (after correction) state vectors at k + VectorEigenVecDimX m_vecXHist; + /// History of filtered (after correction) state covariances at k + VectorEigenMatDimX m_matPHist; + /// The sigma points after propagation for the last prediction step. + Matrix m_matSigmaXPred{Matrix::Zero()}; + public: TrackFitterUKFBase() { setParameters(1, 2, 0); } ~TrackFitterUKFBase() = default; + virtual void Reset() + { + m_vecX.setZero(); + m_matP.setZero(); + m_vecXa.setZero(); + m_matPa.setZero(); + m_matQaug.setZero(); + m_matQmod.setZero(); + m_matR.setZero(); + m_matSigmaXa.setZero(); + m_matSigmaXPred.setZero(); + + // Clear the history vectors + m_vecXPredHist.clear(); + m_matPPredHist.clear(); + m_matCPredHist.clear(); + m_vecXHist.clear(); + m_matPHist.clear(); + } + /** * @brief Set the weights used to calculate sigma points. */ @@ -136,7 +169,7 @@ class TrackFitterUKFBase { { updateAugWithState(); updateAugWithProcessNoise(); - ensurePD(m_matPa); // Ensure the augmented covariance matrix is positive definite + // ensurePD(m_matPa); // Ensure the augmented covariance matrix is positive definite } /** @@ -478,34 +511,18 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { static constexpr int32_t TF_DIM_Z = 3; static constexpr int32_t TF_DIM_V = 1; static constexpr int32_t TF_DIM_N = 3; + // using DIM_X = TrackFitterUKFBase::DIM_X; AtTools::AtPropagator fPropagator; ///< @brief Propagator for the track fitter std::unique_ptr fStepper{nullptr}; AtTools::AtPropagator::StepState fMeanStep; /// Holds the step information for POCA propagation of mean state ROOT::Math::Plane3D fMeasurementPlane; ///< Holds the measurement plane for the track fitter - using EigenVectorDimX = std::vector, Eigen::aligned_allocator>>; - using VectorEigenMatDimX = - std::vector, Eigen::aligned_allocator>>; - - // vectors to hold the information needed for smoothing the UKF - EigenVectorDimX m_vecXPredHist; /// @brief History of predicted state vectors at k+1 - VectorEigenMatDimX m_matPPredHist; /// @brief History of predicted state covariances at k+1 - /// History of cross correlation between filtered state at k and predicted at k+1 - VectorEigenMatDimX m_matCPredHist; - /// History of filtered (after correction) state vectors at k - EigenVectorDimX m_vecXHist; - /// History of filtered (after correction) state covariances at k - VectorEigenMatDimX m_matPHist; - /// Smoothed state vector and covariance - EigenVectorDimX m_vecXSmooth; + VectorEigenVecDimX m_vecXSmooth; /// Smoothed state covariance VectorEigenMatDimX m_matPSmooth; - /// The sigma points after propagation for the last prediction step. - Matrix m_matSigmaXPred{Matrix::Zero()}; - public: bool fEnableEnStraggling{true}; ///< @brief Flag to enable/disable energy straggling double fMaxStragglingFactor{1. / 3.}; ///< @brief Maximum straggling factor for energy loss @@ -524,21 +541,18 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { : TrackFitterUKFBase(), fPropagator(std::move(propagator)), fStepper(std::move(stepper)) { } - void Reset(); + virtual void Reset() override; void SetInitialState(const ROOT::Math::XYZPoint &initialPosition, const ROOT::Math::XYZVector &initialMomentum, const TMatrixD &initialCovariance); void SetMeasCov(const TMatrixD &measCov); - std::array GetStateVector() const - { - return {m_vecX[0], m_vecX[1], m_vecX[2], m_vecX[3], m_vecX[4], m_vecX[5]}; - } + TMatrixD GetStateCovariance() const; TMatrixD GetAugStateCovariance() const; std::array GetAugStateVector() const; - const EigenVectorDimX &GetSmoothedStates() const { return m_vecXSmooth; }; + const VectorEigenVecDimX &GetSmoothedStates() const { return m_vecXSmooth; }; const VectorEigenMatDimX &GetSmoothedCovariances() const { return m_matPSmooth; }; - const EigenVectorDimX &GetFilteredStates() const { return m_vecXHist; }; + const VectorEigenVecDimX &GetFilteredStates() const { return m_vecXHist; }; const VectorEigenMatDimX &GetFilteredCovariances() const { return m_matPHist; }; void predictUKF(const ROOT::Math::XYZPoint &z); diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index 32c09d472..5be40ab4c 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -99,7 +99,7 @@ void UKFSingleTrack() double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree ukf.fEnableEnStraggling = true; // Enable energy straggling - ukf.setParameters(1e-2, 2, 0); // alpha, beta, kappa + ukf.setParameters(1e-3, 2, 0); // alpha, beta, kappa TMatrixD cov(6, 6); cov.Zero(); From ce22ed31d776e9348f9f12d3fa003234537b676c Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 30 Jul 2025 10:12:59 -0400 Subject: [PATCH 68/75] More refactoring --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 49 +++++-------------- .../OpenKF/kalman_filter/TrackFitterUKF.h | 45 ++++++++++++----- macro/tests/UKF/UKFSingleTrack.C | 4 +- 3 files changed, 47 insertions(+), 51 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index cd5444f73..13dddea8e 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -15,6 +15,7 @@ void TrackFitterUKF::Reset() TrackFitterUKFBase::Reset(); m_vecXSmooth.clear(); m_matPSmooth.clear(); + m_matCPredHist.clear(); fMeanStep = AtTools::AtPropagator::StepState(); // Reset the step state } @@ -39,8 +40,8 @@ void TrackFitterUKF::SetInitialState(const ROOT::Math::XYZPoint &initialPosition } // Save the initial state in our history vectors - m_vecXHist.push_back(m_vecX); - m_matPHist.push_back(m_matP); + m_vecXFiltHist.push_back(m_vecX); + m_matPFiltHist.push_back(m_matP); m_vecXPredHist.push_back(m_vecX); m_matPPredHist.push_back(m_matP); m_matCPredHist.push_back(Matrix::Zero()); // Cross-correlation is not defined for the first point @@ -170,15 +171,11 @@ Vector TrackFitterUKF::funcF(const Vector TrackFitterUKF::funcH(const Vector &x) { - // Measurement function to convert state vector to measurement vector - using namespace ROOT::Math; Vector vecZ; - XYZPoint fPos(x[0], x[1], x[2]); // Position from state vector - // Calculate the measurement vector based on the position and momentum - vecZ[0] = fPos.X(); // X coordinate - vecZ[1] = fPos.Y(); // Y coordinate - vecZ[2] = fPos.Z(); // Z coordinate + vecZ[0] = x[0]; // X coordinate + vecZ[1] = x[1]; // Y coordinate + vecZ[2] = x[2]; // Z coordinate return vecZ; // Return the measurement vector } @@ -212,30 +209,14 @@ void TrackFitterUKF::predictUKF(const ROOT::Math::XYZPoint &z) auto callback = [this](const kf::Vector &x_, const kf::Vector &v_, const kf::Vector &z_) { return funcF(x_, v_, z_); }; - updateAugmentedStateAndCovariance(); - - // Calculate the sigma points for the augmented state vector and save in a matrix where each column is a sigma - // point. - auto sigmaPoints = calculateSigmaPoints(m_vecXa, m_matPa); - - // Pull out the sigma points for the state vector and process noise in two different matrices. - Matrix sigmaXxPrior{ - sigmaPoints.block(0, 0, TF_DIM_X, SIGMA_DIM_A)}; // Sigma points for state vector - m_matSigmaXPred = sigmaXxPrior; // Store the sigma points for the state vector - TrackFitterUKFBase::predictUKF(callback, zVec); - // logEigen("State P-", m_matP, m_matPPredHist.size()); - - // Now we need to store the predicted state and covariance for smoothing later. - m_vecXPredHist.push_back(m_vecX); // Store the predicted state vector - m_matPPredHist.push_back(m_matP); // Store the predicted covariance matrix - // Get the sigma points belonging to the predicted state Matrix sigmaXx{m_matSigmaXa.block(0, 0, TF_DIM_X, SIGMA_DIM_A)}; // Calculate the cross-corelation between the filtered state at k and predicted state at k+1 - auto matCPred = calculateCrossCorrelation(sigmaXxPrior, m_vecXHist.back(), sigmaXx, m_vecXPredHist.back()); + auto matCPred = + calculateCrossCorrelation(m_matSigmaXPred, m_vecXFiltHist.back(), sigmaXx, m_vecXPredHist.back()); m_matCPredHist.push_back(matCPred); // Store the cross-correlation matrix } @@ -248,11 +229,7 @@ void TrackFitterUKF::correctUKF(const ROOT::Math::XYZPoint &z) auto callback = [this](const kf::Vector &x_) { return funcH(x_); }; TrackFitterUKFBase::correctUKF(callback, zVec); - // logEigen("State PCorr", m_matP, m_matPHist.size()); - - // After correction we need to save the filtered state - m_vecXHist.push_back(m_vecX); // Store the filtered state vector - m_matPHist.push_back(m_matP); // Store the filtered covariance matrix + // logEigen("State PCorr", m_matP, m_matPCorrHist.size()); } void TrackFitterUKF::smoothUKF() @@ -262,8 +239,8 @@ void TrackFitterUKF::smoothUKF() // Here i = k+1 m_vecXSmooth.resize(m_vecXPredHist.size()); m_matPSmooth.resize(m_matPPredHist.size()); - m_vecXSmooth.back() = m_vecXHist.back(); // The last smoothed state is the last corrected state - m_matPSmooth.back() = m_matPHist.back(); // The last smoothed covariance is the last corrected covariance + m_vecXSmooth.back() = m_vecXFiltHist.back(); // The last smoothed state is the last corrected state + m_matPSmooth.back() = m_matPFiltHist.back(); // The last smoothed covariance is the last corrected covariance for (size_t i = m_vecXPredHist.size() - 1; i > 0; --i) { LOG(debug) << "Smoothing step " << i << " of " << m_vecXPredHist.size() - 1; @@ -273,8 +250,8 @@ void TrackFitterUKF::smoothUKF() const auto &ccor = m_matCPredHist[i]; // C_{k+1} // Get the filtered state and covariance at step i-1 - const auto &xFilt = m_vecXHist[i - 1]; // m_{k} - const auto &pFilt = m_matPHist[i - 1]; // P_{k} + const auto &xFilt = m_vecXFiltHist[i - 1]; // m_{k} + const auto &pFilt = m_matPFiltHist[i - 1]; // P_{k} // Get the smoothed state and covariance at step i auto &xSmooth = m_vecXSmooth[i]; // m^s_{k+1} diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index a86f89b06..f544f44f8 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -60,6 +60,7 @@ class TrackFitterUKFBase { protected: using VectorEigenVecDimX = std::vector, Eigen::aligned_allocator>>; + using VectorEigenVecDimZ = std::vector, Eigen::aligned_allocator>>; using VectorEigenMatDimX = std::vector, Eigen::aligned_allocator>>; Vector m_vecX{Vector::Zero()}; /// @brief estimated state vector @@ -89,12 +90,12 @@ class TrackFitterUKFBase { // Track state over time (can be used for smoothing or diagnostics) VectorEigenVecDimX m_vecXPredHist; /// @brief History of predicted state vectors at k+1 VectorEigenMatDimX m_matPPredHist; /// @brief History of predicted state covariances at k+1 - /// History of cross correlation between filtered state at k and predicted at k+1 - VectorEigenMatDimX m_matCPredHist; /// History of filtered (after correction) state vectors at k - VectorEigenVecDimX m_vecXHist; + VectorEigenVecDimX m_vecXFiltHist; /// History of filtered (after correction) state covariances at k - VectorEigenMatDimX m_matPHist; + VectorEigenMatDimX m_matPFiltHist; + /// History of measurement points + VectorEigenVecDimZ m_vecZHist; /// The sigma points after propagation for the last prediction step. Matrix m_matSigmaXPred{Matrix::Zero()}; @@ -117,9 +118,8 @@ class TrackFitterUKFBase { // Clear the history vectors m_vecXPredHist.clear(); m_matPPredHist.clear(); - m_matCPredHist.clear(); - m_vecXHist.clear(); - m_matPHist.clear(); + m_vecXFiltHist.clear(); + m_matPFiltHist.clear(); } /** @@ -158,6 +158,10 @@ class TrackFitterUKFBase { virtual Matrix &matP() { return m_matP; } virtual const Matrix &matP() const { return m_matP; } + const VectorEigenVecDimX &GetFilteredStates() const { return m_vecXFiltHist; } + const VectorEigenMatDimX &GetFilteredCovariances() const { return m_matPFiltHist; } + const VectorEigenVecDimX &GetPredictedStates() const { return m_vecXPredHist; } + const VectorEigenMatDimX &GetPredictedCovariances() const { return m_matPPredHist; } /** * @brief update the augmented state vector and covariance matrix @@ -192,6 +196,9 @@ class TrackFitterUKFBase { // point. m_matSigmaXa = calculateSigmaPoints(m_vecXa, m_matPa); + // Pull out the sigma points for the state vector + m_matSigmaXPred = m_matSigmaXa.block(0, 0, DIM_X, SIGMA_DIM_A); + // Pull out the sigma points for the state vector and process noise in two different matrices. Matrix sigmaXx{m_matSigmaXa.block(0, 0, DIM_X, SIGMA_DIM_A)}; // Sigma points for state vector Matrix sigmaXv{ @@ -213,11 +220,20 @@ class TrackFitterUKFBase { // Calculate the weighted mean and covariance of the sigma points for the state vector. // This will be the new state vector and covariance matrix. - calculateWeightedMeanAndCovariance(sigmaXx, m_vecX, m_matP, "P-"); + calculateWeightedMeanAndCovariance(sigmaXx, m_vecX, m_matP); + logEigen("P-", m_matP, 0); // Log the eigenvalues of the covariance matrix + + // Here we add process noise m_matP(0, 0) += 1e-4; m_matP(1, 1) += 1e-4; m_matP(2, 2) += 1e-4; + ensurePD(m_matP); // Ensure the covariance matrix is positive definite + + // Now we need to store the predicted state and covariance for smoothing later. + m_vecXPredHist.push_back(m_vecX); // Store the predicted state vector + m_matPPredHist.push_back(m_matP); // Store the predicted covariance matrix + logEigen("P-Post", m_matP, 0); } @@ -252,7 +268,7 @@ class TrackFitterUKFBase { // from the sigma points. Vector vecZhat; // Predicted measurement vector Matrix matPzz; // Measurement covariance matrix - calculateWeightedMeanAndCovariance(sigmaZ, vecZhat, matPzz, "SnoR"); + calculateWeightedMeanAndCovariance(sigmaZ, vecZhat, matPzz); // Add in the measurement noise covariance matrix to the measurement covariance matrix. matPzz += m_matR; // Add measurement noise covariance so we gen the innovation covariance matrix. @@ -271,6 +287,10 @@ class TrackFitterUKFBase { m_matP -= matK * matPzz * matK.transpose(); // m_matP -= matPxz * matK.transpose(); ensurePD(m_matP); // Ensure the covariance matrix is positive definite + + // After correction we need to save the filtered state + m_vecXFiltHist.push_back(m_vecX); // Store the filtered state vector + m_matPFiltHist.push_back(m_matP); // Store the filtered covariance matrix } template @@ -439,7 +459,7 @@ class TrackFitterUKFBase { */ template void calculateWeightedMeanAndCovariance(const Matrix &sigmaX, Vector &vecX, - Matrix &matPxx, std::string tag = "") + Matrix &matPxx) { // 1. calculate mean of the sigma points vecX = m_weightM0 * util::getColumnAt(0, sigmaX); @@ -462,7 +482,6 @@ class TrackFitterUKFBase { matPxx += Pi; // y += W[0, i] (Y[:, i] - \bar{y}) (Y[:, i] - \bar{y})^T } - logEigen(tag, matPxx, 0); // Log the eigenvalues of the covariance matrix // ensurePD(matPxx); // Ensure the covariance matrix is positive definite } @@ -522,6 +541,8 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { VectorEigenVecDimX m_vecXSmooth; /// Smoothed state covariance VectorEigenMatDimX m_matPSmooth; + /// History of cross correlation between filtered state at k and predicted at k+1 + VectorEigenMatDimX m_matCPredHist; public: bool fEnableEnStraggling{true}; ///< @brief Flag to enable/disable energy straggling @@ -552,8 +573,6 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { std::array GetAugStateVector() const; const VectorEigenVecDimX &GetSmoothedStates() const { return m_vecXSmooth; }; const VectorEigenMatDimX &GetSmoothedCovariances() const { return m_matPSmooth; }; - const VectorEigenVecDimX &GetFilteredStates() const { return m_vecXHist; }; - const VectorEigenMatDimX &GetFilteredCovariances() const { return m_matPHist; }; void predictUKF(const ROOT::Math::XYZPoint &z); void correctUKF(const ROOT::Math::XYZPoint &z); diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index 5be40ab4c..99886f9ac 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -156,8 +156,8 @@ void UKFSingleTrack() } ukf.correctUKF(point); - auto state = ukf.GetStateVector(); - auto cov = ukf.GetStateCovariance(); + auto state = ukf.vecX(); + auto cov = ukf.matP(); ROOT::Math::XYZPoint pos(state[0], state[1], state[2]); ROOT::Math::Polar3DVector momPolar(state[3], state[4], state[5]); From 75ab27232e0494b0e8ffdfa7ee4d83a06477e7b9 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 30 Jul 2025 11:02:18 -0400 Subject: [PATCH 69/75] Remove inverse calls --- .../AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx | 4 ++-- .../AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index 13dddea8e..f006798af 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -258,8 +258,8 @@ void TrackFitterUKF::smoothUKF() auto &pSmooth = m_matPSmooth[i]; // P^s_{k+1} auto llt = calculateCholesky(pPred); - // auto D = ccor * llt.solve(Matrix::Identity()); - auto D = ccor * pPred.inverse(); // D = C_{k+1} * (P_{k+1}^-)^{-1} + auto D = ccor * llt.solve(Matrix::Identity()); + // auto D = ccor * pPred.inverse(); // D = C_{k+1} * (P_{k+1}^-)^{-1} // std::cout << "D matrix at step " << i << ":\n" << D << "\n"; m_vecXSmooth[i - 1] = xFilt + D * (xSmooth - xPred); // m^s_{k} = m_{k} + D * (m^s_{k+1} - m_{k+1}^-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index f544f44f8..05ce1b95a 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -279,9 +279,9 @@ class TrackFitterUKFBase { // kalman gain auto llt = calculateCholesky(matPzz); - // const Matrix matK = llt.solve(matPxz.transpose()).transpose(); - Matrix matK = {matPxz * llt.solve(Matrix::Identity())}; - // Matrix matK = matPxz * matPzz.inverse(); By far the worst method for filter stability + const Matrix matK = llt.solve(matPxz.transpose()).transpose(); + // Matrix matK = {matPxz * llt.solve(Matrix::Identity())}; + // Matrix matK = matPxz * matPzz.inverse(); By far the worst method for filter stability m_vecX += matK * (vecZ - vecZhat); m_matP -= matK * matPzz * matK.transpose(); @@ -530,7 +530,6 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { static constexpr int32_t TF_DIM_Z = 3; static constexpr int32_t TF_DIM_V = 1; static constexpr int32_t TF_DIM_N = 3; - // using DIM_X = TrackFitterUKFBase::DIM_X; AtTools::AtPropagator fPropagator; ///< @brief Propagator for the track fitter std::unique_ptr fStepper{nullptr}; From 4caa6010ff6e87ff845b749abd29d375ff609712 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 30 Jul 2025 11:19:28 -0400 Subject: [PATCH 70/75] Incorporate model noise --- .../OpenKF/kalman_filter/TrackFitterUKF.cxx | 4 ++++ .../OpenKF/kalman_filter/TrackFitterUKF.h | 16 +++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx index f006798af..6ee181b29 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.cxx @@ -39,6 +39,10 @@ void TrackFitterUKF::SetInitialState(const ROOT::Math::XYZPoint &initialPosition } } + for (int i = 0; i < m_matQmod.rows(); ++i) { + m_matQmod(i, i) = fPosModelNoise; // Initialize model noise covariance to zero + } + // Save the initial state in our history vectors m_vecXFiltHist.push_back(m_vecX); m_matPFiltHist.push_back(m_matP); diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index 05ce1b95a..b48b90852 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -222,13 +222,8 @@ class TrackFitterUKFBase { // This will be the new state vector and covariance matrix. calculateWeightedMeanAndCovariance(sigmaXx, m_vecX, m_matP); logEigen("P-", m_matP, 0); // Log the eigenvalues of the covariance matrix - - // Here we add process noise - m_matP(0, 0) += 1e-4; - m_matP(1, 1) += 1e-4; - m_matP(2, 2) += 1e-4; - - ensurePD(m_matP); // Ensure the covariance matrix is positive definite + m_matP += m_matQmod; // Add the model process noise covariance to the state covariance matrix. + ensurePD(m_matP); // Ensure the covariance matrix is positive definite // Now we need to store the predicted state and covariance for smoothing later. m_vecXPredHist.push_back(m_vecX); // Store the predicted state vector @@ -546,6 +541,13 @@ class TrackFitterUKF : public TrackFitterUKFBase<6, 3, 1, 3> { public: bool fEnableEnStraggling{true}; ///< @brief Flag to enable/disable energy straggling double fMaxStragglingFactor{1. / 3.}; ///< @brief Maximum straggling factor for energy loss + /** + * Uncertainty in the position of the propagated track. Used to set a floor + * in the model error covariance. Physically this represents the unmodeled + * properties (error in RK4, etc.) Numerically is breaks the strong correlation that + * exists between x/y/z in the propagator when the track is basically straight. + */ + double fPosModelNoise{1e-4}; /** * @brief Constructor for the TrackFitterUKF class. From a0789379f2f2dbc684d52ddd7dbad10ca9df8b64 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 30 Jul 2025 11:50:33 -0400 Subject: [PATCH 71/75] Add more output to many track test macro --- macro/tests/UKF/TestManyTracks.C | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/macro/tests/UKF/TestManyTracks.C b/macro/tests/UKF/TestManyTracks.C index f025b74b1..7f7cf6760 100644 --- a/macro/tests/UKF/TestManyTracks.C +++ b/macro/tests/UKF/TestManyTracks.C @@ -15,6 +15,7 @@ const double charge_p = 1.602176634e-19; // Charge of proton // Vectors to store the simulated points to compare to std::vector x_sim, y_sim, z_sim, Eloss_sim; TH1F *hMom = nullptr; +TH1F *hMomSampled = nullptr; TH1F *hMomError = nullptr; TCanvas *c1 = new TCanvas(); TCanvas *c2 = new TCanvas(); @@ -22,7 +23,8 @@ TCanvas *c2 = new TCanvas(); // Parameters for model const int pointsToCluster = 5; const double sigma_pos = 1; // Position uncertainty of 10 mm -const double sigma_mom = 0.01; // Momentum uncertainty in percentage +const double sigma_mom = 0.1; // Momentum uncertainty in percentage +const double sigma_mom_sample = 0.05; // Sampled momentum uncertainty in percentage const double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree const double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree const double gasDensity = 3.553e-5; // g/cm^3 @@ -55,10 +57,12 @@ void TestManyTracks(int n, double bias = 0) XYZPoint fTruePos(-3.40046e-04, -1.49863e-04, 1.0018); XYZVector fTrueMom(0.00935463, -0.0454279, 0.00826042); fTrueMom *= 1e3; - double fSigmaMom = fTrueMom.R() * sigma_mom; + double fSigmaMom = fTrueMom.R() * sigma_mom_sample; hMom = new TH1F("hMom", "Reconstructed Momentum (MeV/c)", 100, fTrueMom.R() - 4 * fSigmaMom, fTrueMom.R() + 4 * fSigmaMom); + hMomSampled = new TH1F("hMom", "Reconstructed Momentum (MeV/c)", 100, fTrueMom.R() - 4 * fSigmaMom, + fTrueMom.R() + 4 * fSigmaMom); hMomError = new TH1F("hMomError", "Error (%)", 100, -4 * fSigmaMom / fTrueMom.R() * 100, 4 * fSigmaMom / fTrueMom.R() * 100); @@ -67,18 +71,14 @@ void TestManyTracks(int n, double bias = 0) if (i % 100 == 0) std::cout << "On iteration " << i << std::endl; - double pSampled = gRandom->Gaus(fTrueMom.R(), sigma_mom * fTrueMom.R()); + double pSampled = gRandom->Gaus(fTrueMom.R(), sigma_mom_sample * fTrueMom.R()); pSampled += bias; + hMomSampled->Fill(pSampled); // pSampled = fTrueMom.R(); ROOT::Math::Polar3DVector sampledMom(pSampled, fTrueMom.Theta(), fTrueMom.Phi()); - // try { SingleUKF(fTruePos, XYZVector(sampledMom), CalculateInitialCov(pSampled)); - //} catch (...) { - // std::cerr << "Failed to propagate iteration " << i << " with seed momentum " << pSampled << std::endl; - // continue; - //} auto filtState = ukf->GetFilteredStates()[0]; auto smoothState = ukf->GetSmoothedStates()[0]; @@ -98,6 +98,13 @@ void TestManyTracks(int n, double bias = 0) // Draw results c1->cd(); hMom->Draw("hist"); + hMomSampled->SetLineColor(kRed); + hMomSampled->Draw("same hist"); + + auto legend = new TLegend(0.65, 0.75, 0.88, 0.88); + legend->AddEntry(hMom, "Reconstructed", "l"); + legend->AddEntry(hMomSampled, "Sampled", "l"); + legend->Draw(); c2->cd(); hMomError->Draw("hist"); } From 99aed152acbb489caf9970117f15298796199f61 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Wed, 30 Jul 2025 15:20:35 -0400 Subject: [PATCH 72/75] Attempt fix for smoother (doesn't seem to work) --- .../AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h | 9 +++++++++ AtTools/AtPropagator.cxx | 3 +++ AtTools/AtPropagator.h | 5 +++-- macro/tests/UKF/TestManyTracks.C | 13 +++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h index b48b90852..7aeec0fa5 100644 --- a/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h +++ b/AtReconstruction/AtFitter/OpenKF/kalman_filter/TrackFitterUKF.h @@ -57,6 +57,7 @@ class TrackFitterUKFBase { // Controls and variables for running numerical diagnostics. int nTouch{0}; // Variable to track the number of times a matrix has a floor added. bool kLogEigen{false}; + bool kReplaceFirstCov{true}; /// If true, replace the seed COV with the predicted cov of the second point protected: using VectorEigenVecDimX = std::vector, Eigen::aligned_allocator>>; @@ -225,6 +226,14 @@ class TrackFitterUKFBase { m_matP += m_matQmod; // Add the model process noise covariance to the state covariance matrix. ensurePD(m_matP); // Ensure the covariance matrix is positive definite + if (m_vecXPredHist.size() == 1 && kReplaceFirstCov) { + // If this is the first prediction step, we replace the initial covariance with the predicted covariance + // of the second point. This is to avoid numerical issues with the first point on smoothing. + LOG(info) << "Replacing initial covariance with predicted covariance of second point."; + LOG(info) << "Initial COV: " << m_matPPredHist[0]; + m_matPPredHist[0] = m_matP; + LOG(info) << "Replaced COV: " << m_matPPredHist[0]; + } // Now we need to store the predicted state and covariance for smoothing later. m_vecXPredHist.push_back(m_vecX); // Store the predicted state vector m_matPPredHist.push_back(m_matP); // Store the predicted covariance matrix diff --git a/AtTools/AtPropagator.cxx b/AtTools/AtPropagator.cxx index 5838ac706..2a4e6050b 100644 --- a/AtTools/AtPropagator.cxx +++ b/AtTools/AtPropagator.cxx @@ -150,6 +150,7 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur result = stepper.Step(fState); if (!result) { LOG(error) << "Failed to propagate to stopping point, aborting."; + fState.status = AtPropagator::StepStateStatus::kStopped; return; // Abort propagation if step failed } auto origH = fState.h; // Save original step size @@ -202,6 +203,8 @@ void AtPropagator::PropagateToMeasurementSurface(const AtMeasurementSurface &sur if (reachedMeasurementPoint) { fState.fPos = surface.ProjectToSurface(fState.fPos); } + if (particleStopped || momentumReversed) + fState.status = AtPropagator::StepStateStatus::kStopped; LOG(debug) << "Projected on surface: " << fState.fPos.X() << ", " << fState.fPos.Y() << ", " << fState.fPos.Z(); LOG(debug) << "Final Momentum: " << fState.fMom.X() << ", " << fState.fMom.Y() << ", " << fState.fMom.Z(); diff --git a/AtTools/AtPropagator.h b/AtTools/AtPropagator.h index 6713e81fd..56c3ce947 100644 --- a/AtTools/AtPropagator.h +++ b/AtTools/AtPropagator.h @@ -28,8 +28,9 @@ class AtStepper; class AtPropagator { public: enum class StepStateStatus { - kSuccess, /// Step was successful - kInvalidStepSize /// Step failed + kSuccess, /// Step was successful + kInvalidStepSize, /// Step failed + kStopped /// Particle is stopped in material }; struct StepState { ROOT::Math::XYZPoint fPos; /// Position of the particle in mm diff --git a/macro/tests/UKF/TestManyTracks.C b/macro/tests/UKF/TestManyTracks.C index 7f7cf6760..b6bc57964 100644 --- a/macro/tests/UKF/TestManyTracks.C +++ b/macro/tests/UKF/TestManyTracks.C @@ -17,6 +17,8 @@ std::vector x_sim, y_sim, z_sim, Eloss_sim; TH1F *hMom = nullptr; TH1F *hMomSampled = nullptr; TH1F *hMomError = nullptr; +TH1F *hMomError2 = nullptr; + TCanvas *c1 = new TCanvas(); TCanvas *c2 = new TCanvas(); @@ -57,6 +59,10 @@ void TestManyTracks(int n, double bias = 0) XYZPoint fTruePos(-3.40046e-04, -1.49863e-04, 1.0018); XYZVector fTrueMom(0.00935463, -0.0454279, 0.00826042); fTrueMom *= 1e3; + double prevKE = AtTools::Kinematics::KE(fTrueMom.R(), mass_p) - Eloss_sim[0]; + double fMomPrev = AtTools::Kinematics::GetRelMomFromKE(prevKE, mass_p); + std::cout << "Initial mom: " << fTrueMom.R() << " past mom " << fMomPrev << std::endl; + double fSigmaMom = fTrueMom.R() * sigma_mom_sample; hMom = new TH1F("hMom", "Reconstructed Momentum (MeV/c)", 100, fTrueMom.R() - 4 * fSigmaMom, @@ -65,6 +71,8 @@ void TestManyTracks(int n, double bias = 0) fTrueMom.R() + 4 * fSigmaMom); hMomError = new TH1F("hMomError", "Error (%)", 100, -4 * fSigmaMom / fTrueMom.R() * 100, 4 * fSigmaMom / fTrueMom.R() * 100); + hMomError2 = + new TH1F("hMomError2", "Error (%)", 100, -4 * fSigmaMom / fTrueMom.R() * 100, 4 * fSigmaMom / fTrueMom.R() * 100); for (int i = 0; i < n; ++i) { @@ -87,6 +95,8 @@ void TestManyTracks(int n, double bias = 0) hMom->Fill(pReco); double error = (pReco - fTrueMom.R()) / fTrueMom.R() * 100; hMomError->Fill(error); + double errorPrev = (ukf->GetSmoothedStates()[1][3] - fMomPrev) / fMomPrev * 100; + hMomError2->Fill(errorPrev); std::cout << std::endl << std::endl @@ -105,8 +115,11 @@ void TestManyTracks(int n, double bias = 0) legend->AddEntry(hMom, "Reconstructed", "l"); legend->AddEntry(hMomSampled, "Sampled", "l"); legend->Draw(); + c2->cd(); hMomError->Draw("hist"); + hMomError2->SetLineColor(kRed); + hMomError2->Draw("same hist"); } /*********** Function implementations **************/ From cfc18a3011e706b133597d3331051d5ddefe69c3 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Tue, 9 Dec 2025 18:02:37 +0100 Subject: [PATCH 73/75] Add transform image plot --- scripts/pythonScripts/unscentedPlot.py | 209 +++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 scripts/pythonScripts/unscentedPlot.py diff --git a/scripts/pythonScripts/unscentedPlot.py b/scripts/pythonScripts/unscentedPlot.py new file mode 100644 index 000000000..17dc5b79f --- /dev/null +++ b/scripts/pythonScripts/unscentedPlot.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +""" +Colored remake of Figure 3 from: + + S. Julier & J. Uhlmann, "A New Extension of the Kalman Filter to Nonlinear + Systems", 1997. + +It compares: + - "True" mean/covariance via Monte Carlo + - Linearization / EKF-style transform + - Unscented Transform (UT) + +The example is the polar → Cartesian conversion of a noisy range/bearing +measurement to (x, y). +""" + +import math +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Ellipse + + +# --------------------------------------------------------------------------- +# Problem setup: range–bearing measurement of a target at (0, 1) +# --------------------------------------------------------------------------- + +# True Cartesian location +x_true = np.array([0.0, 1.0]) + +# Mean measurement in polar coordinates (r, theta) +mu_polar = np.array([1.0, math.pi / 2.0]) # r = 1 m, theta = 90 deg + +# Sensor noise: good range, poor bearing +sigma_r = 0.02 # 2 cm +sigma_theta = math.radians(15.0) # 15 degrees +P_polar = np.diag([sigma_r**2, sigma_theta**2]) + + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- + +def polar_to_cartesian(z: np.ndarray) -> np.ndarray: + """Nonlinear transform f: (r, theta) -> (x, y).""" + r, theta = z + return np.array([r * np.cos(theta), r * np.sin(theta)]) + + +def jacobian_polar_to_cartesian(r: float, theta: float) -> np.ndarray: + """Jacobian of f at (r, theta) for the EKF-style linearization.""" + return np.array([ + [np.cos(theta), -r * np.sin(theta)], + [np.sin(theta), r * np.cos(theta)], + ]) + + +def unscented_transform(mu: np.ndarray, + P: np.ndarray, + f, + kappa: float | None = None): + """ + Basic Julier & Uhlmann (1997) unscented transform for mean/covariance. + + mu : (n,) mean + P : (n,n) covariance + f : nonlinear mapping R^n -> R^m + kappa : scaling parameter; default enforces n + kappa = 3. + """ + mu = np.atleast_1d(mu) + n = mu.size + + if kappa is None: + # Heuristic from the paper: choose n + kappa = 3 + kappa = 3.0 - n + + lam = n + kappa + if lam <= 0: + raise ValueError("Require n + kappa > 0") + + # Sigma points (2n+1) with scaling + sqrtP = np.linalg.cholesky(lam * P) + sigma = np.zeros((2 * n + 1, n)) + sigma[0] = mu + for i in range(n): + sigma[1 + i] = mu + sqrtP[:, i] + sigma[1 + n + i] = mu - sqrtP[:, i] + + # Weights + Wm = np.full(2 * n + 1, 1.0 / (2.0 * lam)) + Wc = np.full(2 * n + 1, 1.0 / (2.0 * lam)) + Wm[0] = kappa / lam + Wc[0] = kappa / lam + + # Propagate through nonlinear map + Y = np.array([f(sp) for sp in sigma]) # (2n+1, m) + y_mean = np.sum(Wm[:, None] * Y, axis=0) + + diff = Y - y_mean + m = y_mean.size + y_cov = np.zeros((m, m)) + for i in range(2 * n + 1): + y_cov += Wc[i] * np.outer(diff[i], diff[i]) + + return y_mean, y_cov, sigma, Y + + +def plot_cov_ellipse(mean: np.ndarray, + cov: np.ndarray, + ax: plt.Axes, + n_std: float = 1.0, + **kwargs) -> Ellipse: + """ + Plot an n_std covariance ellipse for a 2×2 covariance matrix. + """ + vals, vecs = np.linalg.eigh(cov) + order = vals.argsort()[::-1] + vals, vecs = vals[order], vecs[:, order] + + # Ellipse geometry + width, height = 2.0 * n_std * np.sqrt(vals) + angle = math.degrees(math.atan2(vecs[1, 0], vecs[0, 0])) + + ell = Ellipse(xy=mean, width=width, height=height, angle=angle, **kwargs) + ax.add_patch(ell) + return ell + + +# --------------------------------------------------------------------------- +# Compute mean/covariance with three different methods +# --------------------------------------------------------------------------- + +print("Starting MC") +# 1. "True" via Monte Carlo +N_SAMPLES = 500_000 # increase if you want even smoother estimates +samples_polar = np.random.multivariate_normal(mu_polar, P_polar, size=N_SAMPLES) +r = samples_polar[:, 0] +theta = samples_polar[:, 1] +print("Mean theta:", np.degrees(theta.mean())) +print("Mean r:", r.mean()) +samples_cart = np.column_stack((r * np.cos(theta), r * np.sin(theta))) + +mu_true = samples_cart.mean(axis=0) +print("True mean:", mu_true) +P_true = np.cov(samples_cart, rowvar=False) + +print("Running linear") + +# 2. Linearization / EKF +mu_ekf = polar_to_cartesian(mu_polar) +J = jacobian_polar_to_cartesian(*mu_polar) +P_ekf = J @ P_polar @ J.T + +print("Running UT") +# 3. Unscented transform +mu_ut, P_ut, sigma_pts, sigma_cart = unscented_transform(mu_polar, P_polar, + polar_to_cartesian) +print("Finished computations") + +# --------------------------------------------------------------------------- +# Make the figure +# --------------------------------------------------------------------------- + +fig, ax = plt.subplots(figsize=(6, 6)) + +# Means +ax.scatter(mu_true[0], mu_true[1], color="C0", marker="x", s=80, + label="True mean (Monte Carlo)") +ax.scatter(mu_ekf[0], mu_ekf[1], color="C1", marker="o", s=60, + label="Linearisation / EKF mean") +ax.scatter(mu_ut[0], mu_ut[1], color="C2", marker="^", s=70, + label="Unscented transform mean") + +# 1σ covariance ellipses +plot_cov_ellipse(mu_true, P_true, ax, + edgecolor="C0", facecolor="none", linewidth=2) +plot_cov_ellipse(mu_ekf, P_ekf, ax, + edgecolor="C1", linestyle="--", facecolor="none", linewidth=2) +plot_cov_ellipse(mu_ut, P_ut, ax, + edgecolor="C2", linestyle="-.", facecolor="none", linewidth=2) + +# Optionally: show UT sigma points in Cartesian space +print(sigma_cart.shape, "sigma points in Cartesian") +i = 2 +#ax.scatter(sigma_cart[:, 0], sigma_cart[:, 1], +# color="b", alpha=1, s=20, label="Sigma points") +print("Sigma point", i, "in polar:", sigma_pts[i]) +print("Sigma point", i, "in Cartesian:", sigma_cart[i]) + + +# Formatting +ax.set_xlabel("x (m)") +ax.set_ylabel("y (m)") + +ax.set_aspect("auto", adjustable="box") + +# Match the visual window of the original figures +#ax.set_xlim(-0.4, 0.4) +#x.set_ylim(0.9, 1.04) + +# De-duplicate legend entries (sigma points share label color) +handles, labels = ax.get_legend_handles_labels() +by_label = dict(zip(labels, handles)) +ax.legend(by_label.values(), by_label.keys(), loc="lower center") + +plt.tight_layout() +plt.show() + +# Optionally save straight to file: +fig.savefig("julier97_fig3_color.png", dpi=300) From c2f5c1aaddfed1fbc95a43c2df373e51a8b5c613 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Fri, 12 Dec 2025 16:00:25 +0100 Subject: [PATCH 74/75] Update for santiago talk Add python script to generate plots demonstrating the unscented transformation in a UKF. Generate plots for talk --- macro/tests/UKF/TestManyTracks.C | 15 ++- macro/tests/UKF/UKFSingleTrack.C | 24 ++-- scripts/pythonScripts/julier97_fig3_color.png | Bin 0 -> 229353 bytes .../julier97_fig3_color_wsigma.png | Bin 0 -> 173695 bytes .../sigma_points_post_transform.png | Bin 0 -> 170580 bytes .../sigma_points_pre_transform.png | Bin 0 -> 146255 bytes scripts/pythonScripts/unscentedPlot.py | 110 +++++++++++++----- 7 files changed, 103 insertions(+), 46 deletions(-) create mode 100644 scripts/pythonScripts/julier97_fig3_color.png create mode 100644 scripts/pythonScripts/julier97_fig3_color_wsigma.png create mode 100644 scripts/pythonScripts/sigma_points_post_transform.png create mode 100644 scripts/pythonScripts/sigma_points_pre_transform.png diff --git a/macro/tests/UKF/TestManyTracks.C b/macro/tests/UKF/TestManyTracks.C index b6bc57964..01d1a0b5e 100644 --- a/macro/tests/UKF/TestManyTracks.C +++ b/macro/tests/UKF/TestManyTracks.C @@ -69,8 +69,7 @@ void TestManyTracks(int n, double bias = 0) fTrueMom.R() + 4 * fSigmaMom); hMomSampled = new TH1F("hMom", "Reconstructed Momentum (MeV/c)", 100, fTrueMom.R() - 4 * fSigmaMom, fTrueMom.R() + 4 * fSigmaMom); - hMomError = - new TH1F("hMomError", "Error (%)", 100, -4 * fSigmaMom / fTrueMom.R() * 100, 4 * fSigmaMom / fTrueMom.R() * 100); + hMomError = new TH1F("hMomError", "Error (%)", 100, -2, 2); hMomError2 = new TH1F("hMomError2", "Error (%)", 100, -4 * fSigmaMom / fTrueMom.R() * 100, 4 * fSigmaMom / fTrueMom.R() * 100); @@ -90,8 +89,9 @@ void TestManyTracks(int n, double bias = 0) auto filtState = ukf->GetFilteredStates()[0]; auto smoothState = ukf->GetSmoothedStates()[0]; + auto smoothStatePrev = ukf->GetSmoothedStates()[1]; - double pReco = smoothState[3]; + double pReco = (smoothState[3] + smoothStatePrev[3]) / 2; // Average of current and previous state hMom->Fill(pReco); double error = (pReco - fTrueMom.R()) / fTrueMom.R() * 100; hMomError->Fill(error); @@ -107,19 +107,22 @@ void TestManyTracks(int n, double bias = 0) // Draw results c1->cd(); + hMomSampled->Scale(10); + hMomSampled->SetStats(0); + hMom->SetStats(0); hMom->Draw("hist"); hMomSampled->SetLineColor(kRed); hMomSampled->Draw("same hist"); auto legend = new TLegend(0.65, 0.75, 0.88, 0.88); legend->AddEntry(hMom, "Reconstructed", "l"); - legend->AddEntry(hMomSampled, "Sampled", "l"); + legend->AddEntry(hMomSampled, "Sampled (scale x10)", "l"); legend->Draw(); c2->cd(); hMomError->Draw("hist"); - hMomError2->SetLineColor(kRed); - hMomError2->Draw("same hist"); + // hMomError2->SetLineColor(kRed); + // hMomError2->Draw("same hist"); } /*********** Function implementations **************/ diff --git a/macro/tests/UKF/UKFSingleTrack.C b/macro/tests/UKF/UKFSingleTrack.C index 99886f9ac..f52d3a613 100644 --- a/macro/tests/UKF/UKFSingleTrack.C +++ b/macro/tests/UKF/UKFSingleTrack.C @@ -89,17 +89,18 @@ void UKFSingleTrack() XYZVector startMom(0.00935463, -0.0454279, 0.00826042); // Start momentum in GeV/c startMom *= 1e3; double beginMom = startMom.R(); // Initial momentum in MeV/c + std::cout << " Initial momentum: " << beginMom << " MeV/c" << std::endl; XYZPoint nextPos(x[1], y[1], z[1]); startMom = startMom.R() * (nextPos - startPos).Unit(); // Set momentum direction towards the first hit // Initial uncertainties - double sigma_pos = 1; // Position uncertainty of 10 mm - double sigma_mom = 0.01 * startMom.R(); // Momentum uncertainty of 10% MeV/c - double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree - double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree - ukf.fEnableEnStraggling = true; // Enable energy straggling - ukf.setParameters(1e-3, 2, 0); // alpha, beta, kappa + double sigma_pos = 1; // Position uncertainty of 10 mm + double sigma_mom = 0.1 * startMom.R(); // Momentum uncertainty of 10% MeV/c + double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree + double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree + ukf.fEnableEnStraggling = true; // Enable energy straggling + ukf.setParameters(1e-3, 2, 0); // alpha, beta, kappa TMatrixD cov(6, 6); cov.Zero(); @@ -207,7 +208,8 @@ void UKFSingleTrack() auto mom = smoothedStates[i][3]; auto KE_in = Kinematics::KE(lastMom, mass_p); auto KE_out = Kinematics::KE(mom, mass_p); - eLossSmooth.push_back(KE_in - KE_out); // Energy loss between smoothed states + if (i > 1) + eLossSmooth.push_back(KE_in - KE_out); // Energy loss between smoothed states } else { eLossSmooth.push_back(0); // First point has no previous state to compare } @@ -248,7 +250,7 @@ void UKFSingleTrack() track->GetZaxis()->SetLimits(zmin, zmax); track->Draw("P"); - track2->Draw("PSAME"); + // track2->Draw("PSAME"); smoothedTrack->Draw("PSAME"); TGraph *elossGraph = new TGraph(Eloss.size()); @@ -277,11 +279,11 @@ void UKFSingleTrack() for (size_t i = 0; i < p2.size(); ++i) { pGraph->SetPoint(i, i, p2[i]); pGraph->SetPointError(i, 0, - sigmap2[i] * 5); // Error bars from sigmap2, converted to GeV/c + sigmap2[i] * 1); // Error bars from sigmap2, converted to GeV/c } pGraph->SetTitle("Momentum per Hit;Hit Number;Momentum [MeV/c]"); pGraph->SetMarkerStyle(20); - pGraph->SetMarkerColor(kBlue); + pGraph->SetMarkerColor(kRed); TGraphErrors *lambdaGraph = new TGraphErrors(lambda2.size()); for (size_t i = 0; i < lambda2.size(); ++i) { @@ -305,7 +307,7 @@ void UKFSingleTrack() for (size_t i = 0; i < pSmooth.size(); ++i) { pSmoothGraph->SetPoint(i, i, pSmooth[i]); pSmoothGraph->SetPointError(i, 0, - sigmapSmooth[i] * 5); // Error bars from sigmapSmooth, converted to GeV/c + sigmapSmooth[i] * 1); // Error bars from sigmapSmooth, converted to GeV/c } pSmoothGraph->SetTitle("Smoothed Momentum per Hit;Hit Number;Momentum [MeV/c]"); pSmoothGraph->SetMarkerStyle(22); diff --git a/scripts/pythonScripts/julier97_fig3_color.png b/scripts/pythonScripts/julier97_fig3_color.png new file mode 100644 index 0000000000000000000000000000000000000000..0fbc0cb913463e427522f6f4117f9823243812e2 GIT binary patch literal 229353 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�e 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 literal 0 HcmV?d00001 diff --git a/scripts/pythonScripts/julier97_fig3_color_wsigma.png b/scripts/pythonScripts/julier97_fig3_color_wsigma.png new file mode 100644 index 0000000000000000000000000000000000000000..b7937bfa678049f37066e02c14dd15668a22e716 GIT binary patch literal 173695 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%0AgFFm*+VT_2*rSztDQiDIB~9+t~{mXs@MQJj05$* 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 W*RB?G6VB0pn;35}Dq6ks&_4m3UYb|{ literal 0 HcmV?d00001 diff --git a/scripts/pythonScripts/unscentedPlot.py b/scripts/pythonScripts/unscentedPlot.py index 17dc5b79f..f246abb18 100644 --- a/scripts/pythonScripts/unscentedPlot.py +++ b/scripts/pythonScripts/unscentedPlot.py @@ -19,6 +19,14 @@ import matplotlib.pyplot as plt from matplotlib.patches import Ellipse +# Increase font sizes for better readability +plt.rcParams['font.size'] = 18 +plt.rcParams['axes.labelsize'] = 18 +plt.rcParams['axes.titlesize'] = 18 +plt.rcParams['xtick.labelsize'] = 14 +plt.rcParams['ytick.labelsize'] = 14 +plt.rcParams['legend.fontsize'] = 14 + # --------------------------------------------------------------------------- # Problem setup: range–bearing measurement of a target at (0, 1) @@ -157,53 +165,97 @@ def plot_cov_ellipse(mean: np.ndarray, print("Finished computations") # --------------------------------------------------------------------------- -# Make the figure +# Make the figures +# --------------------------------------------------------------------------- + +# --------------------------------------------------------------------------- +# Figure 1: Sigma points pre-transform (polar space) # --------------------------------------------------------------------------- +fig1, ax_pre = plt.subplots(figsize=(6, 6)) + +ax_pre.scatter(mu_polar[0], mu_polar[1], color="black", marker="x", s=100, + label="Mean (polar)", zorder=5) +ax_pre.scatter(sigma_pts[:, 0], sigma_pts[:, 1], color="C2", marker="o", s=50, + alpha=0.7, label="Sigma points", zorder=4) + +# Covariance ellipse in polar space +plot_cov_ellipse(mu_polar, P_polar, ax_pre, + edgecolor="C2", facecolor="C2", alpha=0.2, linewidth=2) + +ax_pre.set_xlabel("r (m)") +ax_pre.set_ylabel(r"$\theta$ (rad)") +ax_pre.set_title("Sigma Points Pre-Transform (Polar)") +ax_pre.legend(loc="best") +ax_pre.grid(True, alpha=0.3) +ax_pre.set_aspect("auto") + +plt.tight_layout() +fig1.savefig("sigma_points_pre_transform.png", dpi=300) + +# --------------------------------------------------------------------------- +# Figure 2: Sigma points post-transform (Cartesian space) +# --------------------------------------------------------------------------- +fig2, ax_post = plt.subplots(figsize=(6, 6)) + +ax_post.scatter(mu_ut[0], mu_ut[1], color="black", marker="x", s=100, + label="UT mean", zorder=5) +ax_post.scatter(sigma_cart[:, 0], sigma_cart[:, 1], color="C2", marker="o", s=50, + alpha=0.7, label="Transformed sigma points", zorder=4) -fig, ax = plt.subplots(figsize=(6, 6)) +# Covariance ellipse from UT +plot_cov_ellipse(mu_ut, P_ut, ax_post, + edgecolor="C2", facecolor="C2", alpha=0.2, linewidth=2) + +ax_post.set_xlabel("x (m)") +ax_post.set_ylabel("y (m)") +ax_post.set_title("Sigma Points Post-Transform (Cartesian)") +ax_post.legend(loc="lower center") +ax_post.grid(True, alpha=0.3) +#ax_post.set_aspect("equal") + +plt.tight_layout() +fig2.savefig("sigma_points_post_transform.png", dpi=300) + +# --------------------------------------------------------------------------- +# Figure 3: Comparison of methods (original plot) +# --------------------------------------------------------------------------- +fig3, ax_compare = plt.subplots(figsize=(6, 6)) # Means -ax.scatter(mu_true[0], mu_true[1], color="C0", marker="x", s=80, - label="True mean (Monte Carlo)") -ax.scatter(mu_ekf[0], mu_ekf[1], color="C1", marker="o", s=60, - label="Linearisation / EKF mean") -ax.scatter(mu_ut[0], mu_ut[1], color="C2", marker="^", s=70, - label="Unscented transform mean") +ax_compare.scatter(mu_true[0], mu_true[1], color="C0", marker="x", s=80, + label="True mean (Monte Carlo)") +ax_compare.scatter(mu_ekf[0], mu_ekf[1], color="C1", marker="o", s=60, + label="Linearisation / EKF mean") +ax_compare.scatter(mu_ut[0], mu_ut[1], color="C2", marker="^", s=70, + label="Unscented transform mean") # 1σ covariance ellipses -plot_cov_ellipse(mu_true, P_true, ax, +plot_cov_ellipse(mu_true, P_true, ax_compare, edgecolor="C0", facecolor="none", linewidth=2) -plot_cov_ellipse(mu_ekf, P_ekf, ax, +plot_cov_ellipse(mu_ekf, P_ekf, ax_compare, edgecolor="C1", linestyle="--", facecolor="none", linewidth=2) -plot_cov_ellipse(mu_ut, P_ut, ax, +plot_cov_ellipse(mu_ut, P_ut, ax_compare, edgecolor="C2", linestyle="-.", facecolor="none", linewidth=2) # Optionally: show UT sigma points in Cartesian space print(sigma_cart.shape, "sigma points in Cartesian") i = 2 -#ax.scatter(sigma_cart[:, 0], sigma_cart[:, 1], -# color="b", alpha=1, s=20, label="Sigma points") print("Sigma point", i, "in polar:", sigma_pts[i]) print("Sigma point", i, "in Cartesian:", sigma_cart[i]) - # Formatting -ax.set_xlabel("x (m)") -ax.set_ylabel("y (m)") - -ax.set_aspect("auto", adjustable="box") - -# Match the visual window of the original figures -#ax.set_xlim(-0.4, 0.4) -#x.set_ylim(0.9, 1.04) - -# De-duplicate legend entries (sigma points share label color) -handles, labels = ax.get_legend_handles_labels() +ax_compare.set_xlabel("x (m)") +ax_compare.set_ylabel("y (m)") +ax_compare.set_title("Method Comparison") +ax_compare.set_aspect("auto", adjustable="box") +ax_compare.grid(True, alpha=0.3) + +# De-duplicate legend entries +handles, labels = ax_compare.get_legend_handles_labels() by_label = dict(zip(labels, handles)) -ax.legend(by_label.values(), by_label.keys(), loc="lower center") +ax_compare.legend(by_label.values(), by_label.keys(), loc="lower center") plt.tight_layout() -plt.show() +fig3.savefig("julier97_fig3_color.png", dpi=300) -# Optionally save straight to file: -fig.savefig("julier97_fig3_color.png", dpi=300) +plt.show() From a6b646b1b63c955df44e2241465061f115a114e5 Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 12 Feb 2026 20:31:28 -0500 Subject: [PATCH 75/75] Add macro for simulating many tracks --- macro/e12014/adam/ML/readme.md | 0 macro/tests/UKF/SimulateManyTracks.C | 223 +++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 macro/e12014/adam/ML/readme.md create mode 100644 macro/tests/UKF/SimulateManyTracks.C diff --git a/macro/e12014/adam/ML/readme.md b/macro/e12014/adam/ML/readme.md new file mode 100644 index 000000000..e69de29bb diff --git a/macro/tests/UKF/SimulateManyTracks.C b/macro/tests/UKF/SimulateManyTracks.C new file mode 100644 index 000000000..01d1a0b5e --- /dev/null +++ b/macro/tests/UKF/SimulateManyTracks.C @@ -0,0 +1,223 @@ +std::string getEnergyPath() +{ + auto env = std::getenv("VMCWORKDIR"); + if (env == nullptr) { + return "../../resources/energy_loss/HinH.txt"; // Default path assuming cwd is build/AtTools + } + return std::string(env) + "/resources/energy_loss/HinH.txt"; // Use environment variable +} +using ROOT::Math::XYZPoint; +using ROOT::Math::XYZVector; + +const double mass_p = 938.272; // Mass of proton in MeV/c^2 +const double charge_p = 1.602176634e-19; // Charge of proton + +// Vectors to store the simulated points to compare to +std::vector x_sim, y_sim, z_sim, Eloss_sim; +TH1F *hMom = nullptr; +TH1F *hMomSampled = nullptr; +TH1F *hMomError = nullptr; +TH1F *hMomError2 = nullptr; + +TCanvas *c1 = new TCanvas(); +TCanvas *c2 = new TCanvas(); + +// Parameters for model +const int pointsToCluster = 5; +const double sigma_pos = 1; // Position uncertainty of 10 mm +const double sigma_mom = 0.1; // Momentum uncertainty in percentage +const double sigma_mom_sample = 0.05; // Sampled momentum uncertainty in percentage +const double sigma_theta = 1 * M_PI / 180; // Angular uncertainty of 1 degree +const double sigma_phi = 1 * M_PI / 180; // Angular uncertainty of 1 degree +const double gasDensity = 3.553e-5; // g/cm^3 +const double fAlpha = 1e-1; +const double fBeta = 2; +const double fKappa = 0; + +// Global variables +kf::TrackFitterUKF *ukf = nullptr; + +// Functions in file +/// Loads hits from an input file +void LoadHits(); +/// Run a single UKF for the passed initial state +void SingleUKF(XYZPoint initialPos, XYZVector initialMom, TMatrixD initialCov); +/// @brief Create the ukf pointer to reuse. +void CreateUKF(); + +TMatrixD CalculateInitialCov(double p); +TMatrixD CalculatePosCov(); + +/// @brief Test many tracks, saving stat properties +/// @param n +/// @param bias +void TestManyTracks(int n, double bias = 0) +{ + LoadHits(); + CreateUKF(); + + XYZPoint fTruePos(-3.40046e-04, -1.49863e-04, 1.0018); + XYZVector fTrueMom(0.00935463, -0.0454279, 0.00826042); + fTrueMom *= 1e3; + double prevKE = AtTools::Kinematics::KE(fTrueMom.R(), mass_p) - Eloss_sim[0]; + double fMomPrev = AtTools::Kinematics::GetRelMomFromKE(prevKE, mass_p); + std::cout << "Initial mom: " << fTrueMom.R() << " past mom " << fMomPrev << std::endl; + + double fSigmaMom = fTrueMom.R() * sigma_mom_sample; + + hMom = new TH1F("hMom", "Reconstructed Momentum (MeV/c)", 100, fTrueMom.R() - 4 * fSigmaMom, + fTrueMom.R() + 4 * fSigmaMom); + hMomSampled = new TH1F("hMom", "Reconstructed Momentum (MeV/c)", 100, fTrueMom.R() - 4 * fSigmaMom, + fTrueMom.R() + 4 * fSigmaMom); + hMomError = new TH1F("hMomError", "Error (%)", 100, -2, 2); + hMomError2 = + new TH1F("hMomError2", "Error (%)", 100, -4 * fSigmaMom / fTrueMom.R() * 100, 4 * fSigmaMom / fTrueMom.R() * 100); + + for (int i = 0; i < n; ++i) { + + if (i % 100 == 0) + std::cout << "On iteration " << i << std::endl; + + double pSampled = gRandom->Gaus(fTrueMom.R(), sigma_mom_sample * fTrueMom.R()); + pSampled += bias; + hMomSampled->Fill(pSampled); + + // pSampled = fTrueMom.R(); + + ROOT::Math::Polar3DVector sampledMom(pSampled, fTrueMom.Theta(), fTrueMom.Phi()); + SingleUKF(fTruePos, XYZVector(sampledMom), CalculateInitialCov(pSampled)); + + auto filtState = ukf->GetFilteredStates()[0]; + auto smoothState = ukf->GetSmoothedStates()[0]; + auto smoothStatePrev = ukf->GetSmoothedStates()[1]; + + double pReco = (smoothState[3] + smoothStatePrev[3]) / 2; // Average of current and previous state + hMom->Fill(pReco); + double error = (pReco - fTrueMom.R()) / fTrueMom.R() * 100; + hMomError->Fill(error); + double errorPrev = (ukf->GetSmoothedStates()[1][3] - fMomPrev) / fMomPrev * 100; + hMomError2->Fill(errorPrev); + + std::cout << std::endl + << std::endl + << "With initial momentum " << pSampled << " reconstructed " << pReco << " Error: " << error << "%" + << std::endl + << std::endl; + } + + // Draw results + c1->cd(); + hMomSampled->Scale(10); + hMomSampled->SetStats(0); + hMom->SetStats(0); + hMom->Draw("hist"); + hMomSampled->SetLineColor(kRed); + hMomSampled->Draw("same hist"); + + auto legend = new TLegend(0.65, 0.75, 0.88, 0.88); + legend->AddEntry(hMom, "Reconstructed", "l"); + legend->AddEntry(hMomSampled, "Sampled (scale x10)", "l"); + legend->Draw(); + + c2->cd(); + hMomError->Draw("hist"); + // hMomError2->SetLineColor(kRed); + // hMomError2->Draw("same hist"); +} + +/*********** Function implementations **************/ +void LoadHits() +{ + if (x_sim.size() != 0) + return; + Eloss_sim.clear(); + std::ifstream infile("hits.txt"); + double xi, yi, zi, Ei; + int i = 0; + double eLoss = 0; + + // Save first point. + infile >> xi >> yi >> zi >> Ei; + eLoss = Ei; // Initialize energy loss + x_sim.push_back(xi * 10); // Convert to mm + y_sim.push_back(yi * 10); // Convert to mm + z_sim.push_back(zi * 10); // Convert to mm + + while (infile >> xi >> yi >> zi >> Ei) { + // Ei *= 1e3; // Convert to MeV + + if (++i % pointsToCluster != 0) { + eLoss += Ei; + continue; // Skip every 5th point + } + x_sim.push_back(xi * 10); + y_sim.push_back(yi * 10); + z_sim.push_back(zi * 10); + Eloss_sim.push_back(eLoss); + eLoss = 0; // Reset energy loss for the next segment + } +} + +void CreateUKF() +{ + auto elossModel2 = std::make_unique(gasDensity); + elossModel2->SetProjectile(1, 1, 1); + std::vector> mat; + mat.push_back({1, 1, 1}); + elossModel2->SetMaterial(mat); + + auto elossModel = std::make_unique(0); + elossModel->LoadSrimTable(getEnergyPath()); // Use the function to get the path + elossModel->SetDensity(gasDensity); + + AtTools::AtPropagator propagator(charge_p, mass_p, std::move(elossModel)); + propagator.SetEField({0, 0, 0}); // No electric field + propagator.SetBField({0, 0, 2.85}); // Magnetic field + + // Setup stepper for UKF + auto stepper = std::make_unique(); + + // Setup UKF + ukf = new kf::TrackFitterUKF(std::move(propagator), std::move(stepper)); + ukf->setParameters(fAlpha, fBeta, fKappa); +} + +TMatrixD CalculateInitialCov(double p) +{ + TMatrixD cov(6, 6); + cov.Zero(); + for (int i = 0; i < 3; ++i) { + + cov(i, i) = sigma_pos * sigma_pos; // Set diagonal covariance to some small number + } + cov(3, 3) = p * p * sigma_mom * sigma_mom; // Momentum uncertainty + cov(4, 4) = sigma_theta * sigma_theta; // Angular uncertainty + cov(5, 5) = sigma_phi * sigma_phi; // Angular uncertainty + + return cov; +} +TMatrixD CalculatePosCov() +{ + TMatrixD cov(3, 3); + cov.Zero(); + for (int i = 0; i < 3; ++i) { + + cov(i, i) = sigma_pos * sigma_pos; // Set diagonal covariance to some small number + } + return cov; +} + +void SingleUKF(XYZPoint intitialPos, XYZVector initialMom, TMatrixD intitialCov) +{ + ukf->SetInitialState(intitialPos, initialMom, intitialCov); + + for (int i = 1; i < x_sim.size(); ++i) { + ukf->SetMeasCov(CalculatePosCov()); + XYZPoint point(x_sim[i], y_sim[i], z_sim[i]); + + ukf->predictUKF(point); + ukf->correctUKF(point); + } + + ukf->smoothUKF(); +} \ No newline at end of file

j~_qgy7zV(h~MH(gv4fRk+K(svQyL3 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 z z7Y~)CAGsW|#mV{m>EABxOFq*>9n4EwuA4D{1hDQpicO4}?`M!prI$aphr3Mu{Q&!a z4_c%&0nC$rL0cTJQrLZ+dE2}t-mym4Ue~ty6pZ?`mzstJwa1Cb?M5N!-{e2#PtAp9 zkx5Ra8)1>`rqV(aKkuPngqW?qZR4Fx2h}f+4obQn=_@z* z^i0tw`eNMTvSFQ$#QU1;GoQ|4P!&y2q#?Mqb=?f@{`ehLxzKc(x{fuf5nPf({?Wm{`F{s0ry;YT3**Q}wA_7V1f3K@3r(Hn_ARug-n zoRaL-Jchz)*o<&utxDKzuj$vDT*8qbLs3cAJw9rKQGqv0|6{-RQ_5kkb;r`j4qk3q zyP_TK$pb+q838xn_xLKz;|LmZ_?fT+QqQRz?6aFcqV$2dw+PF4K6Y;oyhoXeM&Ba4 z@1^|~u9sMf=$2?Soevb%pdZjkuM_V2%LT)78jYPiWyyfB$XKvYQ6u_X2g{Q2LB)*o zm;qucemOeRAa1pJIb9(V$Fz6XF(u6MD5eRs0cbn=Z%0xk^?&LzZ15N#wt>X#^wax4 z_OLfs|90qT)LE$4G3ctg*DE*)!ev`)0O7UeMGm1(*TLA~*0u>Z2mT54BBS*&XE$DR zM?!f;vOK0Z@;dTu6GTK>{V#cPVlpfHcE8*P=EnvspB^Vda}pc|@_#PZn$^5RoXZP)GL`_@A10~3r*1~^V~M-R zQvRQ@;j?R4DPBEa%6B^>H<0pPJdoOXp!zaBqOl}-T^q$GF6_0z!MHfp^havq+(fj5 zIHRSOn$FT}W0tpxnoK z*SR3<-zG(3bDK+22&3Jgiv)7Mxt1ZVZjjG^afQwi2kGBL!sG4txu(ZpVxf;L-l9_< zqs}Vf=sda{5PWQ^!y|y`X6+%3XbEx2HEi9a;LV+RT-8b(JRIx}&Y019pM78PN4S92 zSy9UB^T<49EI65d?>=9qZ8vt6f9go9|A>(Z!b2eXUaLRjaD$#X$GwYM9g|vq4kMF^ z<4spRv?lIC9SPD!D}1Q=H^#Ic$=cEX0n`#d9`#4?d%DaHcO4COX>s?1S$_F( zf)-rcu+ufif3}iHk&q=ysjWTC)BL~ z7zy7PlU_yPE9>ggulfUd{Hi~OTmElxk>Rt7agF}|vK?#kR1^b+goU#P!nz65x~`jJ z&6brPY@!}%ZF>ABY@eL?YA460{<*4whJo?|;q}3f%4e?n9P&?z5qX{^ahbBhxIILT zruj)8^U1Wn#MJjQ&H;!FVt&ffMO)^OQU@Jh$ah-m>;A)>vlYVI#X-}Io%@}?% zoWQbud+C-nnh6b}M$`P!H~mVD)Bc}oB{%H9Z(d-247C!q|D{&a`g!g=cmFlj9O^F%ODfg#qsR^{ zcUY>N{6f@dy`XTLrX#PQ=&c9=&&&QBJ9X6;%nNdKvud-~d*;$DqdU)~ru0FMf3wZW zvyQ{j`k)L-$d#Zj>I@6r%EA%VJXbblGs0gtBs!GW%h8v9nVjQF73vy1)-l_VHR0)( zdu?PK^ys~N_ht(5Pw22bjB^I&Va-Gg3T<69<^ z?FT)kcnv5k43A6jS$+c7UM_LV{)_rtsq{lN=SO3?G>*ydFqXsw)63moUbsC9gXjCx z^sdzBS7a!?uFkO|_hgJTj4rMZXff2pT+78VZtcq2m*4l8ddx7aCNenb63?CrTr$e} zsk1^r($2b8wl(X)&7`3#CMx|Lz3G>0Yik2SLh|EHfBc@YUb_DvuQ;``<8)ci?fLi1 z`vbMdBmkY~3-SxvI$9`ra%!adT~qHX_>5`SFM~fa`NNha+bYyY0kw7WltEnPVA%5C zhsQT>bIH}%Gi=|{>U=AM5dpM%>;z^z?zV2|y}L z)VRUs)`d+R-G!mJhp!S&0R*%nf#dMeqYZ-XH*Qu*cZ}GGG45bzAG0uAx^0r7;olT_ z8zsHOnlMT9Rd{-z-lQK6xf3N=J2n9u+e?iPcJ|*6^6_a;W%&8}+Ko=z!aFPW za?0YE6@M0hcZE{7&?!u*Svp#k^IMLu=>Wmre7Wa>w>vGHOpl3{^#s+d<=t1+7h1Qn z*f2M!v>DTXC@qweHgGLmv7uJ-|~g zSzv932gZAl61!KHJ}g8bN573EU|SDRUT~uSbGZ94bK99S7T#_cK$1Ur55Sr8Bcd7w z@tgc`qxqDQojJp%>=KUml#||@KzuMfJe(RB?Ju*I0J)4J$Sdir{Ze&IAkcQA#O*vz z!a#*i1t$G%d$Y{yu6uIgUzoUFV!9hOW8KhlC*xQCt5zs&HaJnX(!wT46idxYUFyS! z!*EQQ!aR_(UIU%EmuGhlY`YJ&l{;VhrbLbMh9-H5t;J$~+dD$72`xr9uL*+ST{H1u zr@V;ZetJUce8`AS~=%BsD%L*nr%y+Ft1lsPFb zoy$8Ua%0JS>rEaEbV1gHTGJdQ%2~Sk(Ba`{oP%0V6vrCAp=<~b{(P)szNha1KmQOw zrW~TDXaADf&V+72+zz?~-2D;y0N{JBybq08)^sYww*I#Y=nu*3OwZ&-b<{l-PwTM= zr#5wzYRwaw*g@e3NLiILc$XxYO&dWCIQ(oPp?HYG5yqV>1A6t7`@1RjNsys_1{TgrI)>2ynGUDwjG1pwB8l%$k3;8CC3dR>W6wj|zzwLLp?F49om!s;* z4N9qP)JgEW*C^lM=Azb+c%{|rR%Hm`|6tA@^8kVkC_+KG1ww;4Hq3`6j2ydTFjAV) z)hVAp#FcILXHaW)?Xuxj=pis!7OTXMHZYwYs5-tODW!*jyiP&t&Hlej6A4eu6Jals zr5+xKj*wAV`1z$?^F`@QGxKA_`aC%p>PS->>uy(}5)bhTLJC zV_5zU_Iu<~h}=kE&_1zUyA->qwQv*2+z*y{0ZW6nJr4(xGc(upf4r-xKDs{BhUny+jUBN0_YX1?Hgk$t-U5^~Q zKPC&`G9;q8&mX@`^&-}jKkG?*81z}lQ>51jvF!b>A*=_i#5ZXGqi~5k^EFVvY&%TF zO8OEPj)~GOyP^Jp2h^k>Hw^e@`t9T$T*(kFjTMI;EQ)BT|4ckBAmUjQ|GSFP6-RRutyaO4OBn=!-tj&vw#Dyo(tk(v5b| zn(}g88tGU}psGCT7&$s>6Ud#4+6d=<-w;?j(3EmjJZDV-YM%kNu{$M?le$HoLu0ie zq=aD8W<0Q|)mqe{d@_lEGDew)Z_jwIdl}h5^xQKwXMS=d;-URkK9ENhh zcZkzQaU6FAQ>=4PqQ6K|%AI$AyrfC}5yiMHZ_>VrBshMB6_>P9MCfcxXieXV>0@~_ z73K{%7pwPuC=@oWaeCW;&eEigU+*#-*F?gbuOM(I0k5l9B>99UT?QKG$Cda zI%>q)#~H+zW~v$)L;sxa`F)ohZoTx!W2woqN=lZ;M~>*c6vpR3O zJRG#kRj?=z6vmaa|p!9H05ze^&NrAkZ7biAj1?= zc(=OGLZj*pUi16eLA8Q_W9`?EPz6*^HA2Fc3Wq<1>{IG(v`hd$`%z_yp&?ntYRh^S zy|4QTL=CvTI)hr4aXWT#^N~Da^)ryh99EB1tFlCwJi}1UHGfrhK{PF!E+cS2S|j(f zraMY`L;a6HYsOe-DjlQgD3qNg3jgR9o>NpD6bzktstxeoY=)}*L>fEnb1CetzCoou zsf$zQOR5Dw*B3yE^y%~GgG6wdf3b`IraZ9&Pyb4VL5raOFg~jif1L?B2dI|{9ralr z?=2lzyVhcJ99x9MdN@xEwh_ppb2%KRxbC%Ur|(0sHZo^4TmC%2uA}q08VONE&ux~Z zDFlUW4N!&>R_Oef(Ej|n(OT<9t*n^7g?UKT5iL_9lQ*8(i!K4j^*bBDvT_4S$Ao{U zEJp691rMS#nvW5f7{Ym3sc0DRFV&NubP0;rqXZXTSBG zh(5dX?rDfqgY~Q$H&Du(R971^btJEI6@Rq{EJ~-r(K&FOXgxt(Icaewbm?-(pHIR# z&XKcY`ci_b^>EW6Eu2G{4M1;_QDU4?)T;l2&h2bgqDe=Zp^4&)@F-zKQn0KrQ6kYZO`u+t@1U6j$WLu`InY_=bLogA1S@{n)H$r?US%kljapYeD9j@#g(@x z#d-&#zsHO7d3YQ2s`Jz9LK*C}@-t<*mOhCUm(IqD9=|PO-Ht&AYW}{{X@%3AbXAXr0Cxbcz)rSUs@U1bLTN?N+osRN4RQ)jEL&^ZQv1(aER{(i4lW>JV&pv`0ffP_d}s~L51*vCqjQi)*Au2 z``Rcb3`GgpTS?QtpI0{NP#+!YCM;)xWxze zJOxlUhUsZD0bB9Zzq%$)IG2s4>KDf}-MNbqZ}N*lx&Bp8x0}(KkvR>Zgn+`RuZDy6 zYBD+8_MoXl5w}WkZ2pjxgc^S8qxSkT`wpQa1G?BYuv6` zRfNg)TNdxC{K+F8EfP4)xD{$GZE?~16|eV;CDVTK?{$ojYAQ01K<6QmfX{&K4L#WK zg))djkyrLJ#o1evS`e+I1%>q(eRw1Wi$ggKI$=zS1*?&2uHS90`gIv642kc~0s?!{ zqnmTiKNbo-YegN(*J0(IsVko>MMPbMmEp`U1pS9aAanM_WWfA?b@@X5M8LXk0j6+w z3|IvJ9VB^uSFBaM>1PdLNt+m6Bfp#q3IjAQMl8ZkTW5ucmN1xQ<2Zq)WmJNO(}r6o z&sKESv^oFJJ5M$uGC0H_ktaOu-;F``XxiEyeVt=3;1;eht($N`nG6UKPbY4cE)p=Z zd$`5Dn!zk%!6p>G|9_aSS01u8nXF2{@Q*PRX5edo zD`wg!4@8zcs}aX`P8Q0`UgMY7Q+Eu~B|TZ$p|~m&#U!5P);~77aO!6@(1p_v9@`0V zovUZ2c9O9~H?o9!B21=pBMQuV)1PheMb@-ubbpmr+Z~UNb(wsUc2bWixLdTqnKUuD z88#psMJ|9zYFqrl>Li544+f@JPb6p?|&Mh59u0?br%m& zU*k^NPgqY3?kpb`mzn0kQfIR4e@-Sad2@2sw0*u*WJ-NiQ(V+-44XD?BnxB->u&NRE{kTtsfDd_7A925hmD_mewNl-ypM5J2`3m3NMfFsjh3JGN zy!LSunqO^C?sPbHGJB2lKAGa|v&9Hy?$mKUrxi(LtL(|hwfb-il!e;PZb8i47EBKA z9&WcyW;84zpPjUL0!Kxm)7V5u_53|qTX?nYZ?OFR;>}=~tFXuwgOsbs8l~Uno{zhl zrQL<3COXvNr@c78#LFc0#*y*zXpGc3mC)LRE#0SG(Y3c$tTi{3_CTYe{qN0#j!@Qq zn+NmPB<@`ulfpG^K7y99J*F}0wdOpseCqv?r``Fe@0>OQ&Z>?pqF9>R#MLnxMM^^q zW>dtiJnngTiPWBXH_XTDRvS8o$Aiu={=zClRS=LX#s(so^Dwa$0hV9Z(aVe{1dQtZX-KP0;V%y9M84bAGyi8u=_kOH5N(+FP{2h{4#? z+&-%eF@zLI5Thj&P7IbhDoMpD>|d(Aa`8vAS?2FrM2}?kNDt`i7M<%jjbRngEUCNS zX+!_$?up&}Y1Pm_8onxxwG^jr=3AWyvIllZOKo|}Xmc^;dsi}jQNIz1VNFhQiD6{G z8?*(o(?GVOT9G+*`#7yA-q2QlLa1#I)JE!V>Aj=+YF%fPyH6ixozisdkJfM7(}U3mI{ z;0Ks5izyO2Y8}s%Bz~0sRL02!YU-gYhU^!xJ9UG0i!f5z3CJ9T}o3cFos`-Czyt`IU5*SeuYn{Anj$?PFpnFF3!q$ zdh?lsT%rXXC;>+jMT+nqZjJG=KnnR(<+ zwcnQrw3&`ys-}ac=C;QvQzA;9GY6V}Zg=x~*Aav31pZ!k`Bx3^$KXO}q{4I;kC2;V zKTvy7NatK}_Ij7U56($@@FbBpd!R0yGU{Lia6R%$40Yqa_Q+;-US+|-EpuP`TKPUo zCkM@)I2bf*DT*_MU$D6zt$%72E%~TIH<_?UhP00uKFR!6Jwp5iJuR1R5)Ko&SY#TZ z4?{TprR8do03m7zZ^u2&#^;c<>5;`NH*0>-L^@wn_{ufw*;Fv&>Y5oJ0t7uW}U=|GP`i` z+V&`unTa3&0y&m}N-t^$0u-w?L+Fu$mCB;_`r7>^7*GwgS*SE7k zw|dYeSmo)VaO9)3>}%?g0l&jl?x!b7$@`V~^;^kX62c%P2c!JA+?BOiYSwZOZikkz z4BrJASo)OE{KVC=bhK=!r59zjD|S;OX1^$s2lTieUZS~n7LK;7(&3z?_Z|=LybK$6q`3ln zjp=7W`zqUq-;fy7smrUriW*~bu7dW5aeb11={8bFzzaisxA6~VDbM{wl}c;ri^rV^ zT*u3Qr6iW@!`UYxyjKo7tZnC!AnVY!v^YklAWPwvQ8aPqt4TgjZRb^fESi*BS>rX9 zPc7Z*DWZT?Aq$65 z29T@KAg_;V8UC3R?SJ7n|e2cmk}n#uOL*C7y8Sw2aM8Orc3SOFw* zk#?#VnN0|CVT-y^3y&%9$D#R3j3p_`wW?hUW7oQLCJJAYIV zFH(V5NV1&VhqP6;a;Jz&*?BumcfpBGtsf1-7wJ~LwV$l(hDJXaiVb<8^!R_(@|ER8 z)kq&tfW(=7hFW~))BK6}F7&Y0jgF2=)e2nta%qypBjtbrahFbuv{5ARNpS8kD_|-6I-MQzA&9D zXs+_cwziW5io~a^Q&SXCDDBHgT_4l)(ve6A6|@y z;d4#vP{Ci7&Y9jdaaHGL{}fqU8yee$K3%)}OpbXIb?qSPgLTnmAK=?$r({#U4_U9% z0!iT&-;n4w9#T?o9nz!Yv-qiwF5O*>t>4QIO|~t0OO+lI&s4e*%Mf zvYrw8R$yXGt)Shn3`RQ%xyF9!1N_YP6@V=$SDwq|du|hK$&(=QA_GbLFo8&<}=CY)0r(f_YTk<)|C#m#{SZC>mY)<~6m%8XZ0_&lb!X4S`iuB83_ z>6H>Z&nGRCG17bE90u$4JW7TVCqKE9`{afIHPvH`?!GZ|@Y7Sg;sdm&+SkUL5?rkW zkcWl#3Tm?@EN#wbtnm^^JAV`zF^xV-|CCeqFzi>&1Q62yl>ddx>clg4|GQ%5O{g1W z9*Cvtw*Iw<>vU$+WPo|i0P$Ip5?d1V4fQmp`|YQ8!QEb>Z8L#v^W=AL0{641S`JIV zlRGW;YX>#t-Kp?!c>kuAx*@mz{Uo1|cDgQ;ch;{31%hbOsZYOB!odL~m@jPG3SK)c zn5=%te0N|-^$Jrdn(-s~mr-~gz1z{+XfaoSg6!nR40@(<|LNpING+eD3KcT)?SfRsee_w5Q zFrLoI@w5N?a!&VAW9H#gkQOne8EvC=$A$5e=an``>eF6VTTR|Zz3KGz{8KnVfxQ3D zRNAv8=F}}ZLr^5hf5?bL@4|plCM;Vjs60^U9p{E^WM- zuv{lcc!oO1CRa~jSz@}x0LN%sOshVxMikzB(V1p_HvyM&#UbCjZy4!}x9a!zmj|_t zM_it7<}&rzP()~GjwIi#Rzq~i5nEHlS+4LA26=(z3KC7`nwNingyc8sLkad8WjJv( zW~&}6&%Y08p+WmYFkCogcsrIF6-e*Of#tiXb}Fz2bv;3KY^pOhzNtC^2bxFYq}rUD z-r$W01?7VVaZEvCqaEzIGMi>RJ+U6yAEBAUPX9iCqm_s5VEzYve_xUr?8ghB3=M|d z{4Sly*K|VDbu)FVtfX$0CMH?Wyu12O2L3tM#+y!FRa@P{%n?PL@A93h!8k<&@S2!l zI;wW_o;#w<6KU^+V?gM8nEkrdbHsQ5&!^gi6^Zoz=@>GNgiRYTTJ^R~Ss=GbdDALl z<4#MVd2`RxVzV117Kihyj@7=}maVetAO6?6MFHxPVJDA-eR`putS5D~(X@_5NbRa@ zr?!0V+a~$ly8}$a9_}_dBK<-)V&BEceQc{=-EL_wkoZ#}U_T~U?(eT`bYg6-%Q~&m zKdmz0&|%)8%R@NhY6`M^)Da=>HHso%M+Ph9+ZYv#w4 z+h`uPxze00EIFANbiN)b#2Pl*Pm%pEHQ_^Bf@(piWzYiMo0vBE_ z3?b#mRkEk0R0i~@mm3f7{P>FTcQ9n-Is7&eD4(M{Hf(g&ma*qU%i|=YKBy2+`2Nd| zO`nEeg^jM~U$27STw*{&FkdmA8{ajd7`| z7tfqIGguSpzM6sc?FVN%-c6qi?r-?`J-N3sIL>;g!MOX=vzAOV&9-SF-9o2$2c0;z zqy|47$C!#hVbj)3v)IH$Rb%6rmMrt6yr~xXCp+!lqObO1++rE!w)=GYYujDL0`yGk ztNd9>p8lj*jWc1D*&+)*;e!(K3zPE?bnTjbg!xq0hB4eHUWNd=49M{hJ$ zcQ*R?`3(`{_%_K?Ugxpl#l zhlgjP%bkA^#K5{&hpwhOF3i$DOE(U2oHd>t>(Ie_dLMRCV!Rjb2Bl>)ubAfP2l^k|LpuAIuPNNexo_n+hYUe zMb3?qsbCiRlA^@ElkUI5aJQcwqy+cX-r`TP6E09fj`JdJ&6=+!2XykUoMp52-g_>9 zvo$ktesRBY)b;-AHvz)t+S{cYj|&M2iS`9qT7BX2yIvoAsba7j?c_$iCu~vZH;Qvz zz0G4)bct6FR%*9@q@yvH{27m9j(LD zCxLr*e*eJGa9UM^_kLxKZ;kx-XXF>YZ@mAaTjbe&i_NjIu}5xx6A_>J;kAEYBjrWw zy`D4OLa9CNO+^dN{l4RKa1o z>K_|1mS7wm9L21CN6MXYc-@UNBPjaPet#BHG9VYHCGlgK!NoBU#CrX$3}C;8os@^bQsaf zw)zT6%4-P>SZqIbJ0ja^AWV|u^>a2clRrPd+x_~&Jvq}CYKr59C-ZEJ-I&N>XNM2X zyX;jAdTU%8#dU9Fb||S4>z+VP{KJ{Y@5nb?*cY)=+^*k+QL)-(E#m|ZQoTOW|22R5 zagpA$ZGzG;pvC7qQuK;eljF_snmP&!468yCz&2c}z1tTY4SUk*>FHUwa{1u5_i2wA z*r@0J>Nw6shS&}6_wn}rGkc5mteW=S$gc4_UL~t3pEo}5{-d~Qw$%NvG08l*#8D(L z7{YeU8=2~lRm)HQ$ml8aGyUkYHh;>~b2BDH>_tc4bx-|S+;!@4KMo$ekae@|UQ*uA zkLzb)OBateT)U*A(ocvm9~(z;>e{&=5vAhdV*BB6)5gRAQR~=B@#!lare%Kojkw*8 z9~R<^f4br50+SqY!d3%b#oHnVu>&!VeEEO$&jlVm_dv36x`kQi-ka%;`vqT6Bm8)B zy63cM*XTLkZ`6G7p8oczld7NVdlNjB*y1YxmnpWa*m(T)EsL#6^t|}0o7kvVH}Uoq$^7w3J;qfonceOC z=qQqz|7_ZAZq}B4sY+rgH)+9X1w(bDpLR<`zBGQ*>->r4#zmZPX*%9jT(R@2!4$d3G(G1io_=X>9gYdC!j-XtU`Pc2D%t-Sx<@Hd+izajZcP`f(4 z5hLQSdkWamTcCV4Sf%=4s9BS%=w)}{9AcW;N|(=?WJKy?&IDn!`Wx0R#W`Ty;ZF-| z$ya&)yt?>tp%PCn`Ev)+@bMssX}p8%IC!|VaO^gY2kfehL`2HpCmNhMt?8Y}Dbt zAu;$iT%*qX)L7^V2fK@7{Cs>jzxD~AxqJ8SrxWWbFOIdd)mn%p{yWlsY2;u9)|>Lf z{h?EI`SpkPNC-Z2;XTXaOk^eNjQjdRH=2^SPPSIhVXC-j>ttX&%$e)PSX+**`>a|2 z0^t*095e5a=)|#@jo)?&{aJLFYK}hRfZWN;wj|y5Y=$1oN*A)6n(Zbm?K3yaUH7wY zG`cN=WcMHV+8Veb#!PmbCcT4$wg@z9Tl*>k8^avE>Muw(bASit2dY^6=38n^yHftNegBC1viumzJRE_Csdh)`<7(AQx|pYr<#rC zS{mXnhm&EN%b^Q6n2_s6LFdEu0rl(@4%_09cjj~%)BdzIt2(JF<7@oB<=*tBPishG zC$!EVA9qP|fG-urQPqKU%oL94nNdxPdG0lNV!#b4 zPWQSG`j6#3dBKPpf%>EqBUv4@E!glOo;BB8M_cExO9gLdCn-lOu5HEcYYTIno>;tA zRMnnmKZGf)-x*078FD4;_j56Vb)T!gO0ep9cv_{Xe9Ehwa%uR;BgQom5wkEC<}t;A zfIH7v-``KV{gC9-GnF|57506)|2^Mla#oO}L)%Z|U3}xKRw;HAwU1_@7}$W)9`F>bqXA3an^gJ3}@GD5E#+? zv)~EVL@X*wELe%d1r8$Gd$l)iLF}*o^lV4qL`bB(Uv^0N*SBFxHZwgr>Ib6k93Qn? z{ld{l$R{LpUqxQ`yx?sD(nC!vWI4hhbdUPPKg0K+>kW<`LVrODvI>(^=(jKcd z)WKf@i1BCKCYk8BY?pczr^j3aVz<{P37Gsxf9Z+lw4zCCCR0pHUW)YNYXLz4D_?Az zXIsgk+0xY^@gTq{Y`o3AQ7l|6vU9Xic;p*r+<&DXKRabUv3}hUgU!@>>bj7|wE4Ak zx$n0`^z8R-k&|8epevwfe=0Ok&evc7L3}EsH8a69DKGQOLVaCbe{Z09dw1iJ>A4p8 zsYzJakSlk{g(U5~fQk0ewwyQ^rLj2l+dpr3dFQ`LR@<&rw&}|8*ZzNO(Jr98FweDY zRsZIo?=vS>NQ%DX-4AfEK;QD zKh_m9`N;OIaK3FU_MPbTH_Hseih#X&_rHdnnH@2|A4jqtzbm(0VdXVH4k06U7Up=` zJi2Ogk-!##<`1Y(of9*yEvk^Kb8zx=TD7@m?DG`!1{2mTKIaA1@Id z%l%D!#ol`kg-**vyZ+r%mtm^L#LR43=6i4!PGb_uw&_qqLSoXhVtu>W0mwjqqCXcb zDrD9pos|2l)P-Ac`r1HEV;3D}CzNoTN6p*gi`|)rkfhe3*mKBuHQj6!>heZgPfxF= z+Ya$T2xr)z**#jz0jo+a;{%(aEdn7(_`5w@$=8m>Dd9H!7fIl?k`gYbNppwZOEs(v ziXqwGz&K|d(#3;(M%(FE@0cIk`AdG`!tqUOepWHbq`msbYg<#^nk@;grO7r+8`B?N zcrY~C5WlGZ%GlqmcW(8I9e>2uNM@#I6x?4Ir1e{~q&fKMr)TdT{$I3xc{rBc+wP-z zqF$BC6lF+;RAfqMGGxk>A*4ZOks)NL&|6AUDMXTt88SS{7%7U(^O%&(l9{m2_2~Qd z{`Rr=v5&ovWB>m7z2v!{`(F22>$ZkpHAgD^S`wQ-s(t@N)g>UPiJgw?9fo7z7QN(X+5Qad98n*aqO7dCqP4d;cZA0ZM`;VDtoa*?69L$l z*xT+)zEw9cvJD9h2glKNK0!ehU8eoo1=T1z7^%)6*>cbE_6HncsET>!kyTtNxUHD= zGOhgj(1qxStv<2pkX1`P#0uJ)UE%Lm+>j&b_4}La)m#r=la<}bt6Izb`1Ch7YZRw5 z2x4Il#uTqs9X4m%_Se=6?)e0rcvFkJ|kh?0UFG{IK-wi;KDHjC1mSKXAV2P~^1d4pj0HismG6rVF*JTx$0c z?k>8@@)KE-2(rtUN1xYcIrh_{rliwA;~DcBhkg`8;a9%FiekJW3&%faAUX{~Dpp$R zIPzgjKQfqFsKNV(RIPe`?g!X%yja-f_j%(`kb;kvQ~AF2C1E1wd($3Mjw6uE;FPI zl06YOLfUMP9rHP0)B2?L=LJn9!nM#?@Z?^c|1+IOMM}xDZeu@5h&HwlUn$|dDbhQ- zaT{<7fzsb?_olERq-WYvWSu?$RCB7WLmB2G#2ziFae zYM9GP29cS?J=wY?j$QSIK`;1!w#R7_W0t+pX+vUIw;47n9!!oCuN z7sv1Z&JZ-9zAD)F{H|WUM=ri#r}6B+|Lokf=Ev~rn-~k8gWDuL{!B%+cVMKv2fxfo zG)#WjVmvjpy|o69^IQHL9p@w=07i(8mOHaaAm~j3|4hAwwc7vma=BvTA*7?G<6@&J3m3AO zNBpM_#3kRpy+(Sh@<`r9l3_NFqndHjOxN~>SYQhk4Op6Q5^{15_IlvV@9op1JjFC` z9C)p*qtl0NL3L&nDf9bNyFYa<6@w`Nu~czh9Uh)qcm?)4DL`E_`kZ|CeNQXZZ*`S=_FLIkI|I=FNtLoJqj z3u=Ao@mlXe8nl%9>QmDaDj51x8{i_pjz3|dhW&Q_8aEFwAG-Mt$vOY3 z%I8PwkbPTs!ZdA%KdNYqS>T=N$2J zkn2roAgkBb)_zGKc@h}MYScF0J2_A6iMIxI0Lm@iKd z^!L&;izv{rJrv(ay$KA2(33bnKbcQbLi!0~Bcmiz|E%%R6ow=8&YPCyPbK(PujGappPiv z?D+XN^nvxZ9-}Od!|l0x?yR1FM#zgl1enIu^BIz!F2k??HV!S{P@29;nC~nB2&e_w z66I1d{!(_w27Qz7&`~%H*A?46{g}V5M27!VQf<0<%@}qk3lt^#Z*0Qi6B4v_bz>o| zwEX(+n&d>|Ig$-yKJ03FZ(RwTZS0=spCf4(E9$>}@XLTTg>`$59!m1SC={6EJyl1R zu2@&Ox0v;XrmDm-Y)55{Q;}7%hvuiw0V29co(5}E1ItVWtsqSmmAlxuYxCUQ5O)-@ zL8tg1{H?%-wb|E^@A>g#BkS5NLJp4(8lnfFU#N3lM})Z!>#sjt3Teg};U^#dp-Zgj zV@t@8PR$*1G5B)8I<84$^7PhSTKAl!Q}?F7N#tDD&%etM%N^&j`n&b#!vaCJM(V<9&Rl#iuLe9xUu7q;cxbljC>H z4#umWnfz3E>7X)c3uRI>E`UEsG4B#reZhVF1{1B**vwEP?9Tbz*?*wzJdjtO#f~30 zx=Q}7b^R~wCf#>0BD4lwVN2Z-tjNEoUZZvJ_&N<6Q>+c_c!w-@au5*Lper%5Gfd1a z;Sws?)wJ2KaO>k|KW1M!a!i`n0p9bzrz=jj7+%XY1-9nbUGZ$#%^gvthSWiLS?1qM?)SmX z^z+B{pyJU3NP9u9N2z40)2WWlr~bsR>cr6%5?fLz5VHTGKhk;8Ttp?(h%%fFzy9w0 zXL||%2PyY}TbZE%hNIv-mm+BoiXxp2bF9~s#Ci-JA?l@pvr^Qv{@I1s_Hd*Ve2`Lk zz^5$0@`xc9`$?0Cl%b~VCI*U^!%mJe95`Ix#H%Z0hg22?|2&x`}Iujo$jiVQ|Uvn#_K>w-Bu8t@~}K(n>qR7crQNp<((RGrF5qDK!w*=l&MyyShxxQu2RP(!Te~~kXHaBz*gm*9Lg_9 zvo+l1_z~ZAReUi|BjIILO0`DXyQa^=GN56h%Uyc2C-W?Jq4OV%6G;eAbMF=Rp`D@0FF0i_a<9}PLVYZew3T84&+eSH=P#pZJ}<76cea5=-W!2$tp z20h#ir$POOi(Je{B7gltowV4`M5HBtj4wwkWMtyp80jdoEuHksa-|~qXdE=EK-E=o z#(TRqHM*w?v#w7N*M0g4+<|7{C54-UB`!ykR*9f~dWqwXWF6}%ofadR4Fdf9HHdWy zSOUESZ4KZ&LZaE&#VwmR$6?72y~W`?RkFF;oB`<9%-9mH@Gf2Y^%tPxfdZAC-`>zn=VJ|lf3Z( ztz?%AJ}ldK7!8Xmda>3B7%Hj+%IGHipkKaT;`;{LnnCB<0ayGiQe!-rft^4Sy&6XV zRsHZV)HulAfS$oP61qO*w>9XhbuuSA|EW$Iwng9~U2lhay^H2B>g}sbIMrMXRg(QF z5%x!dEp^?3_l_<`WJkIEJq)Y5PR6y1G#NZX$>s2kp^#~+ZP+7*RQ$N`<+i;&+Oy5b zyT-rXSZ@IwKSato$tHX706m+e$j;6)JAehCD67Q2mgEV8NPVhRFgw~I_~%+jF%$Rk zVZN)#kwq?jmBo^A*V_tH#gfhKVdYe7{-Gj6Ym)3^lirfrsx6wMEhdgKu!L&~ZUV4H zB0AUJ&TEviuNNSe65_yU_m>*95J+Z7(t@UKmuO{qs7;n4iNK_{IMP*|@aom8f{02x zR#IqS(K<362hG*~Sdr5+rZFK01zHgBa#kG(lb0V*D0IZ$R(;J&czKTVxP<=;x}_`3 zet-Wo1J1*EvCRC*m23{`O!AEmWc9)TR~}H8bI3uLRIojIq_m&(qyy$RbIbX0G?&h# za@{^c%#;l!dnWfWlxvj`O*(LLDBo}XH9*D**+u%L&clXRN63o!JW;71h4q0Of6XMI z8-a#$Z|n3MOFG~&lA zJm)o0Nt90nRG&q2WCk}*x~#@%r;Bi6vITrMF8Xz7Wtl9gtkDaI^4j)GyZ$RX$;LKp zpjP5xlgS)>cJ=oylEV#lo-bO>`!quB(DYHH&LI;Wo(op%vBjJZc0ReYyXbTU=l+ME zI&mqA1hT=7?zd0aT8Q`zH`cg>>_69+uXXsi?c#a2U3zx-f3wa)pP3G{?XA}1g`Zcf z_5XZ7J6={`$f))!t@=wj@viZ*Y){)&1pKTN{hB3@4W0C;Hv=4lE{t5p0;2c(D7|*8 z|0d_w$yi!GK*=WY5J5kHZ!sB2oBr&_v+LGT+UchQ)n>Su7cLe;`|F6NtIp9^hPFe= z8C~!Y;gk}tLKM0pb-8d=b$l-U7V*+}_xLPTcbZ?3!I@3%+VEjf&fa_B{yeBRd< zvx-F6CRthrVrs`K53KOXKhEEhE)IVq=}=|8OX;uKC57VHC^+U8m=kLrwMiTXMNLl*$iL#H+hQC%UQeSmg*)(QrA*! z?mpV8B9gp-8w2lcEC_E zHBx%z&Ghk;DyZd+?I|EkSgF-B)4nFVn9dc0rKC`n9ndLwx2RVRmwT7|+DpATV_WjO z-6gdFh{GEa4oLK%LaoPMo{VlWxrj5WAJw4am>XZ2?z*XNkR0-%r;XUV3Mn<}1%8`i z_~aWJh^S)76!z#?+n3;jFR|GBhdmdvql!#$PUN<2YvssdR6fT|W28|iGNmb3t+8_U zH;`x5&G_^WDKCcd7d(!=sLHyO^jcWhkkcsoF#50o9nAEr=%Yi(_&qk+nyOr?9VydY zv`NEk^78dm3~Ll)!T~*$XvlPz-4eu?wi6iacZK z;fm8CqzCD6u8yY^7BV`yQ{VgoomP5`A1UxXEUV-Nxulg+`irI(o&2MO*yYu>8o|e9pBy8RhNL&V2lqBs@)eR;3LM&^At_8rNok*$EsJ8wctJV|lKrpJQYDn} zQE%Kt`*Qly^vpeAE>X<9E@YODH00k?5438!-D3B7VkxEC*Gu?B`;{*GhRfFKW;iWF zYCxFu-ueK|JFUQ!ulzT&%jn%NAv1NOP2?I+CYL>TJ|s98a@k-9$1+OP&EP@0bZu-; zifx`r`Pq3nIj3Q`+_4Ho0a?}2piSc|e4`6Q$htQB7atXDaa?}?Vdfp2X&qPiStaNy zEOV32q)d!`C4mYfX=-;Ke|vW{jAQJe<{BFFQ8#irb#fD`4tk30?teCzexl*u_qOfl zdB2km*OeUp=HvWkKcNd!ynKUwd$ZHBU+*J(2P40j9^=2?& z{0P1rI!^Ls!N}^k*`%DtGBz_YnVMjRQ&eC}WIIwwZM()v%Q?NIaqUs-!Y=F&vdpjT zA8_M6JG9l|&p-hTKvjr$%6l?yCm!L#;w)`^@KNaw`Og4V+=)HF-`csK&1-KpBpN(U z?8J9)3058Uao&4_wxzracllY?uHxK^_^iN-I$(O_p67sNOO|5->WOEx=jGZ{)wau@ z^=}%CTUzcNe)med%9Xz(WMA};d@Q0iux)rn1#e$Rp~!A5vYJ%wp1DwC`44Spax)Ea z6WZGs=1fNDRqOdGV9=-EMBsF3pohe6zOS!O;2u61VRHA;p?&MXF2Jpt9mR^zt@!&r zq`ftW95smwjNJ%P&@<@xGR!dX;d0ZV?f5TBNO^W#Im;?Zd&c$!^0GH zS7~#`zdCNWf5A8>4DUbaM->^mZb8TMEz7*|QCUrwpI_QtGK%;x(lhvA6A-pIa5hV{ zEkxMw^|xNGsDmY02<>xIqB>_E(s+ilT{)ws2Gs9w+d~Pvq_?n7g<-3VM-Gqjldo%5 zzwb=(K$osR(od+mKG7iUAVP&#Th0altK7#`7Gug?MJ<>2+0*4xWGB;Jwd!&aI4;Q` zfE`0&8tC+x#eIJIXI-|lEv?^5pe?`GmZ)#fpE~EqM2n9U=Er931A_aNko(DIElZWA z3^)OSpphrhEs7+4LPt&$ZS6-qYT*P>N`Ol6CY#d}Qt|){OhBoyW?A**y%DkeX_e*~ zl=Rb|+^$#7l0qJ{ypX_>-Snyc>7 z_QbfjDw+9_^>YA=OG>VgAmsJfc}tq}KRZvxkqJ)mh&I`s)HZ5q%Rz&TvNmk!euCP} z5Pn9(W67R#2}8@Cn{0KId!8;Op!{a$8CEdIV93oYTljJ+7GIQoPMuiJkJ;QhNi3_) zOA1*>kbw3UuH93Mgvp)m%-ls=-6ul9YdEokcqpdd2&PSR+0iRv4K7O$56^XAI<28I zVVFW+(-F5$7%`3orw4oGl9iV^Kg9ua6ne!?8(g>S6?lM+9#dmqPH6wtI!ThlDs03W znMFg)0bm|M2m`-oUrzDB+3&Jcmo2-jT7q^?uHaFEG=oC9pJMRBW z$PE{6ZJ+lMVhuD%QZ{OxyFH%0*m2l`)8B(K2}!nK*);M+5}*(rj6iQnn2m%%q0LCN)10X3X0hfb@zio=<3`Jx8CT zM0g#kYFE&|uS6S;VB@JJm?rkOoeK*6Y6y``_P6repA~nlOP11RaoTsZ{3kQ&d~cAH z5#7ZQA0nt32zFkX=1li~Lue>I<8^*o=?>2>J`1S>EF%;OU$JeFK=A&Hst5$0w}0X6 zjDI>gP*mvM4cf}hfww{l><*LR-OZ;&AZ1B%dnh~}r(Ep;*!pB~r;4fC=>1ThWLkmU z(%X7UJg*C%#Jdp$gu-p`#%4SN3`?`d6cfFzKx$>}bNn_g&Z$vu&9fr^`IkW#>ab-R z|0cWs61pTGeD~W@+KQl+R~#X^$G$a|OiD2lxzE3IL zan;$A!ySlwp@;}n{dxOZ#RczV3BR~rHI}onix+Y$fx8$+4vF?@KQWH)v~)Qj=jiA~ zV?ECyJaH5qF1?OfBz8!K#h0_#j&P_2{IMt_?5NCqdq1<$W@k0ymuI=5ziG2`C1$4t zpiFmb@LI!m^Yf<(|Hs`((ke1a2!GX|CbzS+{F>p_*Yls^V#0UVI4n$ox&w$J{3Qt~ zBK}x20eYhgqQ;>ItUgZ&pFUvS7&W~R@@&wABAd_&z@af3>5&Gis}tzuMgw2}GP!o1 zBp{1>a^^>;{hByPT;pQQ95yn5~2Oe zCUx}7+Yban?n8X8vVPO1^9H;1`c8t`f&gslavr02OH}{t!(MJmR8daTsKbYfB=sNV zN`eMC0=$O{n=f5z`gS3{RWFX;!)ZAuCnd_u`2ibQeEY6#ekwq4DsoIfmPfCY{|jW7 zmf&gXu_CLHJ&QEb;Os-J^vbj2?+f&s7*_Y+ypgeLH64d&PUt`m?+TJSDUgwML&HH7 z&R7!unr|Wwc+Ov)gBjvUan|zRJ-EGAW`1WTzwDdr^9c#xoZ|9Ngaad-iH?HBC z#?Pd%8c8OK*_U;1e^$1uxR6j2g{kfj7UK=DduZ-wSSW%?z){W6&uF&IpL3{IX?ud==LlZ=VUuVk=wanLVm zVwW{A)zi6=T$ld7BY+XUAPB_5Tsu_EVh4BDW-#-jXWWzc3EN&f4?wGGl z>_I6F5cOLJy7)(3?doR^})zq0s@e6V)3kW~LZG8A-VlfZp&VyGw- z7oMhoU$0JHG%p%H9vhJ!ktF>`TX(o^BAlAoTz_VP41%;(5pH}5S!noQz|Fw74=%OD zTf0tDowZ-TJuEuHtZkK4nf)gB1kOcv@s?VR;rd%0n>+`&pM1HY+;yx@zAnSIVMTuQ zS#2NF_Pv@Xw(O}ZlQNAzt9|T-aZ85nDe0NM7v$>{waER^(e5wsFvlv&+n+6i9esQH z7b3fcC5(Q!Jr?8~U5_fVN`{~@7eUfXpz3@5^xgX(92_-}jH} zze;Y8(NnK(f64s2pVMB6EwTNrKK^3!n=aRLy(~|;mU*o^^MH-Q!9pwB12#DcEtR#2 zy{*&x<$`U!&y9ZmhEhA$N6SvsCOgZ7*)Q<935e+%lPCKzuStG~e*z_?{!~)(+SMJW zC!*scwaw@KVL7FojRpd3z%dKhgWp<^e_B)gUf(#w$e;C*&*$32%uvv=kFni$Xca8x zu4}M+!x|IGaBR&JI!FGY=v4J!RtfUQ_pcn)uG;^~+Dyy;XMzuWH z>!nn|np2_qfji|{;t$k@u?}d|z8o6v?Mtki>A7|mc?Ku7X(m?@K`<-9!7+j3SgwUAN zeidJdAEpi;E=}3we^dLPq)YBX6pr!C78cqDi@Q_U#sqea`vD#6S~SgmlvGiF9Z=MW ziB!d7DXOerY3XvL&}*AIei0aD-Dz$A-E7>yu30OW&#_}2g~MFyH|7Oa<64+IV+9>n zodS-WCXMF}pnJM?nb*LKeb?i_sLy5)E-D9^#EIzcj+^d0BWSCj%PjEdf-c%O+Q<0~ zpREU^(cOlz;^45l*7^P>O|6ASZmRUdSGt*(o_qZ1+HMIoN)tPBv63ZBc#1TgVO>GR z=50-Uio4f7`XqcOs<7a9`w!;v%3Y5pg=~LW_TBV`oK8TidnTv30;;vG$B+Q($pUk> z%^eS7knG{O%Oa)<{Q1+wrF~~X5Xk~QL0aYgsF2GMZ}Go~I=`m9)=BLGikJ$4QQywk z*>`@$Y#IsQSod6QW!RO9Te~}?-Ug)X()~V@qoN(UDeYX`Oc11dXV0-yIM`OL(&Ru* z6nrtf3S^L&6GCr~Ldrux%5SKVs?pquMH@-2LVEHkWGvL17&yQ~h0%Q~x25bpgk_@P z7r3c|U2Qk(pQ>SxsK|-z!o0|dV>9^Sp_YP~?Hg{zlT!MVO++}+TO4+SHiF7@?}ahR zkw=HSPw25-dPgUT1WnX!V!&#>AU7#SdhYBmul!nYV;sf6F6O(R?O1vE(a5nYTTbD) zjCmiZ<$S%O-Nv%pdOr*OO0}$EuFa0S>@&7F?y6Rt468L$5Fe!mqf{FFS?|A;5+KPS zdYq07?UNn8Cbi3bK$C7xHFgaRj4FIED_uw0M`)}((-ie=t{mr(c3HutUc0U8W8cqOPCagZL7f(wkrXEX7 zZbq~c`Q5Mj6ieK|SZBE$4P2zYBV?d^hh`Mev6>hRO5Pp~5Is!FxsSZ|lKVUBz@jyD zs5&;&Z@dtX=C;+k+;WtYqSnnKpcl9h;iSj!2hV!IGSNa$sBQ9LPfMfabWTOawir%hw3dhuk;H&=tOxJeiCUV z$)g0vNJRv%;C%3u*2m@KHQB$we<$5xvtpn_3sSETb+R02_-6EwyAgdRFfq0D2Y-37 zyN{j3XiOK1+PbKkpVF(qSu>W9)Dfl;w<1c>8nmlKuKYSfM3T9#;~Ev=(sgMU)j%7F zs~WKa#Tl&Ix)4#zbTrdY9_o(%tZ4W%JxT<09a|aiR**pP@L>;eqzA_G3U5SA5>2AY ztS7#nCWR0A44m51z_Er!M0!~?eW*i|-eYq_Y(&b-$bK_8JF&HjY&~pakfHK8%rQ9J zb&k(*sQInlv!#?@qvg*h({V7iZ2X1|(dsuD*c{NH?I)B`V(@E@?#k_>J5~=5p4h}x zPx>qwHf@q$p6q=;xeZ%ZN{Tx4w4`0QYm){S<4H%y48pd{eqIqbSTQTw{(AGK zO+DzzHJ}8DCd{+R_t-fSRU>^?L({)d$wAV-3{s4oD9=1v%CyJwm!&>4L%k9Y*J0mx3Y^0sVb15+Y023;+s7h}pb3p(9$0`qnFZORmcD*pRz-5qq<{X=9}2mi zRxLV!M+o1BNHNh2(@!(KOMV2~xQW{K)or7@>YQEe_GewCs_W3GzVO&@m1lQR;HNslZxWy93e%LibA9h zGD;#iL@Tdo;5N^)QdEE6^l5LHV9Gki8*J~?l(GpXh zD?#vOIH7g6Hv38eDks`EM}{9`c4*;PUV4LXZs3fI`VAYFsK{{ooBrP3*v^ozhD$g_ zx*%Q%ez1MKZkM279qCb$N5P7%f+_J@@SR1su2Qo2wdu^hiLrLyLorcNM*&XPL17j{ zki$|~t!7uBv-$y06v*g)a57!|a^AjP&H=pS5w8ygsi2CU&=< z@Lgwp&#ECS-tl(RYg2cdGJkW!I!XNMG3ODt&BHGPHfzKrg^jfuhim39^fY{RYe+PV z8E&}R@v34uMTC6=4fJKWX0@Jz6;=&0stBl8iW-pSlK!d0b=40RV1a%bR49K%mE~g|6sHC{HHeECqplFKmDEmjn(o97cv_cE8{z zLwywAga0VO8!YGMUp)J}QRu~FNU3P%2gf@VIBDEU&$Gzg{o+ylt-4svle~{soOlq= z0PNc7sI%GSaZ_Xh>&~4LZxm`&#j)Y(9x-vwdbZNzGn%W_4g{t1SK&KErYZ~g<|NQb z{+3wS+2$>uv4@8JbVppjzJv|hP|4pVJ3$q0PFI9%TI<4vmq1lEG2GNvSC4=g7AlW~ z^mMH-NypEHBorFR0>M$pKjJT5w1u&L{Yq~0XQ}QYuK86Y4Y$~06{DUVP=6}%lsJ36 zZ#;dom6eWOtn#sf9W*M7eCsWK#(S}n`iX=02a(7qYFyJ+cebzGCwRBrHhqGxMLw|h zt>r~y1zi>fcKagE;6Ed%(cTkS;{a;EU?d9(mw%Qwv13x0y#NGE51dd|vUdII?D2u_ zH7hO0Ej!d?+7@D5FE@n-Mj5@#)%qu?5}7M#+Em{+7M52MxUhj8@nw~`%|Fz~duBy# zUPSKawVyAPsN1FJ#*9lP3bDS@mf<@6#)++ygA(=cPBqy^1wBt1P($!^#nBt~V~l)(?oN^A5R3-o9~bI4+rfb-wYa^o3(|9LKQDF())* zz=0y7GUPA^hUd5oI7-|@)!Zr!OLA33e{06&8`rq8zJYhK{FjTj|2~eo)vL8C*6BSTby-7R%^TeD4zlQ>T3~(8YO+AS6!tr4ak_5>|(JwY}Zggl-gRfHGjR7fWqJ|d+1B$^LvYOsA8!nifU zG{rFU5AZx#evui`Tlkq?|K%}1re30W0WCENyk3zhN2vHE2h(dD#_LY?g8)+UbycVk z^OV%gRC;D+o3cA&2oNWgdR-OJCZ!Rm`d};%B&A_>cs7!&k2Bv(`_gmxn-)*;ks(9|tw z#(Le>t$b-HbCcL$pbV9SuyY*AO_bwRfaP_B!;IlFz=;wIaTSMVUsVyWj!{qWWE_D( z^`yLKs3GT3)Y~ng=nI{c?J-P`SZr251=(W7x^ifbs?o?x{P_DvDY)PA5Hu*J4o2@$ zRt?^F48*6p9GB5*;jC_)ZeAtv^XUsxM<(bbTIg&iidR}Id48s!kG)L@AYd(yzlgoH zk#{#M!<*P-U4fuTbQG%Lpt6u;T9M{_A8`s`Zu=KUtOwfiT205P?}Nn35_mr(MalZL z6-b*&Occ~j;W)O_GLIB9P@FfALxLZUIU@NY1arQvO*Wv%M5!K{K2JD7e1luyt&wvr zqBmG$k!JB}WQ8|fjr-Zfl}1$8TW%dCq!_M)CKtE*LQDGsC)1uFz8H%%UQxiIq$)2s zQ#lz9T(pN*X8IPUH2fx37+xw^9wP1LRM-y5Z&RUYuIvkL_yvpiFCLMD(&2H#vGbOSgY~Br~5>+<3c&#%X6itTDX&Mbs6kPqpNPuQE&}U_t{K z0qJK#E+G%DVA2FC*jR|lh&GQLKt#*+(PZkJ3Bm0`p+M+Fy9ETSCZ^i|>XVhHn!x}a zzbW%a4wfDxxlC((z~)PW(7XqaQ=jZH&QD|mWHFHgXH4&{1T@#~rk-;lfVQD~edG=i z3yqE`qWJ-t7q0$a_+{i>s9f}AeAl)`!8 z!UZDx60vPRNXF?4J0$Lx(Ot-;d+xkN1e2GNfAr-vOUoVPpz*(0R=5me!oyc(B17$o zupH7c#rS(f$&M8~ks;+@b4LBK>OHLP!r#$5<^5|>FaKKXTVCB-avvHwn^LVF!|QQP49UiOG@a*hlrC1fo> z>ftf~q16KiX^ST0|FiRWEX1UF6aPo|i`ifq&%AcGVQJ%8zzw@g&!(=SSSy0Sz*hVm z25i{4R}FuZ96lIY13b%tD31Q$951Tfxpx25#}a^WGrOs43FSMl?X8pVg|8YPp*8FW zg00<#IL5TPzrxN-R+4Dqi2efd*}Mr*VqwSN+rEHUMvd(_kGvToI7loEh@Zv+=6~SO z&ML*vuszZ9@$vb>#zf&zn|3-78no{1k;PPjMpBaoEAkqe1aU>S+rGafayKiPft2_9 zwZmYPZlqR(iX}sQm+;&x1>^$3zBkzHSGTatNVvL#uR}D~?t|DxY^yTCHQ=j~-bH{u zpy($&t}e+iiuS<@1XGiqR;=4eYWT9+-hrtd!v+G9Lq$&Z8r(27BA z@&&!VnwWdZ?${^=`^5C6`COR1myKMNHxm zBzW*iavW^dCJTeu&a`2Zz;;eeENhs2vV(f2KV3)~srW zu|bS#SZ$J_lp>)F6Y!B}49h_1T&6##Dh;tZP<)=u~(GkW577)A; zz4$&43r13p2Sx@Rs!=;M#RpOay#?LcJmKtqgRbt=9AJ z;o9xjgxu@lSXk^cGm++RnpZ#dfbds1K~jZdb3?sz$SGAE&ZeDsEEMSUh)A)dq~9QKvb@Se5-Z9W8yp#Hw@&t z2kl*R;ZXU&>|^&$AAx^AoV{J#m2{Ev<@^b!GAC)Nk+zt^uGg8f;|I$BtGUHm&jpb( zC##ODO)2!JLeELPIz|zEN7OeResM=+pDeW~Ju6wBWIO}!5)xLhg~<>nwk&`hby!^j z_x3q+ty#%k_bA&cuDZI~dr-=0KtXZA%^pB>e0qBAq#jHT{E0~K>+1K?F8zsb((QkI zUSSO6XqLSrANFj=cHH+)zRNBxX(HbE#@3h(g7?1q#87C#pl~K8TZjvJN{tmT)o}dG z7h?v!cVBz%EPHVcE78ggVUU={K^wWR_2hOdli!Ea3m$-jWxb4jWG`z$%q6LBILUhN z<4f0kp&g?C;5es^Ice6Z)&}?EU1d1ld)n><9mR7cT?y0tyt!_fzhZc={n3ApWw>>^ zxj8^Ja>-YIMLIii_&~Tt$0eb__B-p@t-kBt!|V*UYnVfIf^S+QjdERkp^hcaUIBxg z-QJhPM~a~Le;KuKMz}GmLWR3D)d6|jY%>m&Bl^(q^9URc*mQG>Y<2~0|IxB?jXyINCzxnJux%9FmDT` zF?Z?C@^D}O4KWsz@<-@QqWy)i9KJZlDD}JHMhPFEr|q6~3(fru$)#;mvB}1+1kpXzsYiU5JXg{oXfse?R)|xJTI|->7bR zPj{_LKR?}!@@sQ>P4~Yu*8-sM=$Jsh-vi-6HQ`T^M3`ihWAC@m3y&@>kMwm?YjU#a zYZR+hi*~Z6-zqGdKlNeH7!cZ7qwF6+2B>s+h(`o+S&Ni=NO3JthPZ+s#5{Mfw0ZRW z{*$`BqNZhArGe7i^)O778p=FEFfts2_uXx9GJhc#9%A1f&gcAZeKu0}a!L`$mHZko zC8N@pRVrV*BzymJTd2aT%$JGv^;s7UsB22sJeZNLW@@xEh0>R*C&{c|DA9s&ZJ5Z|@uk?hc3)+(! zC>qrSjq_S3n?YlC%Y`bLSrt_V@s=t*GEv1^A9HiwG)CIe6_kQ7C=E8t2eOig*e&T{uzC5b4CITq?#iST`xHE zLT2svn={X^aBTScjK5O(*zUk>?YWs7_Z`_M;Jno=lzN?M!M`p(f2*l}bQ#6(9A)24 zV!(qvipx~qy5Wz+)>T;^2bP=CEx2t~p!@5?!H|DHEf!&qc0?Q+8s;c5h83prC%#$rmjI5{?Xh0QBcs3oq(HU+%Cn( zMy_$Nj>s%dR*XVhftD6Fv|}FfOs=Gqyo%@z4+xFstJ$A%Mie;9jo^wW$z#CVrk-a5h{ag^AGOrUOJ8pp;`+=DDo<6!udP^?W`*5Jq- z3IMl*uAb1=!p{(w_9)bd-0m@Dk~RwsmajA2&Xb{&Mw@Z3l+@T13RQ1eZs~Ksd6<_IN`(bdt*O>A0>99XyXRb0xMhO|x`!b2*s&I$vVQUU3m}Rp=6v(tvT(|B z#O~XVbyW{-S~6@-PTS^;_#)c#Z+Q59&6-?mLaHL#W}-@hOQ^q4_P{zb@7sUvkYhc zR&0J!^BRAGdh=9o(v%4hS?>Q^q?5LwG!`OuG<(BeUDDD|V!zn24z|MP1`tO$X!qm^ zvcBEsC(cTd9}4t&xYuL=euDs-FfR^M)Sz*S9usDVykvvXNZhVQnDhgF$q@96Ij)h|zMY)`y`4TEcZo){CE zxb6O2Hw05A6E`>l`pvGOL_MA?if}%;NGbniIWRAGiwx7)P7bT(ss~4o<^mnk&vni4 zn9S}l2TWr9+t3GzY(tdd0|$H|O^Zc(;UOU**w5}tAeL>UdV)EO!shwpcDtf~P&c$d zq8jEs2xA{u6596s%)L+=WO^^|^&v^;q1JCmr+e!f*}SReN}UE44Vg<^pv|G%$x(Br zi3y|iC))fNaR4MIZtA0)H7$Gc90ql1dNFGtyG$g0aMG@0m88dyBpi8^+7s8l{)D@YE<~O*X^9U$Vw-k*P0L9`)45pyEN2NgJ9| z=jfgn@ev649e=k7;|885r@(x$SmvErOBzTh)f0_!^--rJ11)?o7FP&370wYrH|x-+ zCV7H7ff)-sFCEAid<+5YbTi^p{h8p(kFS8-Zg(HMpX?5A+ciFizx3_jQWhp9*7>Of z1Wd8GXM{A`Kt%?DHg;yyIZ|~MQf9!kQR}lg3U?atq2CLi-$rasaoR+Yl>9`SjDice zztCP$?Fj-y+WHu0bKIL`*PZ?6^@8}fK6HAv%c@7K6K^8^KbR53*9sn~7e}(2fBaW( zmGb~!qhxrE|KLd5-x&a!i4T^gh71rhqrwR|DN`lh2Xc|7W2&`h?^|$q#IoTj(#_S} zG+;eC+7D*qrJMR3TkVQ}%!S>fR0dtyvIi=-#bUJfHCv14^-7k-)+XIDZ|frL zOC+5qT4Z80*FV&JVpAmdn#>7@_)B2t&>(QIw(fYS1v&t+BCuZG1gIbV@K03P%|!@Z z|9e9hv*7!sa(mpy8YAfEs>*P5orqTeZ^+g?N0dnP`o zhJ-M;!M|q)Or}=|#&_KB0nP(c<`l=*{H4ABe+*yhNrfBvdJf;I1;D_45QLvtfA}<$ z9DzR18E`D=M!-+l-#Z!T3pip$8x2GbQDjX;nK#&@Zw9t9?@(rgrSki+#47Hreii`8 zU2jzQiA4c%igd+=+%FvMR}*Kp*uU<`&f|9~k8LrGLRH&Gcw$nYs4pzkJf)Q z;h=vUt{j9~wi@XN3AUdmn_#f0Rhgw1I}-y}9USxS|*oflYRUq70wiDqWez2T^#K9+0&v zr$)N!@;yBjs|gWlj`ZK0x&AvSv zYyvdEazwh~Rc6M7SElwez0;^HZju81fP4sOEyY%h(rti4_jo90}f&j6vBB#lz`-`Yq$EI-xibr>oB~U#+OxI0lyDA2C9s7~9gS>TmvNcYVcZmhZawcmd zF8S=3hnCJs+TzsgK-r&><-7TJm42+W_hn1dHauxKd0NYeYyZ3d5j6M%B5tDDLH)_r zjLd{)pXNGg=iLKQHr#EgdqpDZh(G7qwgZltH-wAf;zS&xTw&ob4*Xx#_01V;gE>ui z?B3*b1}tl0qqXa07n*zhq^C_5Lf-4E2v2--uc8V1Od1-*IA4!V{sh^s$g5et<#fQ{ z^MR9!inmBFqW7ZAEOhfrC?zQo+q8=OYo%A-@hWU)+feTbEfd8ofwkCtY?(=9;2hyD z=iAxW#%m{xkrxkej~Fg;iF)W)8uX>1wM(!Fq5MDgDAo5{pUWMuHy|;wPP!p{$1CX{ zfa3Mv1Z=kV@#5qM$N56r{zZYB;pkc0y<)bCRZ9{$h&N8U2`arJs&y{B+{nYjHua8a z7}LWjNWhJVg?+hr*Sh6PdKct#kltH7bDyZfEu9`jZezY+1?Gc0EYO{fJJ(CQZIS4E ztWK(g=J?pb@GYS*LviTnU`str&c=wKM?phJj!ZzBLvFMe1={1JwYhn6!p~WdpbuGI?rCb&O8`9e&B(57ToJ+eeE+v?n(Ag_X51TifU6uVDM2E^C zqS!kqdEU2C3$Vjhz~wY9EPB5QLvLT#T+gsxzI-`tkHaoN1U}nX;9n!VquotIx#l5g z;!-17@LR`Yr#lj3-ZC89;AOP~5Bn*cX#iv_@{SDcKXH?Ey?bTzDkpIq`F3D=VuW&V z>V<|3(8h4wgkuFfNq=t_ASCPNI}rc6KEtle3t{B_o^m;O&xAxDzsvlR?1nKt(pI;BC41RBAgzh}^ltX-tQAwt{h~6 zPrtVf6X6SNSaqLZR!lTXz{ayqW#a?VawX&q5(gsulJZ@q zMtHMut6N2mA3LqVJob%Mf<< z9w&`KuZLy>%bs_M)6_+>pxHZ4kk9*zjvTyd7aO!HW`z9q)`@Q?NZ!V)W4L0c73%IC75FOE#m z(Qp4WTzL1FX*lb8QSOrmGlx+WS}iY zb9`l3wNytox-;C~b&Dc?wueB_uk!8jOIH;d&|EcDejAk*z6!`x1Kz^z7)E{ zw!QA3{ZNc7rU-l&n4@h+-e((x5Y0NJGNj2r@d?}I4DiQXf0kg>7#ByaJe#FCdn*_d zxJBUB+vYoa$Y!{8D~5gX z>-Dc}kv<#^OSUfY+s#AOC-7m}JRnE~UjL}STX&wE^Dwo@a1^{PFqykM+I)uTaOO>t zhb5{MO+4lJPI~YRi}$U+?7va=-r-obfBg8}&=8s^QEA#_HAKpzVPtPYnRg@#S*1Zk zqC!$3*<1EZLv{+c>>df(kA!IbUgv%F`5xcj@4w%1JfGwFJjHdL*LlA8`Fg)!Y0>lj z#@aX0qKU@U^rmTP-*++YS?Y6sZR< zqdrl#%xj5!Unpe}BF+n2Ev)lZYbsqhELyRg2{)Y&jdm=VNrA)a$l?s7SPRG>&3lsn zx(o_p{DRb>e6Rj}9(aQ0No^u}PnoKgmnQtLOinz17ef?(7xj^jm@XM_TQgXCKXd!> zdlU(ey`@~Vt(@Fw1!NRQys`_7T?8u(+H;6#)+85n7#~@2=!e9N=b1q-K!q`vJs-T?-FZA^u z5Pdw3(W#P3tP#`)E&BUV@8VeYq)p}WOe79%C?ekI8 zh+=Q98;2!F+RKUH zIfIv%tnWfkr@+7IVLfSy@X6q|A%d`_Bw#ZR7_2CEou!jN?$fGVyR~P#1JC%geeC4M zw^FBc=zOgZzDnekORn=xBE++A6&-vZde8z}+ zOv>F=&Kb_~m+Q_qmJ-&JH|Hx$>N*|Hy%Sw0KxQig8nXL#Z%5}LQbR1G{N0;y|D=Pp z*(vRxnC9ejR3cyKXe-{j^dK>qE(9u#nXWYu=IraMZeQ#BpT&@VkpETr9ot;Z4JPb9 zg*rDaz~{@8U6AH4A-1%aK?^<(T41J zfz=p)4xb#gD;R%3<_Tk%x50W+f+(bj0oQ#HYkfOqYQ);Fx)>A(iz*{--asQX3>sq`b=&P7d+hjm3wU@Mz zhIt*%b^nm-{+`rpt60$ci=h$p$w;nm`gPd+57e;;mn1AHElz(rk$A#Qgjy_*9-pg| zmGF`^Rg$6xgp+X72kNoH?S7pB+x;#bH1hYd^wVK}rVMiSn7%dyY=jL5?0XC6_SmNkYftZU~Pm}tE#t@_jzNBq2%5{6@T z-Y#yxd1Y+XESb$%#Z8^1(D>$??%Zy>H-wG0QuKiT_I4F;{XNh=<$1JO4E4t&j2L2H zmM;nPC;QgvHD1=Ch<(Fm)t=l9b__3o0b#b#%rbi*=iI;IkCBBR*yxs>#I5;vVu{hpF!Z zVY0#9>SAGxV(u-PVangt2*bePHfc3ZeuMRt@wf-!hv=<22T|o%Q!8y~12fEh;Vd+0 z(++aF5eEz&x=+LjsW8`6mvrNU!0Ade$8PQK%%eR_9qqc&%VQOsPDzB|(QaP^(}_m2 zr`*ynid>dDAJ#XeUJi%2X(P1>wjus1W(`ag=%c6HD&!`~mG8L;{mlPPTP%CP&$)<| zsR!DWJnVUDmibtG#W%5+RG{@>#qDdH*(cLwojkP07a>ag%ECM{7k|sOfnn>?ZZN)c zk;vIscM=B{almgrzk!KSB2zIpSyRHGQ&OB609(pFlM~9+!6UoIA7$_Uq5umsUx;;$ zH3@@a*{J8L6kEuxlXTm)_P6%2?ygQeT2Y7)fPGqN@G$?GvVEeizvU9JIWedC`91<= zYerq7d%l}`$srO&8sZo1l!Q)>vD>4sT0-*+y_ZB#3Y7A`oe;|lS*U8&@~~*{ZKVff z`gCky{t3UCZ_(D@cjZDc|H|)KSTxJEWqcjX%Ol0TL~cM_3RjW0ERShpGbjr)fVgSA z`@ndNnqKNn$)|YWkv&dvtmt=IgU(Rfqu&rDk9n5%k6$jj{ z1JEQzM!sYC@TR`Mueq$RsT@`uqI0y|%qC~AzUW_8Ra(vo^xuE@*%&e??5aWDbBgkb zkNPlkQtU0vozpd(FYTRa_tzN63+CoCW9S?-6XTDojJhlCBXzyhCnhyy_8LM>e$Ur#}}}WA(EiDH~uSlkStCTq>OJ?*K01lPEZumht*Tx=pAnk?kT=VCKmg) z{FOSSK{~u=#2cCV36_V2kSKjS+j9xSg`j$`%`tWCd-9bTZ{|=5&oSX;p@or3mx3?b zykpLY$4*}D-JN_}uee=h{#XsCvi_aRVP5EqS1hCAuw%j;T*3&Y9H$7MCt4V_R%bAc zXGz>BN#X3&sSTVg3E0_P5drNwvhOaXU7Qp8u>zT_Yce|@Hj^Jpqdc5TWPY8UWwd5k zPucP}Ube7jv|!BY^#&kY$+(IQ(ict&3c>o+26}9JvC4c?t61~fsS+tF;)_qYt@ooQ zAa7aGK*S>uWar5K{ZVr7W?d2>3$j!gWwNcdv{X>h_8m$ zIgK(GrLDj(AtEj?QwG~uLK(6Wi?#kv&Tjy%WS%yYPlWb4i|-C!`aq209g!xV85yW< zv<$V)-FD{hx~F(E=YxZ!sJ$d$A9h7?^Nq30Y5hb2jy1wog|+PqXzzF^A#qx>h*i(7 zaKV9bzMYILdfV;C%o5C|ll&>TV!w$idt?{AHDJw=z$Yv>vaB8^T~O{E6=zVjujIQg3oRN7`_x zI0(%OefQ3)lm6d$lhg3kf34`+`? z{}a~J-BmA09K+VTiCcY&qrF}|ur@3!PDfksUx{*8QHqls*02sVb6?q#zokftti=EZ zixkN)SK0cBwQQGw3Vek8nuM)P%_`VdK{E;3e-|%%>k)I3};}fPM^xa?BJAE^!+vvQ_eT;;1G8j|pWTN-#R7=Abq7xN9d8fFVOyYDK)TNljCxYXr2* z&pGWC78htB$xRjLY4LfVMaMs>zhh0_r1>v>Qfxk~MYN+zjz?Ybi_|J1?Mr=;8uK9o z)QX%Spu|w*xy`%y;QgF+EPnr;?C7nrbD>eYtdqBXE~9Kk9%U;$>b(xD6>GvtfWx#> z%Q{E$)l>2T2$jPGIJjyh+#1%iuS0o$k_fRrRk84>FRty76y;>OY3eAqS&oW@KZirf z&xnlBh1o-kB0E?*(4NTDlxP0T*vLcF%Fp|fMR9O(ot=8>p*!cd{@tp!4Q*qdd2D~; zB}t6mzKZ%}dGiPFE#!>+`{=#oj!3Zs19IFBb@nN%D#axmgdUGq`*;I;V=P7ecqvAs z40j(`_gm*GCb?M4xdFk7({q3)pXhT;YrcLtqvc7o5;F#BVQwyFV~GtdO!Y-1gaiCUB0l5% zGy77RUoCp({wmRDH+6Z>HLQoUjxq)6ozR-a0eW_vM0fZ17#jYcujX=GUcBo%N+&Aw zx25NC_%ZF3{6eB?b}}Uaa@#Jg`oF&2f_%GsQ9C3j`J!tsG(sC+kVEBbb}}_eeb5MJ z_N3J4x+xi)h}HkTU#$b9ODdg*BH7o`x#E>tvv2opa)Cq; z>X;IqoSLc&P%)v_#bjzah@Dv%ejPWzf^Hz(5YvIdCjfR}PV{Gd0KMGg^c)p*;~Gi# zQQAkIZsQO7JR*#|2uGKkQ_fYuHQgHd#&V5(ky%OT7Hk@8_}J7c<{=BMT9~6kcr7Gdb?R4e(c%a%YYd>)HLUAWTxy ziavtm6cOr90W>jh*S|+u>MIDnuh?iRADH3II%!$S^wE#d2BUuxV-Nr{ehOC1t!-{i zZTa`q0$dw_kSHY+6u0QIR0E_` zFJR6H!x#?TTKfbr$5=b7`uwv4#|T`Xun~mZEqkl*!vQTB+gP@!zQthAb9lM!$RrB2 z8L82j%>#40OfmGmO2z1T%I>kXuI$1rJtClGg%%zMAf0iDraCH|4ty>}7lG8<-CKH= z%Ds}1#)gxtF|egxp>7mYF`bZ(`75}&E}H@OxbptlY7HX3H+pt4Z}4$Dyt=Ff)BONN zFe6TaVnXC51>s^kNBRM5pgyCpM202sUZ^%P0j- zo^-K&8`%HH2yJ1y=$T!!WMOmG$-@|yU`jB5q~nyda+J-a8)hB+V0u=4yMGiJ>xjWE zIaf0X8kEpFx)OoL?YaSZ0;^v$ID{XcdAT$KM9pQh{p-<5&_2^m_3S0aZ!dKhBO?+B z6fA{6H&ljg=hOaWJjuK=+$JfC=~TpQQwnfYFGmFxP5NZ_;Vv}(^9zSCgRHZlIQxLk zoOA#ezPGEYsC>g!>L6ez9lbxUv8{_)uJBzE)?l)98rSe&pr5L3hWALDF9m)t1};Lz z`O`n>I2+Fj0v?f$J2L_h^2q=U+msB8Re2L}1XDf$U1BvbA#5k;VT~P?!d+P`F2atM zqtL>AEoLGSS87!3&{*ZwaIT7#nT1U1N02)$(zWG)B-poMK$?ILC9Q8i+4`}Rtx>V0#6&k+nfp)GR`)~?e3xv`;Wi--j zObfUL?PMy4nUOf{b-0$^5Q#jVW=ee)2F7PX3d3y7@mljhjpKRBE8WH}3C*rF` z;qV^2MLsht5f_GKc?Ok81NM4#6thtb1Lg?0ZAOB2@)^09+*ShEP$Xg5Ry*%GZisUW> z5!G!!KI^rInDOy3!XZxoGFAdoM)Ww6`D0Hst3LdOk0hOr)^lS|2|*Y@J@ps~Q-YTx87Za3|g?oT@Zey7{Bai#y?~VBgI`A$Jn3K8Pq# zE3prcY&c)7sGVnTkS$81abW~;b1WJZx^+&SI+aqTy>fJP)Y?3QC;JBv4^K@CVDJ@2 zfyHT)wmcVp_bk8o4tU732BVHnUJn5rY+V<)m{x9@tyY`OZMkp7ebo7QvXOJs=t^0- z%|}kZ)(ef-<#@z$?V9AU7RouVkYRS{((=- zdmB}~>gmaoCka-_G#74q+em1gX`1(&G@K_qb_1e9Bvhpcalj6F1?L;@lD-3?HU6Mtwom+`5y*1FrwtkJy1LRL zBO~n}PXaSoykTI{qi=FnCC|@8Q(%p*2$xyVJQ4YoQZYT9|Akd*2`9h!6s;n3^e*on z9u;;EoAqxGB^hcb8M??X@}>94FDdtaP&*TKSEo_a^T@HmP+y6mpqg`Pp{yAt$2LFu zcfiBq#;3jSKE3wx>|3!Sraxra;%$OFf%;ZItEU^*uUjW*k`YT9L9-0&CXET>9c)(N zQQNhrYst_mG+}6XdY?akzVT$=KF~OkUN6&JgXeP$LUNnXWs+-3AfA{mU@PQZL%L(FU7qP;86*RGt0G*P1NPj?VHe~ANig6vDCIlBt&IfpSH=U zqg_?@H%?j|XZICa-TgxEKSw#Wg=_-SBc6XY*gI1*P=X><%~8R)=GRU0MCO?;Hh$#q z-^vxM{rv__X2-+&u(Th)LXE^;4os>Ky9!r76Dh@D@abmNxVu0T-Len5Uu{LEE1jO) z(==Pe>UUXT?tM_Qu4mt|eXk_G{Tja*<&;kIjp)h$$=ZIvKy+kT@dn(-v9dt`GtcaH zkq+BPjEEv{yr4#cP2*So^IvJ; zpnNG9q$3o{9BvDMWoQ3n{G=;E0D7XA)-;z4uRM;V9|Gc@hKE-#dqmnS+vx;0J|XV@CGgv^o9V=6SmyF%}j zz5V+SWz)HWkM~CrUd(4wQu%!Z{so@8>^|qscQlohl>FqBayeO93~1)&<_#kT7E6WM zI70+&^7hHb_{#>KZvR@47^Bj2H>Rvj{N!<=upHNxwKuO>8F!{Ej^2BI`FsiU{-IyC z77mK-LoSEPSq0Xq7ezMl*y~CDn?BFbxPT*P_qJ6xW=~tb|JP{&{jHVMPodoE5vyKZ z`qHMP0`PZEUHmh1k${5bD+#|6sGnTtstzl}VEBB!aV|Y{)s3rjb93Ja%!_P)ddXLn z2ID#c9|S&ke4Y4OHcxt=_sGeOI@|9#Gk*Vg{>P-+%4NvBb)Ws|)B0!6PNu&3Z`S+X z?=~gJw!Lfe9Up$gC!V@3GaUE+e%?Ng4oknU{MM`)-p|*Iu?cTTu4~jaF=vw!)rqgK z@SrhXd9CoB)gGLQQsaO6=-xfcQkVDp*BvW*lgn1Et(=!BPbpZJ8EqR>Ls#YpsUmScM)=SH4`n2t+xSpHE^>n!FZw^{GJcI}$5<7U{--rNV0} zq?FNU;ZB&`r|{+X?+p{mpLcboEMl`R-8GE2N6)=+>TWlF85Ml|ftr9${Lp+?y)ibL zZ##m&h?8p%wPmJfWMpjaotd5vOEZw$6cUS*)Jla^fU(d-eed^N zPJX`A*+-y8Gh47=L3%qf7>`lJ|4l=SX}*4mVwyW@&wM-&6Muh=I(K6Y=UIlsGOWW| zt;D%jTCKr0zsTaTNR!veJs{*jD4;IHO?P#3)3+LN>23b-L8B;V(DampL-XkS>e;?E|taD8$Y?%8E-$S={l zhtU*ig!Vbz*xfx}J1OL4Puu89GR*y1#;%V@m=~L2ki}iuneWKl6B7~+mzI{I<(ZwG zy&Y3Mau5ZCbR|w<>gGhVw?dQ~&}c`^C&{3V=HGqhzm*|iJvl!v5}VaBUO2FU43vqS zJ9T`Lhe%WSG)_yxZ0d>Ul=U2XnR7gHFaNXY$#1nkVFS1NDs=@otsZ)NBNhl>{;JS& z&+Y8KEdEzgUJ_$LwYDA^H}HAl>1U$1hb|*cVL*}QApCfHzVpcw{;6-@)CQ;en-G-b z&>6|_erQ1UqyvCy-X`H}asep?Vb+_?xl^mv1~l*#Z=Z<|9dy(`6ehuj~uSBEnSCPzW4 zD`;}DQm$L8?-(8(4*#(;N^XVP_tlc$Y9eKC_hxdFdSPhjqq_rAhOHc4VXGY3Wc0Iy z5%g@F$4_s>=!ROcEDxlD_4H1c0nCsnozBg8OrDJ~ofFNF4|)4{tO!h`ow=Skw%z=` zIlcXiTkF}Gz1aW$V})&3sx2C)cH6OVP908Z<-~fy4<$2<{hn?nf#1>lOS>YWzFlZj zi=0D_C;0s1{--;+FO#;1k-92WuB?j($WJ0=+x#c2!&dWd*|#|HP@muVWm9Qw7X}e4 zslFaC3riL=9Pl)CQQ@iK&?CX1@3vFV+O8as`;siPu+0ng_Ce&G^>gKm6*TiMRB#&9 z*_9ArX+@0tp>nGX9btvZ2My=xpeo$>qqVaC$$Es7c{fybW<{c66lS`8SCAVr|* zzBu!?*z?uvwkrxv18u0|(;Uc7wGcL9(4^Pv{COXAA(n(Ir&1wmKEQ-eH$Qy%@RVm% z**N<2iV)t*6LPdkm95|ZW22S-<9yMSzg9jVQ^By|IH_ss>7VZNcgbG-@Sr+IKEgA5 zE?J_L&GzbD;XveYl5MupW2BufytXwbAESyjG&nfXjAoE?ELUDg+J?+LS?+TL+3fFM zh;MiI?G)-G<5VGIV@V{TCB0_J-3T3WV+1=tdpelp*=juO>C9w}J1EnEuQSxH8U3}- zc{=*zH{o?>J|F;SMywW3idLTB@E%S2gLa!{sKU;;V04y=H zqa~VYUDV~5i#^9*8W+a4Rp##cJ5TF{*W7B@?7$}T*5DyTDBxIO>EbPJ|IF(s<}-^H#I72F0z6-L6LDrm$@Afb%< zaEYcsEExf}Lol(h=}JpUJRGt93IFRi$9nIo71>sPrWx*Za~I2%xg<_7WW%fl6-G{BQsnG0jT?(s!XHGjc=Gf0ybnw3hQ)z714 zUX{MKV?^1m{pR9M_mrUi!=d8G%Ie6ibbNeg_m!y^o*#OnIjzPI&}c&CXLu_gjBocX z%)Ic+v+G=wdJ!5~r-%p4&ej<`XN<|Jfq{YTkU!?|a>5c$=qW#r9wWB`WOU9HA+-j5 z37KHR`0ybiD{JRbZ4oXVFRxs_pgnUo`7o<}tEFQPgmM&he9eS)xJ9;w~TOxo~t_smp{=e^8QOr}W_3 zxPwOrpBPL_v28mj={Uw%pvhOVcwV@8lgA&yu$!KipIIDzk|c)$lJ|TnvpnVDu%|X) ze)y{N1)|4H^V4``RbRwl?}Q#BKO;>61I3|6ZT1O|A-8ga`0hvJ`Y;`LBVN^ep8j4a z8syldFS4RZ)6GuRQF?!})lR{viseKsrFMq%o%2JG(J=BNm~%7uilPq|Sd*)vR{u=# z)I{;2HlrNObau0sf=~Q5%r#j+vv|+ECZ^?W8CfRWf7Tpy zXR81VsT_PUV!>Gq+n4C$r+r{c;YtJ1{vZ#V<+9g0yw0Mh8cLBT^OSx364K{4gLSnzm*Wjg}6lR{qEPd|I)uZTNN{R^aCu*`6zpBGk zdo%^4^A(KP9q>+Cy+Pz<*3WQBKQ*QSPCWYS!G-aW7jT-7GWqmhb0@jjqSA2T?~_?Z zqd)x~ZP+~0@QtZkz7h9CZDb7+7 ztKselsw`plg>>=(7GumSbXrq0@98l9-q6eZjB7(?4T2%8Ja0J+3Z-x?ekhl+TP-q` zi5i@&+i>g0C)OV?^+ph6$6+WNmJ+`Gd7pvLJH91{pz9;SDYD^wx=(ZyZBF6umbGi1 zJ~FG_t(UT_rP^ma^hwwe)lfMcpgnaGHqnC3TRSq%a&J7btQPbb1LYIVF!|;&tt$_Z zH`^^nq+ymjXHohy?D@^d(Cly2YNae5ghjkmPd6^-5aN2p}^zvD++` z_mb8qc~K+lAmi5=l?5}_$Gpd+N$GMY>VQ7+)Qi0jRx8-dB>YGr8F2aKd*Xetz0Qo7 zvc$`7@AOWw$<#=uk>{1)r3gRkf-Tq519OV#dG^BV)QaeX2aVfKpL=?9uJcF#*7-}@;=L}ugo9>g=Db8c1TIMyV3K0Z?-1BIKn?8Wpj zOTl$Vis)MA&Dr;HTH$h(bcsN^kPM|8ORvx?cN^E(9kx;DXdH8gBr+QPa&E@Oq_nJX z_^)n_!_s_R5@UrC&M#zC^Rcnd*lP7oEs-tQa9?FxVE-X%lS%~l;6J*ed=PoQ_x19a zE>e7a*)2074d?Qi+FwqKhj~0of$7HYID|e06p&q-)h;7HL8}Wuo~W{IW@srp{|vh> zU{!*hx(A3CN{Ti4{hN;ih4*ca^goJr{7uE$fk0+4n?k5jn5`dK~9OKDkkdcykzm&RMt(knqBHxyJvZ#$vCW%dp|5BLg+0p3b2^P&blDRv#Ls2|yyRUEJ}y4jG2 zOipTdUmkQjQmxpix2(gm!EAEDgL#~)6+&+0K=Ti|Y^mYOPB(tdJ%LkUFLp{KuVg|d zX?2(SHM8rMZp=rPGVGkCt84zo=Nj636w`!4Sce+E6%)B6JHGVdLCYwwqi(QZW|SoG z{%52hbCt(57ZtB}97S}PfVgFYlqBq!YxEJ?v&}$s!}a0~4<#9^PAA!z-~-v1i?z5d z?ayPqe#}}KZ?(OV)_pDm&}pV#bB5L=FsoRIvVO*Ygs{j|+0G`nA=wn5kT&64;w_V9 zK6RUe@uA`O)!7rrue7}wlEIg19zaHgXQkMz3mdxZnjYcUPDi3bc1dC>^7Wre1>UCHK`{kMK__QtU#}kXF8r@{iX za9gzF8}XdzdAY*^<+_p1USl1cmfORF*$L@u+kG&fvn{#tK+eV~xp5d*S&zV?Hv zGfAIAOmp9yd79Vl*7mM{eJtB6D2kyWrDQ@Xi|$goEmI}kxv}1>sWknUH@=eVrmjL9 zQ{*Qr*W_GDH#W;WcA?gs>_GXNzpq6HpYdi}f(!t{f z^*CwUbx5go3X+J<``R#eWj5HhIKW3Jedr;066Q>cK> z{$i0)yqDkAydw|UvT_9AIVjA##Pasdj*>gPd7W5g3&mo|kt$(`ghkosn|s&?{LONM z`Lw0~?LN7Oyu$q>8xEC=>e8^-@Lgp5naPEYjPskulC4?SA?aocsPDDj-9txb3Z{>s z31DaQ7tcs*Lzs5`V(60u)hkrK57r3SZl>?~{~T($H3DP@{^4W>%kbxH?{wp{1x5-@ zW^k{|{z{Vu+bd0z*m|9Z=XYEnsv4icw15s;0-ViJgYJx<@#ahu(8kYLJe<;sYrJql zO71uK`B*X*56S0V7ypzD!z&ZEF@>pbcVi=UgH(=H!!zxor=;D(taY#0(i*rQ-v^W! z_8Vaxc|FJVbNHJ#HJ!O=I<7wRtQi}bnA|gx35!n(7;AF6YGZdTTu(ey<{9c-C+j3d za}ODt$#mo}Rydqtz%zI@$}{Cx#F7}_xR3)nGJ!!Q@?ruV}<0pfEzWP>OYgM*5q?EOSG}yXcodI;@~|%l$Gl`P8(ew z44zkkk;djl8h+YOm~yktMnW!;Y;naeXFqR#1JNE00UYjUgpgbc)J|Q3Rm=77QVleY zkAt+Q=)0&49dl)>LFQGf03!qeW-`wQ4V0=%a}pg}=>a)T6i8j4#vY7+SQ)I6h3`-api|LC5nHCGy67G${HG~sW+d)6CR(J(LhbglTd;NkMfZm#)phE7 zYzz=vO~rbex)aP@oo-QZi>6zZ-ZH~^43?oz8L4Q5OUxbTWcE(dL=;hAV9HMW0hmYw zQIk6QCMnH|w@}j)y4ZrFQw8++<7EfzQ$m#ieG1B}l`cpHQ;KM>94eBuOvPM(6S&ZH zW2MXk7i!0ou}z_ZR!p@pwDN2!g*Rd9zC!IUqu8CziJ9W2OdG&UX)>2UZBC9zQJtA| zY`o^ZTWL;x_nYqxX;lmAWy#^`hA8DplK>V@?nbm%-(_$`=p<6MU=I_kN)xew-GZ_k zz_jafpBrZh5$|I}4Q3E2w^%6vXQn<=M31-hysW4bzxM>6B-@O-3OthR-|aJnlphfO zLUIchM|a2Fa;drsddv+D99Ex6HWVtN2R=E}7B-qJbL}B#=6t3w*75%p8u(?t_Z*1k z{hN8|VS9SE_-GVWp1p$_O7x?iM>G8~*TrKJ_YbAFt(3XiDGX=k+fHqKvu`w??%9Gi zRHIB##571V6!HvPiZcrrFt1BND4uNOANNM{X+q1&QD)>D%}QBZp7$#pG+Q|^u1v90 zXMJsuz4R_h9@s(4EXEKpO$yyDNuWXuw$CNl z=&q#G4Ru5ca^jZR|1YBV4$EXWalDhq3<#3{hE+RVU+k^$-f18@6eQH|{sEEdoa9)k zAEc}iRp#c$Lv9SVb#&HU)e>w5rSk1!9XTm&aI}e=nfn~QC|9||YW-M%8;)XD{dtP) zBfb{H3#m-qW&uDB5>LvXQ>c}bXgg(jv)Ac;@Ztf>nC@8f3f!@y*32_=RQ>mKjODzUH)0@x~sz(>csD@c=bgkxrLRySv0# zfR{d=L<``iKB^aC^XPjw1IK5-mcZ|lwUw{>-w|SsU!;5@8&=^R&P+%)jqB1sn|Aj7 zT2icSCbS2UJ2InWkx&hFkB={`-&;rap=YZ6(_gc&>fY$#wUd>mxu!c4y3aW=O@dp7YoEfd|hyWY{DdV+udq+^_&Srt-!g}xwpZItH^6}RR zZkBAja8w*7|-irupv&BKVm+0s-s9+snWFYz};QTb-w}%&|`H)5~{k!M~sOrxt zmZtP)dn2eTq7yH`7=2)gji5IjIsFgG$?Kiw7;A*G$1QI8pzLE1v8TQ_#@r!vK{GP1i6ld}?H;Lk+ahD1NYPVJj~Cs@yK36i!p6#3pme1OKGH ztUh9!otDU+7h4if#C&lwQ#7OR>S)h|&-h2dTl~!P+W5(8Q_@_tSKK^&@87I4Af(}I z(w6ofG1S=#j)wId*LIMQN`!r>7=rlWPLuK~o4KYA zGkEDpN1F7hIJ8ubkwyv`(@-6ese~W$yJi=T`0`w4PY3yg@dgvIrhi>F(P)3IYiazM zdnd|}cjR12MxBz5I5|B_3>gCR^lcE6#vm9bakKGD(TFn}gWI2uel;0mib5lmD2U91 zjLoV=k%}AVRN?&%wfDacsknMfOF+Mtsm2FuITd}-ld{5QZ|*oZexP%$gKp=>45n~j zqRudk){&FDFRwE@)ehS*Pwrn5JRt;T?qj&Wch*mKc_=m#%oMJyEp(3J`|BZ_)F*^p zM?8kuM-MZWL@meFuDtI|#I1_n2nS00%GH8NQj9^PRr^*R5#Tx_E_ix$Puxiz+3tAM-Y+(EGGI@ECE%)BrPt$eplhV2g zqL9^W*4LyT@wqWXUTL2yU}RslQ!P-wp%_EiWoiJr$bm3>$K!5wXT;^mSI}#*)vyvPlhtMS7vE)OLc{}Hp|oOKjnga+!_>bsn**73O#}F5HL58 z!IV}$=!^*>(*eT-Tds>fna8#MT>W*m{=U=C2brA+85RE)zgFF6+s;^fPF&drq5qaN zk~wUw7x4s(ZXzFtls&?x6r7QV|K8_}bVL?^TF)DBo(zOlqpeR%FjaJGRkp zySSqN@j+xC}Us!;nvJ!KP5thDU-sK)3zRsx1@V` z7dpHu-Df1SLT7U@Gs4}g(=C4oIyrI+L3Z%EGYs@ApDNG`8$3Q&%-!2H*G3yVAOR$5!K& z<~>HI(V}FrmwJB)V=W!Pu5+YTaw=9YzUQ~r2^f^JbLO1ID9AaJ*o${qH%s>%w#02-BU6M(10 z!dBZR>lB`NtGC>69zs_6AIoI@B0`oF++U9b?{Bt5>p*cv|BV64d z5iu0Gps%P$y~_2rtB^@m(a?pa=N@5YgJo9t*|suxPcvU*afecpOtTBM8KD!QM7)FM zo&v~qKJw$eF)g8}WPS2YXvQK!MAvVtDDy!WWPCkGQIu#2Gzn7>Bfi^Q50ovuNy$rUwV(nyLfDajgGgt@LRj0S^Nv<%XWit(t=peNXP?6Ec?_xdY)8Ip zP%rC{b~EE{!#9seIC{;aOe0LQNk#hc#^PzQUE+v~J<3OqPd-K*B;F2uIprjpL zy-BHVi!|bBS5Z-sg+9s^$;+Ax=?EibI)@6Y9*Ej31@*J)!J=S& z_hggXy#+(K3?dbu`j8}Ybi|X=`dKNZH#!xNBRJ-6K9ZxRns!IOLDK%g!9!*z&8}kB zg7T@KUR|%xNWhU1+Xw3vBsxRM_LM)H8grS&CpMItk_uNqPPL?nfxOufse3DE znF0u69eFxF&u-wJsbs96|F`cs0uM@G)2UP^amlOm9*eGyM$WS|d8l-->CB(+cLc3K z2u}-85dt$hY>;D5RgQj*BxJk^TA#yx)|YS55d|34HP9nG~k*(UPev0mTZ0Ao>*s z)kHNd7S~+6T9GOnxDw+h=nZzKb&-f@>|Q&iT>F+cb$PQB2Rqqy%Fy}l>B;ss4*k#n zDhfB#cEvlk)^VO~-<_vLY6Fgto|9V&?>y_VsN1LmJGz>qg=GjfKkWob(Ek?s!==y+ z&$z+(`ar>;%XYRI8N6fBt5@DTGt{3xB>T6l`^;%W`J@dR@?$~7Y9xJFJjB@Gc;9ku zEH5oo~u%lRVT7BLBI z(O7hszC`ByP~eM0VVp2<%Qz?rO#Y9lVY=^Fi-z*-Q)JqOp7*gY-F4ehH$6PX<{`WT zvj6Dc=bk4VhtByVcI!BC$fRs*)DA=pK#Tu-7L4yWi#G;BR(__gsPS;wPhP!Rp{@1s zjVw8kelx^Y&})rS1;Y#^WC=)j1d9ojr2HV2QAH;^GJm44M`*^q_O1a8n0x>pWr`n>!5DO5~m(TzbHoD-Zk=VH=}aG?{B z<6*Ow3hn&Qb_e7lsHyp`E{GDjK^M%NX7-6o;R$6TMX4jVVB38CsPwqJ1Ppwzznqim z1+Db82yQ~ymwM@x4r$C7KsU-)q0pBDIk7c5rR0yi^X8(MNYBGLN+dwUhSBQ&8W@jV z8&1 zM4)u`e9s|%^COP$Wzi*=jQcQ0-Y$}%rE$%p$)Hm2nouMYzU~`uz+4QmxYAws-=f-@ zvMOa?Es3U@c_<&XOkXMS0QON{YS98WMbreK>M3KpZ^PfokRaqCsw+A!D#UA%8`CW0 zirClfsdj0>e2qh|=`5+f18B4GHU2AB2LgWdLPP~S!ZTk#v_=%39?bXkQPaaQy^BT9 zjYVPhjy%zQ=l`A^VPbj_zmocs?YL%@A9(HKOk5udG{o=__!s_FewlmPFL!U2@r5S& zleknSDhKS@0FW4r$3xR2f-E${qQ8<+7ViF6GPwS`updQhJ0ph&YFBFQ5SL8+dk+Z@ zV95XG|1^GVIQn>}-4VE4l&$v-AnYX(HS+>K_KQ7PCq|JxL z?9vb*~DlYV2br7^ltv>jXa4Ok&Iu2m0GK`5bjHQTMsojtVDrm=OaFS z;+*sgKYmTw(*DGEwA?z_@Y;YA-0m*sd#pl@2_) z9@3i3*iN&UcZT;cZWgBEz2qxVQ-l$2Yq=_ankgW)-DGY@rr}=;a}+L`M2b_vPLnL= zm>@Mpg`(^>i&C2TX%-5cDlCq?9f>H04;VIc>zTx=ql_TkziXGct=Va2AXY(52T`oY zLhU>p3hS2cZ~s18ulfekG4Kf2i6k6yQDM+m(=@GdwaJv@D$m>4(Pm!zmaCjeoX?c7 z(`M6NNQlc%P{I)%k1BTBt&9*yHN(wspCN2Cz-8v!qkTH<5=93{fCih8Y=w$90%uND zWrMDeBvj0CGZuLkl|7~xbz~kw1tlMf^~-*|#B0)gk+;Ags^%yoifPzGX_g%djA^p- zXlD~YhcwHy#KAjYmLK2b1BKkf9SD}>2V@qSM+Tlg^8i7k2%q;?_+G>#s1@pO(-(B` znh8`{bdMOu>20`%g{Pj1`>~Jv-aK0F7K1wbS6>zd_EqkyeCc+6@>x@!``|ld=*C;R ze>vbWTyiby_a0Xx*8fm=pf1WHUr#MRqwf2rSmt^Tt^t>093_ah1T1Is%_UZFi2==(H}+A-OYbW zRK4&*T?AIdGXy?eFB7ufg>TvH!J+GJiNcIHhxesB7zkK#Ig2WL*!=~*JW{v;*GNtn z>3)XY+IHorMi;_yY{^Vz$~%4>*UMKLWriVS=>!KmhsKJev9YnEQTLB*{g#(ssvh zfD(|~t^rhH9M^V50~dgs_jbnxw0Lxyk=Q<*?;EdFVEqqvf1hrnKID*n6Md|P^O$lI z_wtP4;l%+}|NWP#@M}VlOvzzFV`6M<+@Dzdpcsu^WM26&W@3j= z7m``PWq{w=6Xf{BFgi-QF8mlJ)aBn&%zvo>#ow{tepR01Fabz!yKfpZJw3fHNa_RF zKb=RxKV!0CKjA9@lyx9$i|#>%C<8RNgjE7k{B_oQvUR0z+~t|5*?Tn!-#%3Z2<>KR z$htFf9k&qaG_AYviMXSfpk=FM?hawy7I*D<;R}h7CDl1?&I@x7sx*M#PPF$#ZzT<; zgn{)N_&@3oxJ%uuw)%EyxwqoR-~U~+UBjW$NZ7rwa04<^*S53LxsopdZP>>90UNjB zCsn3qZa16vZ$#M)cx5B+h zU3j7<%*1$a&7Q>NjMqyrDk7mrA@CT`a%TE|Ip#Xu7Dd45n#IV8&8l$q@j85!l8iymqMc%39XA4pLb72&V0EkoSyi-6}oK# z)rXVOX8K{|BElq1mj$~JKC`vvEen?Kiyj+0ZJll8*qV6&BUl+Yq_lrF_v}e9 z6d5AaEg&BGaC7R*5*?MGPR{;xeF)r4vmow%P;bykK3tk~T)k5i zcm)LUHDc_1Fy{A6pI#T&PSc06q0je1r;`upw?3d%Nl&9RDd{K~eGZD;;G+#bfGLtq z(IMS(1FcJUJc=gd3!I~_$z)tDLHAk*J$7v|BD|byIK1vx^L#+R7U|kS-&4lEO7ufK zC2TJ*EY8&YDj8IFY9uH}(I5<9clY`JYGVs|r2nGv$KO|j@r*p1@cv56%J%O$4$Fw9 zLUsF8x~1rF#f{v7d3D^~7rDX!W8pr0TsQr**)~g&{qd2y8f|gPy_X75oDfmJ7V&Ds z)kIfV$gZTZXHrF|DIhv%aCv`McVnUfiASHVV7#*3M4P-Z6J2p*I4|^-#==B<@_k^- zI-wyCnqlB$KicdTI7;S%LmS+aa&sAHM+mJfAtJpwwO0=|Vy?!ZOnbWZ;;#=E2T5-O z`GxxWdM1UF)1PUl$dlxm1cBR-m=Eq0HGFdZ24Ew}cIj|@16ul;!Z&^bZ55L%cDArk zH2?mR+opLn{8|V7`n+Sm*7n{m3q;%&VutZ?Fw`($=Whd?j`E=O{io=~Y~Ni( z5O58v+-bfwz~YfW6ga+yA8CT*));V}*rl^kAHSV)J&}9&h1FC9lE%aX=wLKL(+Xj3 z7)b)$cx>*=;r)Blvbulv?;1us{MY6`|6Rk}yoAUI=OC#Gth-zXb^80@xXGy`ETH|n zo)0Dk-MSUx^SIi(PuToDF#LN)KZp;AI(Y%D6bh*aXL zgPfh6hL0)&Ro}Rqq?^Eh{b>QY-jO<~h7kK>e}d4V6*Zc)=eE?~t^C4F@EwqG4v}+O zhxy{jkcUGj(Kz75K=b;(so8dOt-A`}Un~zx`!lFI9iYQr+tJZ6a*lmhr^M3t0{0cT6>^ir3rK4SM4L*S3HRtK2A1u5Ffd{G+o~EjKSOEY023 zRj&|g+`XIP4}VRXY{s zk2IdA(efR3roft`d_2vY&88}PhfBuqVa@3+jcd^K@|r#?dQrw03SxK;y%+a3fP3ZH z6g<1Te(xAKgRO^`gY_!)&Ew?i`7f;`_oO%v50$7|AHDZy@7r8L1>)fI?13K^!$72A z%x9j4>VmBih;EzLp4P=BUszAqFnzaNDsd;GHYp4-TJxGpXX)^D&2QcZ)}XDr7nfA1 z^=>Y^J`hec);ky2YDuPHZCD;nO{1hSjJ6z(j9EfIXb# zcaY!p+_^fsDOg5;7T5TNhLwkm0v(w)wdXK#kR1sLiNUODo5}h((vM+1?1bPqxf8AN z<1R@%rkj9O0HZx{(DQmvG~)_byoq4#rrC83w4~w z=SJIAT39N=0^|~mdqWc=?|}gV2#(YSM4|>d!^Ptvh}JU~AV}qPD_6v94>jS+*8cG! zbp4K~h6YcrkGiSHON$&zO`~P`I`%$~P`$;)%0fIC+`dbVpx!V_vlj74=vuaZ5a-deE%>P5TE^4**POlA{j&Q$ODkBBg$x zMsUNoMWo&t&W9HH|Eqyq7wy+?28V}Js=jx)fMMu<+8-n-;S=F;+DE|#FdSQ%GT|Ed zBjeAU_%HPK&NO#)bWFinl+bF|8J$QIsWb~{E>7KL5~ousw*3faUaDb zORFpJsn)g{h`IrV(0Rwx9={WZy<&dLUz)|cP38m9Pj$~ zuC%12_MQKSy!Q_0^8NqDwU@L|BrSv#k-efN$u4`8>^&-5WtBn;$x5=4m7SSFHX(^f z_DXj4{W$gcevkY2$M5+4?&G-czivmz@qSmYUe|S<*Lgn2d9rl*Yi85zT;pYvEFtQZhqo)E8O%!TPcIz6unm$ENV$xd_B zA9Xzs`o=vb&!iO#KGo`G_|=}G=-o9c@nBc8(NS;g&;K-yFZnzioe0o*fBju^A`^mx z-?s`iFkmVmK^v{xkVWVx4aVuEmDjn6EKJsHsEM-{8S{=PdM2mfpuaG}{3+yIma;m_ zm~YyQT`!(+!pqC+jZo`O+bDU~?A7)drqJqqbS>A6{jyryD_P}e%?qbn4%VhVl6-eD z)64c_#qJ;Cp97T@JB%W{60ZV%2(0@U$`&A95p9!9O<8_YO@3oQ&u0aY&7>?|4?jc> z0{ReG7Y|RIHqHss$h@QhAe+uFvVdCL=@_l^`qf+!_S;BTFk2ovkMYaXS%po=90hu;rK6vaG1R2Rwbi*@(@iTXD#{V1>;*Zj8N3nZ6KPvcjg$xR z75d{X0wxx#j0HRi`mv$^l?%Xg0amf=Zg^9F99#L!seAM%Tf&l}74?tD$neQ#{7(6y zAEY&jF0{hom1g!ibkauK329CL%?=M|%i{eK$|# zC3%pHM0t~JCj$rQX|FGRRMv}A0EG-Jq&VE_oBOlrXuhxhlh8>2<*BbyJhM#f zFY@BafVKeyKT{03{jVO_LCR_1@c!9*B$hM9xKe4Ror6O|4N7vN(L{QPObQWIO2nYY zcp{m>qs`Tyf9mok9DH9?hHS@;Dt^7vFmm;1vhxel&tO-+wfB(~q7(!=L z%ecT}skq3<$l|cb?+`UG?LFt>B6{&6`efVP0s~oiQN2&FeVb zO=&JS-d;~V?TISZBlACfYtvNFtv|*0Jknk&=IkE#7P_Nm1@V#noYMNPntro-B+B8g z=y;}%P7I~&`?^+-yLiy-{>iEC*E1=m-dxYK?uA?91S93+%v_w(24mO*qyrLbFF(2> zd+2eUFMoR+l+*HtUsRAo158B(dAfrV{pgKkr?B?<_L)I4FN*<_&eDEog<^FsH)}g3 z##+3Hah;6XKY%#4GBnRZ5b1}Bg++MN9qVNcSi`IY<{czgP}{e9v)l4fQ9ISEK+@db)I)q(4}9`@hV`wnFcfyh<=t_3iMx z=N~_Qq@i;g@-@x6G7`;&?lMav_{N|4vDnHzo7gth_k%3W69eSHq^9A`2jepsZ#VKP;%j7c&8li4W zvn0(>oBl?|uOa=7f0YcdG^Gw+_R1#}z^)E0qS?-Ya!Ag2y97m#+ImZk4*IJ8@*K zfBOfCiU-IIAo(FY*+}K!#jI7j8fa5NS?Bqr3$O;zMGznbm^)aSOaZ95m+$QEQDUP@ zZhR8}2Np8;(rtOZLn{MVNwgxH=f^aSH`n1I^>r)@Mp8afkrBGg=D)`XUJ5AH(a8x* z0*RdTR*PI?_*k}FstJtn?24at78}W%sG%7cCH4jJoW}Gwg1C@@bBB@bsai14kr3_Mpf?JGRNabEc0U)#M zzi6L1aM}y}HjqQd7_l%Zr&eO6|E0eDvwS!52@nZI!~oHo$X7_qZ1|KZ)`WuvEgVf_ zP4u6sz$;9A)z!`^WV9iJBBtl=_6{EO@vCYr@X z_A^yH3Z1w2u{w#Ed7~J5GBn(eo`1W#*qhPONIhBQXt?X#z&od- zj7T@r80Sd~_IaRy@8;#vL%WHLM>kv~$?yYoZ@mGB2VI(`Z!ezCiQFXj2xUlglzG{l zrmvukfBj^L4Ru!>ysi4)#CSCBPyD4qp_>ZkO^Bt?XWwUwEwWD*0(IbhJ)L{9I#Q6! zbkd@_(EASA6ovA}A(59X`Mtc!Y#N0~?~xx|@7r%xv?`7MR>2RO%47ONqxNOO_+v6? z6f=z#q7}<+hebyn{0WHh`#R3qH>!w%hIik%6CWl%LvM*+NKE{SS>PX)Q?*zwMtkOO zSf=VpYdyj#HVzk!kkDii=K)m=_Os+#9b49db`H z9E*rU&97^}*=n9fpVcqw3^hfQ3D5_}FkOj8o zt<{awUX`%0^EO2wA2P2ed+Sf#JO#4Jur1hmh%2}sDF@H*WV!%QJku!TP{HRm-YrN@ z1wT+qHogU>`(4Qi(h7V?TJF(c$%i>u6Y8#xLkFaR~E_KU0zo9(zv4 z{vj|B=&=#3P312b^Cj3X`@1Y`p&~^+`WU2k-`R2+dLNAI|K*5Ke{~GWIwP zZ$)W0L4LfwZk_G`eYNi(q+l9xvWSfhpkyLRPr2?pGXbN$nTWx^AIw?Pd4PiAl<)tG z)WM8*RJnHS73W@_h_S|BW7%TUss7bQc}}DGDwddD7f^m(qznU{XV?|@z?SFv71Fhakz51diKnJh-YJRHj2ea>L z@6YO~(`v*Z;*x1_DmQYD_y7*LqE?FEF=l+N$sU;4|EFp~x0<7!6T;TX5lgpLB}+Kl zDKVFc0R$XL7KMv9<;d8|yq)(yrI#TpT#6ykx}IKp&s#Dgco9Lf605SS9C&;bGCFls zCP+(A2V=V)v>Zx<+9*e+3@WHlRhfJ;Gzt8D?CY1%^A`pqieCT1qW>7CXBGkz7k&B2rIS#g%U4eF? zO-pst5#Y9c5JM?b636&z8p=SO%=pl4F8!yGz)y^BbNl;|&0m2H@|C`_kZq82L9)_B z6ct9sNDU+(jq;6kb&vYKiOmm3ks0nj=c0H9Y4BLBv}0$}SyInczN)OKr9Jr?1yJx3 zU_XNL?Zr>9ex}YlDoLTrAT4Vw&A)!_a8YmY=lUzNBjY@ecP)9`l=&Tpa;aIvUPj