/** * @file ABC_EQF.h * @brief Header file for the Attitude-Bias-Calibration Equivariant Filter * * This file contains declarations for the Equivariant Filter (EqF) for attitude * estimation with both gyroscope bias and sensor extrinsic calibration, based * on the paper: "Overcoming Bias: Equivariant Filter Design for Biased Attitude * Estimation with Online Calibration" by Fornasier et al. Authors: Darshan * Rajasekaran & Jennifer Oum */ #ifndef ABC_EQF_H #define ABC_EQF_H #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include // For std::accumulate #include #include #include "ABC.h" // All implementations are wrapped in this namespace to avoid conflicts namespace gtsam { namespace abc_eqf_lib { using namespace std; using namespace gtsam; //======================================================================== // Helper Functions for EqF //======================================================================== /// Calculate numerical differential Matrix numericalDifferential(std::function f, const Vector& x); /** * Compute the lift of the system (Theorem 3.8, Equation 7) * @param xi State * @param u Input * @return Lift vector */ template Vector lift(const State& xi, const Input& u); /** * Action of the symmetry group on the state space (Equation 4) * @param X Group element * @param xi State * @return New state after group action */ template State operator*(const G& X, const State& xi); /** * Action of the symmetry group on the input space (Equation 5) * @param X Group element * @param u Input * @return New input after group action */ template Input velocityAction(const G& X, const Input& u); /** * Action of the symmetry group on the output space (Equation 6) * @param X Group element * @param y Direction measurement * @param idx Calibration index * @return New direction after group action */ template Vector3 outputAction(const G& X, const Unit3& y, int idx); /** * Differential of the phi action at E = Id in local coordinates * @param xi State * @return Differential matrix */ template Matrix stateActionDiff(const State& xi); //======================================================================== // Equivariant Filter (EqF) //======================================================================== /// Equivariant Filter (EqF) implementation template class EqF { private: int dof; // Degrees of freedom G X_hat; // Filter state Matrix Sigma; // Error covariance State xi_0; // Origin state Matrix Dphi0; // Differential of phi at origin Matrix InnovationLift; // Innovation lift matrix /** * Return the state matrix A0t (Equation 14a) * @param u Input * @return State matrix A0t */ Matrix stateMatrixA(const Input& u) const; /** * Return the state transition matrix Phi (Equation 17) * @param u Input * @param dt Time step * @return State transition matrix Phi */ Matrix stateTransitionMatrix(const Input& u, double dt) const; /** * Return the Input matrix Bt * @return Input matrix Bt */ Matrix inputMatrixBt() const; /** * Return the measurement matrix C0 (Equation 14b) * @param d Known direction * @param idx Calibration index * @return Measurement matrix C0 */ Matrix measurementMatrixC(const Unit3& d, int idx) const; /** * Return the measurement output matrix Dt * @param idx Calibration index * @return Measurement output matrix Dt */ Matrix outputMatrixDt(int idx) const; public: /** * Initialize EqF * @param Sigma Initial covariance * @param m Number of sensors */ EqF(const Matrix& Sigma, int m); /** * Return estimated state * @return Current state estimate */ State stateEstimate() const; /** * Propagate the filter state * @param u Angular velocity measurement * @param dt Time step */ void propagation(const Input& u, double dt); /** * Update the filter state with a measurement * @param y Direction measurement */ void update(const Measurement& y); }; //======================================================================== // Helper Functions Implementation //======================================================================== /** * Maps system dynamics to the symmetry group * @param xi State * @param u Input * @return Lifted input in Lie Algebra * Uses Vector zero & Rot3 inverse, matrix functions */ template Vector lift(const State& xi, const Input& u) { Vector L = Vector::Zero(6 + 3 * N); // First 3 elements L.head<3>() = u.w - xi.b; // Next 3 elements L.segment<3>(3) = -u.W() * xi.b; // Remaining elements for (size_t i = 0; i < N; i++) { L.segment<3>(6 + 3 * i) = xi.S[i].inverse().matrix() * L.head<3>(); } return L; } /** * Implements group actions on the states * @param X A symmetry group element G consisting of the attitude, bias and the * calibration components X.a -> Rotation matrix containing the attitude X.b -> * A skew-symmetric matrix representing bias X.B -> A vector of Rotation * matrices for the calibration components * @param xi State object * xi.R -> Attitude (Rot3) * xi.b -> Gyroscope Bias(Vector 3) * xi.S -> Vector of calibration matrices(Rot3) * @return Transformed state * Uses the Rot3 inverse and Vee functions */ template State operator*(const G& X, const State& xi) { std::array new_S; for (size_t i = 0; i < N; i++) { new_S[i] = X.A.inverse() * xi.S[i] * X.B[i]; } return State(xi.R * X.A, X.A.inverse().matrix() * (xi.b - Rot3::Vee(X.a)), new_S); } /** * Transforms the angular velocity measurements b/w frames * @param X A symmetry group element X with the components * @param u Inputs * @return Transformed inputs * Uses Rot3 Inverse, matrix and Vee functions and is critical for maintaining * the input equivariance */ template Input velocityAction(const G& X, const Input& u) { return Input{X.A.inverse().matrix() * (u.w - Rot3::Vee(X.a)), u.Sigma}; } /** * Transforms the Direction measurements based on the calibration type ( Eqn 6) * @param X Group element X * @param y Direction measurement y * @param idx Calibration index * @return Transformed direction * Uses Rot3 inverse, matric and Unit3 unitvector functions */ template Vector3 outputAction(const G& X, const Unit3& y, int idx) { if (idx == -1) { return X.A.inverse().matrix() * y.unitVector(); } else { if (idx >= static_cast(N)) { throw std::out_of_range("Calibration index out of range"); } return X.B[idx].inverse().matrix() * y.unitVector(); } } /** * @brief Calculates the Jacobian matrix using central difference approximation * @param f Vector function f * @param x The point at which Jacobian is evaluated * @return Matrix containing numerical partial derivatives of f at x * Uses Vector's size() and Zero(), Matrix's Zero() and col() methods */ Matrix numericalDifferential(std::function f, const Vector& x) { double h = 1e-6; Vector fx = f(x); int n = fx.size(); int m = x.size(); Matrix Df = Matrix::Zero(n, m); for (int j = 0; j < m; j++) { Vector ej = Vector::Zero(m); ej(j) = 1.0; Vector fplus = f(x + h * ej); Vector fminus = f(x - h * ej); Df.col(j) = (fplus - fminus) / (2 * h); } return Df; } /** * Computes the differential of a state action at the identity of the symmetry * group * @param xi State object Xi representing the point at which to evaluate the * differential * @return A matrix representing the jacobian of the state action * Uses numericalDifferential, and Rot3 expmap, logmap */ template Matrix stateActionDiff(const State& xi) { std::function coordsAction = [&xi](const Vector& U) { G groupElement = G::exp(U); State transformed = groupElement * xi; return xi.localCoordinates(transformed); }; Vector zeros = Vector::Zero(6 + 3 * N); Matrix differential = numericalDifferential(coordsAction, zeros); return differential; } //======================================================================== // Equivariant Filter (EqF) Implementation //======================================================================== /** * Initializes the EqF with state dimension validation and computes lifted * innovation mapping * @param Sigma Initial covariance * @param n Number of calibration states * @param m Number of sensors * Uses SelfAdjointSolver, completeOrthoganalDecomposition().pseudoInverse() */ template EqF::EqF(const Matrix& Sigma, int m) : dof(6 + 3 * N), X_hat(G::identity(N)), Sigma(Sigma), xi_0(State::identity()) { if (Sigma.rows() != dof || Sigma.cols() != dof) { throw std::invalid_argument( "Initial covariance dimensions must match the degrees of freedom"); } // Check positive semi-definite Eigen::SelfAdjointEigenSolver eigensolver(Sigma); if (eigensolver.eigenvalues().minCoeff() < -1e-10) { throw std::invalid_argument( "Covariance matrix must be semi-positive definite"); } if (N < 0) { throw std::invalid_argument( "Number of calibration states must be non-negative"); } if (m <= 1) { throw std::invalid_argument( "Number of direction sensors must be at least 2"); } // Compute differential of phi Dphi0 = stateActionDiff(xi_0); InnovationLift = Dphi0.completeOrthogonalDecomposition().pseudoInverse(); } /** * Computes the internal group state to a physical state estimate * @return Current state estimate */ template State EqF::stateEstimate() const { return X_hat * xi_0; } /** * Implements the prediction step of the EqF using system dynamics and * covariance propagation and advances the filter state by symmtery-preserving * dynamics.Uses a Lie group integrator scheme for discrete time propagation * @param u Angular velocity measurements * @param dt time steps * Updated internal state and covariance */ template void EqF::propagation(const Input& u, double dt) { State state_est = stateEstimate(); Vector L = lift(state_est, u); Matrix Phi_DT = stateTransitionMatrix(u, dt); Matrix Bt = inputMatrixBt(); Matrix tempSigma = blockDiag(u.Sigma, repBlock(1e-9 * I_3x3, N)); Matrix M_DT = (Bt * tempSigma * Bt.transpose()) * dt; X_hat = X_hat * G::exp(L * dt); Sigma = Phi_DT * Sigma * Phi_DT.transpose() + M_DT; } /** * Implements the correction step of the filter using discrete measurements * Computes the measurement residual, Kalman gain and the updates both the state * and covariance * * @param y Measurements */ template void EqF::update(const Measurement& y) { if (y.cal_idx > static_cast(N)) { throw std::invalid_argument("Calibration index out of range"); } // Get vector representations for checking Vector3 y_vec = y.y.unitVector(); Vector3 d_vec = y.d.unitVector(); // Skip update if any NaN values are present if (std::isnan(y_vec[0]) || std::isnan(y_vec[1]) || std::isnan(y_vec[2]) || std::isnan(d_vec[0]) || std::isnan(d_vec[1]) || std::isnan(d_vec[2])) { return; // Skip this measurement } Matrix Ct = measurementMatrixC(y.d, y.cal_idx); Vector3 action_result = outputAction(X_hat.inv(), y.y, y.cal_idx); Vector3 delta_vec = Rot3::Hat(y.d.unitVector()) * action_result; Matrix Dt = outputMatrixDt(y.cal_idx); Matrix S = Ct * Sigma * Ct.transpose() + Dt * y.Sigma * Dt.transpose(); Matrix K = Sigma * Ct.transpose() * S.inverse(); Vector Delta = InnovationLift * K * delta_vec; X_hat = G::exp(Delta) * X_hat; Sigma = (Matrix::Identity(dof, dof) - K * Ct) * Sigma; } /** * Computes linearized continuous time state matrix * @param u Angular velocity * @return Linearized state matrix * Uses Matrix zero and Identity functions */ template Matrix EqF::stateMatrixA(const Input& u) const { Matrix3 W0 = velocityAction(X_hat.inv(), u).W(); Matrix A1 = Matrix::Zero(6, 6); A1.block<3, 3>(0, 3) = -I_3x3; A1.block<3, 3>(3, 3) = W0; Matrix A2 = repBlock(W0, N); return blockDiag(A1, A2); } /** * Computes the discrete time state transition matrix * @param u Angular velocity * @param dt time step * @return State transition matrix in discrete time */ template Matrix EqF::stateTransitionMatrix(const Input& u, double dt) const { Matrix3 W0 = velocityAction(X_hat.inv(), u).W(); Matrix Phi1 = Matrix::Zero(6, 6); Matrix3 Phi12 = -dt * (I_3x3 + (dt / 2) * W0 + ((dt * dt) / 6) * W0 * W0); Matrix3 Phi22 = I_3x3 + dt * W0 + ((dt * dt) / 2) * W0 * W0; Phi1.block<3, 3>(0, 0) = I_3x3; Phi1.block<3, 3>(0, 3) = Phi12; Phi1.block<3, 3>(3, 3) = Phi22; Matrix Phi2 = repBlock(Phi22, N); return blockDiag(Phi1, Phi2); } /** * Computes the input uncertainty propagation matrix * @return * Uses the blockdiag matrix */ template Matrix EqF::inputMatrixBt() const { Matrix B1 = blockDiag(X_hat.A.matrix(), X_hat.A.matrix()); Matrix B2(3 * N, 3 * N); for (size_t i = 0; i < N; ++i) { B2.block<3, 3>(3 * i, 3 * i) = X_hat.B[i].matrix(); } return blockDiag(B1, B2); } /** * Computes the linearized measurement matrix. The structure depends on whether * the sensor has a calibration state * @param d reference direction * @param idx Calibration index * @return Measurement matrix * Uses the matrix zero, Rot3 hat and the Unitvector functions */ template Matrix EqF::measurementMatrixC(const Unit3& d, int idx) const { Matrix Cc = Matrix::Zero(3, 3 * N); // If the measurement is related to a sensor that has a calibration state if (idx >= 0) { // Set the correct 3x3 block in Cc Cc.block<3, 3>(0, 3 * idx) = Rot3::Hat(d.unitVector()); } Matrix3 wedge_d = Rot3::Hat(d.unitVector()); // Create the combined matrix Matrix temp(3, 6 + 3 * N); temp.block<3, 3>(0, 0) = wedge_d; temp.block<3, 3>(0, 3) = Matrix3::Zero(); temp.block(0, 6, 3, 3 * N) = Cc; return wedge_d * temp; } /** * Computes the measurement uncertainty propagation matrix * @param idx Calibration index * @return Returns B[idx] for calibrated sensors, A for uncalibrated */ template Matrix EqF::outputMatrixDt(int idx) const { // If the measurement is related to a sensor that has a calibration state if (idx >= 0) { if (idx >= static_cast(N)) { throw std::out_of_range("Calibration index out of range"); } return X_hat.B[idx].matrix(); } else { return X_hat.A.matrix(); } } } // namespace abc_eqf_lib template struct traits> : internal::LieGroupTraits> {}; } // namespace gtsam #endif // ABC_EQF_H