Merge pull request #2115 from borglab/feature/templated_methods

Invariant EKF
release/4.3a0
Frank Dellaert 2025-05-07 12:11:48 -04:00 committed by GitHub
commit e41c2eef12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1102 additions and 4 deletions

View File

@ -0,0 +1,77 @@
/* ----------------------------------------------------------------------------
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
* Atlanta, Georgia 30332-0415
* All Rights Reserved
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
* See LICENSE for the license information
* -------------------------------------------------------------------------- */
/**
* @file IEKF_Rot3Example.cpp
* @brief LeftInvariant EKF on SO(3) with statedependent pitch/roll control
* and a single magnetometer update.
* @date April 25, 2025
* @authors Scott Baker, Matt Kielo, Frank Dellaert
*/
#include <gtsam/base/Matrix.h>
#include <gtsam/base/OptionalJacobian.h>
#include <gtsam/geometry/Rot3.h>
#include <gtsam/navigation/LieGroupEKF.h>
#include <iostream>
using namespace std;
using namespace gtsam;
// --- 1) Closedloop dynamics f(X): xi = k·[φx,φy,0], H = ∂xi/∂φ·Dφ ---
static constexpr double k = 0.5;
Vector3 dynamicsSO3(const Rot3& X, OptionalJacobian<3, 3> H = {}) {
// φ = Logmap(R), Dφ = ∂φ/∂δR
Matrix3 D_phi;
Vector3 phi = Rot3::Logmap(X, D_phi);
// zero out yaw
phi[2] = 0.0;
D_phi.row(2).setZero();
if (H) *H = -k * D_phi; // ∂(kφ)/∂δR
return -k * phi; // xi ∈ 𝔰𝔬(3)
}
// --- 2) Magnetometer model: z = R⁻¹ m, H = [z]_× ---
static const Vector3 m_world(0, 0, -1);
Vector3 h_mag(const Rot3& X, OptionalJacobian<3, 3> H = {}) {
Vector3 z = X.inverse().rotate(m_world);
if (H) *H = -skewSymmetric(z);
return z;
}
int main() {
// Initial estimate (identity) and covariance
const Rot3 R0 = Rot3::RzRyRx(0.1, -0.2, 0.3);
const Matrix3 P0 = Matrix3::Identity() * 0.1;
LieGroupEKF<Rot3> ekf(R0, P0);
// Timestep, process noise, measurement noise
double dt = 0.1;
Matrix3 Q = Matrix3::Identity() * 0.01;
Matrix3 Rm = Matrix3::Identity() * 0.05;
cout << "=== Init ===\nR:\n"
<< ekf.state().matrix() << "\nP:\n"
<< ekf.covariance() << "\n\n";
// Predict using statedependent f
ekf.predict(dynamicsSO3, dt, Q);
cout << "--- After predict ---\nR:\n" << ekf.state().matrix() << "\n\n";
// Magnetometer measurement = bodyframe reading of m_world
Vector3 z = h_mag(R0);
ekf.update(h_mag, z, Rm);
cout << "--- After update ---\nR:\n" << ekf.state().matrix() << "\n";
return 0;
}

View File

@ -0,0 +1,95 @@
/* ----------------------------------------------------------------------------
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
* Atlanta, Georgia 30332-0415
* All Rights Reserved
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
* See LICENSE for the license information
* -------------------------------------------------------------------------- */
/**
* @file IEKF_NavstateExample.cpp
* @brief InvariantEKF on NavState (SE_2(3)) with IMU (predict) and GPS (update)
* @date April 25, 2025
* @authors Scott Baker, Matt Kielo, Frank Dellaert
*/
#include <gtsam/base/Matrix.h>
#include <gtsam/base/OptionalJacobian.h>
#include <gtsam/navigation/InvariantEKF.h>
#include <gtsam/navigation/NavState.h>
#include <iostream>
using namespace std;
using namespace gtsam;
/**
* @brief Left-invariant dynamics for NavState.
* @param imu 6×1 vector [a; ω]: linear acceleration and angular velocity.
* @return 9×1 tangent: [ω; 0; a].
*/
Vector9 dynamics(const Vector6& imu) {
auto a = imu.head<3>();
auto w = imu.tail<3>();
Vector9 xi;
xi << w, Vector3::Zero(), a;
return xi;
}
/**
* @brief GPS measurement model: returns position and its Jacobian.
* @param X Current state.
* @param H Optional 3×9 Jacobian w.r.t. X.
* @return 3×1 position vector.
*/
Vector3 h_gps(const NavState& X, OptionalJacobian<3, 9> H = {}) {
return X.position(H);
}
int main() {
// Initial state & covariances
NavState X0; // R=I, v=0, t=0
Matrix9 P0 = Matrix9::Identity() * 0.1;
InvariantEKF<NavState> ekf(X0, P0);
// Noise & timestep
double dt = 1.0;
Matrix9 Q = Matrix9::Identity() * 0.01;
Matrix3 R = Matrix3::Identity() * 0.5;
// Two IMU samples [a; ω]
Vector6 imu1;
imu1 << 0.1, 0, 0, 0, 0.2, 0;
Vector6 imu2;
imu2 << 0, 0.3, 0, 0.4, 0, 0;
// Two GPS fixes
Vector3 z1;
z1 << 0.3, 0, 0;
Vector3 z2;
z2 << 0.6, 0, 0;
cout << "=== Init ===\nX: " << ekf.state() << "\nP: " << ekf.covariance()
<< "\n\n";
// --- first predict/update ---
ekf.predict(dynamics(imu1), dt, Q);
cout << "--- After predict 1 ---\nX: " << ekf.state()
<< "\nP: " << ekf.covariance() << "\n\n";
ekf.update(h_gps, z1, R);
cout << "--- After update 1 ---\nX: " << ekf.state()
<< "\nP: " << ekf.covariance() << "\n\n";
// --- second predict/update ---
ekf.predict(dynamics(imu2), dt, Q);
cout << "--- After predict 2 ---\nX: " << ekf.state()
<< "\nP: " << ekf.covariance() << "\n\n";
ekf.update(h_gps, z2, R);
cout << "--- After update 2 ---\nX: " << ekf.state()
<< "\nP: " << ekf.covariance() << "\n";
return 0;
}

View File

@ -0,0 +1,119 @@
/* ----------------------------------------------------------------------------
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
* Atlanta, Georgia 30332-0415
* All Rights Reserved
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
* See LICENSE for the license information
* -------------------------------------------------------------------------- */
/**
* @file IEKF_SE2Example.cpp
* @brief A left invariant Extended Kalman Filter example using a Lie group
* odometry as the prediction stage on SE(2) and
*
* This example uses the templated InvariantEKF class to estimate the state of
* an object using odometry / GPS measurements The prediction stage of the
* InvariantEKF uses a Lie Group element to propagate the stage in a discrete
* InvariantEKF. For most cases, U = exp(u^ * dt) if u is a control vector that
* is constant over the interval dt. However, if u is not constant over dt,
* other approaches are needed to find the value of U. This approach simply
* takes a Lie group element U, which can be found in various different ways.
*
* This data was compared to a left invariant EKF on SE(2) using identical
* measurements and noise from the source of the InEKF plugin
* https://inekf.readthedocs.io/en/latest/ Based on the paper "An Introduction
* to the Invariant Extended Kalman Filter" by Easton R. Potokar, Randal W.
* Beard, and Joshua G. Mangelson
*
* @date Apr 25, 2025
* @authors Scott Baker, Matt Kielo, Frank Dellaert
*/
#include <gtsam/geometry/Pose2.h>
#include <gtsam/navigation/InvariantEKF.h>
#include <iostream>
using namespace std;
using namespace gtsam;
// Create a 2D GPS measurement function that returns the predicted measurement h
// and Jacobian H. The predicted measurement h is the translation of the state
// X.
Vector2 h_gps(const Pose2& X, OptionalJacobian<2, 3> H = {}) {
return X.translation(H);
}
// Define a InvariantEKF class that uses the Pose2 Lie group as the state and
// the Vector2 measurement type.
int main() {
// // Initialize the filter's state, covariance, and time interval values.
Pose2 X0(0.0, 0.0, 0.0);
Matrix3 P0 = Matrix3::Identity() * 0.1;
// Create the filter with the initial state, covariance, and measurement
// function.
InvariantEKF<Pose2> ekf(X0, P0);
// Define the process covariance and measurement covariance matrices Q and R.
Matrix3 Q = (Vector3(0.05, 0.05, 0.001)).asDiagonal();
Matrix2 R = I_2x2 * 0.01;
// Define odometry movements.
// U1: Move 1 unit in X, 1 unit in Y, 0.5 radians in theta.
// U2: Move 1 unit in X, 1 unit in Y, 0 radians in theta.
Pose2 U1(1.0, 1.0, 0.5), U2(1.0, 1.0, 0.0);
// Create GPS measurements.
// z1: Measure robot at (1, 0)
// z2: Measure robot at (1, 1)
Vector2 z1, z2;
z1 << 1.0, 0.0;
z2 << 1.0, 1.0;
// Define a transformation matrix to convert the covariance into (theta, x, y)
// form. The paper and data mentioned above uses a (theta, x, y) notation,
// whereas GTSAM uses (x, y, theta). The transformation matrix is used to
// directly compare results of the covariance matrix.
Matrix3 TransformP;
TransformP << 0, 0, 1, 1, 0, 0, 0, 1, 0;
// Propagating/updating the filter
// Initial state and covariance
cout << "\nInitialization:\n";
cout << "X0: " << ekf.state() << endl;
cout << "P0: " << TransformP * ekf.covariance() * TransformP.transpose()
<< endl;
// First prediction stage
ekf.predict(U1, Q);
cout << "\nFirst Prediction:\n";
cout << "X: " << ekf.state() << endl;
cout << "P: " << TransformP * ekf.covariance() * TransformP.transpose()
<< endl;
// First update stage
ekf.update(h_gps, z1, R);
cout << "\nFirst Update:\n";
cout << "X: " << ekf.state() << endl;
cout << "P: " << TransformP * ekf.covariance() * TransformP.transpose()
<< endl;
// Second prediction stage
ekf.predict(U2, Q);
cout << "\nSecond Prediction:\n";
cout << "X: " << ekf.state() << endl;
cout << "P: " << TransformP * ekf.covariance() * TransformP.transpose()
<< endl;
// Second update stage
ekf.update(h_gps, z2, R);
cout << "\nSecond Update:\n";
cout << "X: " << ekf.state() << endl;
cout << "P: " << TransformP * ekf.covariance() * TransformP.transpose()
<< endl;
return 0;
}

View File

@ -27,8 +27,6 @@
#include <gtsam/slam/dataset.h> #include <gtsam/slam/dataset.h>
#include <time.h> #include <time.h>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
#include <fstream> #include <fstream>
#include <string> #include <string>
#include <vector> #include <vector>

View File

@ -277,6 +277,10 @@ inline Class expmap_default(const Class& t, const Vector& d) {
template<typename T> template<typename T>
class IsLieGroup: public IsGroup<T>, public IsManifold<T> { class IsLieGroup: public IsGroup<T>, public IsManifold<T> {
public: public:
// Concept marker: allows checking IsLieGroup<T>::value in templates
static constexpr bool value =
std::is_base_of<lie_group_tag, typename traits<T>::structure_category>::value;
typedef typename traits<T>::structure_category structure_category_tag; typedef typename traits<T>::structure_category structure_category_tag;
typedef typename traits<T>::ManifoldType ManifoldType; typedef typename traits<T>::ManifoldType ManifoldType;
typedef typename traits<T>::TangentVector TangentVector; typedef typename traits<T>::TangentVector TangentVector;
@ -284,7 +288,7 @@ public:
GTSAM_CONCEPT_USAGE(IsLieGroup) { GTSAM_CONCEPT_USAGE(IsLieGroup) {
static_assert( static_assert(
(std::is_base_of<lie_group_tag, structure_category_tag>::value), value,
"This type's trait does not assert it is a Lie group (or derived)"); "This type's trait does not assert it is a Lie group (or derived)");
// group operations with Jacobians // group operations with Jacobians

View File

@ -138,10 +138,13 @@ public:
static const int dim = traits<T>::dimension; static const int dim = traits<T>::dimension;
typedef typename traits<T>::ManifoldType ManifoldType; typedef typename traits<T>::ManifoldType ManifoldType;
typedef typename traits<T>::TangentVector TangentVector; typedef typename traits<T>::TangentVector TangentVector;
// Concept marker: allows checking IsManifold<T>::value in templates
static constexpr bool value =
std::is_base_of<manifold_tag, structure_category_tag>::value;
GTSAM_CONCEPT_USAGE(IsManifold) { GTSAM_CONCEPT_USAGE(IsManifold) {
static_assert( static_assert(
(std::is_base_of<manifold_tag, structure_category_tag>::value), value,
"This type's structure_category trait does not assert it as a manifold (or derived)"); "This type's structure_category trait does not assert it as a manifold (or derived)");
static_assert(TangentVector::SizeAtCompileTime == dim); static_assert(TangentVector::SizeAtCompileTime == dim);

View File

@ -0,0 +1,89 @@
/* ----------------------------------------------------------------------------
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
* Atlanta, Georgia 30332-0415
* All Rights Reserved
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
* See LICENSE for the license information
* -------------------------------------------------------------------------- */
/**
* @file InvariantEKF.h
* @brief Left-Invariant Extended Kalman Filter implementation.
*
* This file defines the InvariantEKF class template, inheriting from LieGroupEKF,
* which specifically implements the Left-Invariant EKF formulation. It restricts
* prediction methods to only those based on group composition (state-independent
* motion models), hiding the state-dependent prediction variants from LieGroupEKF.
*
* @date April 24, 2025
* @authors Scott Baker, Matt Kielo, Frank Dellaert
*/
#pragma once
#include <gtsam/navigation/LieGroupEKF.h> // Include the base class
namespace gtsam {
/**
* @class InvariantEKF
* @brief Left-Invariant Extended Kalman Filter on a Lie group G.
*
* @tparam G Lie group type (must satisfy LieGroup concept).
*
* This filter inherits from LieGroupEKF but restricts the prediction interface
* to only the left-invariant prediction methods:
* 1. Prediction via group composition: `predict(const G& U, const Matrix& Q)`
* 2. Prediction via tangent control vector: `predict(const TangentVector& u, double dt, const Matrix& Q)`
*
* The state-dependent prediction methods from LieGroupEKF are hidden.
* The update step remains the same as in ManifoldEKF/LieGroupEKF.
*/
template <typename G>
class InvariantEKF : public LieGroupEKF<G> {
public:
using Base = LieGroupEKF<G>; ///< Base class type
using TangentVector = typename Base::TangentVector; ///< Tangent vector type
using MatrixN = typename Base::MatrixN; ///< Square matrix type for covariance etc.
using Jacobian = typename Base::Jacobian; ///< Jacobian matrix type specific to the group G
/// Constructor: forwards to LieGroupEKF constructor
InvariantEKF(const G& X0, const MatrixN& P0) : Base(X0, P0) {}
/**
* Predict step via group composition (Left-Invariant):
* X_{k+1} = X_k * U
* P_{k+1} = Ad_{U^{-1}} P_k Ad_{U^{-1}}^T + Q
* where Ad_{U^{-1}} is the Adjoint map of U^{-1}. This corresponds to
* F = Ad_{U^{-1}} in the base class predict method.
*
* @param U Lie group element representing the motion increment.
* @param Q Process noise covariance in the tangent space (size nxn).
*/
void predict(const G& U, const Matrix& Q) {
this->X_ = this->X_.compose(U);
// TODO(dellaert): traits<G>::AdjointMap should exist
Jacobian A = traits<G>::Inverse(U).AdjointMap();
this->P_ = A * this->P_ * A.transpose() + Q;
}
/**
* Predict step via tangent control vector:
* U = Expmap(u * dt)
* Then calls predict(U, Q).
*
* @param u Tangent space control vector.
* @param dt Time interval.
* @param Q Process noise covariance matrix (size nxn).
*/
void predict(const TangentVector& u, double dt, const Matrix& Q) {
G U = traits<G>::Expmap(u * dt);
predict(U, Q); // Call the group composition predict
}
}; // InvariantEKF
} // namespace gtsam

View File

@ -0,0 +1,165 @@
/* ----------------------------------------------------------------------------
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
* Atlanta, Georgia 30332-0415
* All Rights Reserved
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
* See LICENSE for the license information
* -------------------------------------------------------------------------- */
/**
* @file LieGroupEKF.h
* @brief Extended Kalman Filter derived class for Lie groups G.
*
* This file defines the LieGroupEKF class template, inheriting from ManifoldEKF,
* for performing EKF steps specifically on states residing in a Lie group.
* It provides predict methods with state-dependent dynamics functions.
* Please use the InvariantEKF class for prediction via group composition.
*
* @date April 24, 2025
* @authors Scott Baker, Matt Kielo, Frank Dellaert
*/
#pragma once
#include <gtsam/navigation/ManifoldEKF.h> // Include the base class
#include <gtsam/base/Lie.h> // Include for Lie group traits and operations
#include <Eigen/Dense>
#include <type_traits>
#include <functional> // For std::function
namespace gtsam {
/**
* @class LieGroupEKF
* @brief Extended Kalman Filter on a Lie group G, derived from ManifoldEKF
*
* @tparam G Lie group type providing group operations and Expmap/AdjointMap.
* Must satisfy LieGroup concept (`gtsam::IsLieGroup<G>::value`).
*
* This filter specializes ManifoldEKF for Lie groups, offering predict methods
* with state-dependent dynamics functions.
* Use the InvariantEKF class for prediction via group composition.
*/
template <typename G>
class LieGroupEKF : public ManifoldEKF<G> {
public:
using Base = ManifoldEKF<G>; ///< Base class type
static constexpr int n = Base::n; ///< Group dimension (tangent space dimension)
using TangentVector = typename Base::TangentVector; ///< Tangent vector type for the group G
using MatrixN = typename Base::MatrixN; ///< Square matrix of size n for covariance and Jacobians
using Jacobian = Eigen::Matrix<double, n, n>; ///< Jacobian matrix type specific to the group G
/// Constructor: initialize with state and covariance
LieGroupEKF(const G& X0, const MatrixN& P0) : Base(X0, P0) {
static_assert(IsLieGroup<G>::value, "Template parameter G must be a GTSAM Lie Group");
}
/**
* SFINAE check for correctly typed state-dependent dynamics function.
* Signature: TangentVector f(const G& X, OptionalJacobian<n, n> Df)
* Df = d(xi)/d(local(X))
*/
template <typename Dynamics>
using enable_if_dynamics = std::enable_if_t<
!std::is_convertible_v<Dynamics, TangentVector>&&
std::is_invocable_r_v<TangentVector, Dynamics, const G&,
OptionalJacobian<n, n>&>>;
/**
* Predict mean and Jacobian A with state-dependent dynamics:
* xi = f(X_k, Df) (Compute tangent vector dynamics and Jacobian Df)
* U = Expmap(xi * dt, Dexp) (Compute motion increment U and Expmap Jacobian Dexp)
* X_{k+1} = X_k * U (Predict next state)
* F = Ad_{U^{-1}} + Dexp * Df * dt (Compute full state transition Jacobian)
*
* @tparam Dynamics Functor signature: TangentVector f(const G&, OptionalJacobian<n,n>&)
* @param f Dynamics functor returning tangent vector xi and its Jacobian Df w.r.t. local(X).
* @param dt Time step.
* @param A Optional pointer to store the computed state transition Jacobian A.
* @return Predicted state X_{k+1}.
*/
template <typename Dynamics, typename = enable_if_dynamics<Dynamics>>
G predictMean(Dynamics&& f, double dt, OptionalJacobian<n, n> A = {}) const {
Jacobian Df, Dexp;
TangentVector xi = f(this->X_, Df); // xi and Df = d(xi)/d(localX)
G U = traits<G>::Expmap(xi * dt, Dexp); // U and Dexp = d(Log(Exp(v)))/dv | v=xi*dt
G X_next = this->X_.compose(U);
if (A) {
// Full state transition Jacobian for left-invariant EKF:
*A = traits<G>::Inverse(U).AdjointMap() + Dexp * Df * dt;
}
return X_next;
}
/**
* Predict step with state-dependent dynamics:
* Uses predictMean to compute X_{k+1} and F, then calls base predict.
* X_{k+1}, F = predictMean(f, dt)
* P_{k+1} = F P_k F^T + Q
*
* @tparam Dynamics Functor signature: TangentVector f(const G&, OptionalJacobian<n,n>&)
* @param f Dynamics functor.
* @param dt Time step.
* @param Q Process noise covariance (size nxn).
*/
template <typename Dynamics, typename = enable_if_dynamics<Dynamics>>
void predict(Dynamics&& f, double dt, const Matrix& Q) {
Jacobian A;
this->X_ = predictMean(std::forward<Dynamics>(f), dt, A);
this->P_ = A * this->P_ * A.transpose() + Q;
}
/**
* SFINAE check for state- and control-dependent dynamics function.
* Signature: TangentVector f(const G& X, const Control& u, OptionalJacobian<n, n> Df)
*/
template<typename Control, typename Dynamics>
using enable_if_full_dynamics = std::enable_if_t<
std::is_invocable_r_v<TangentVector, Dynamics, const G&, const Control&, OptionalJacobian<n, n>&>
>;
/**
* Predict mean and Jacobian A with state and control input dynamics:
* Wraps the dynamics function and calls the state-only predictMean.
* xi = f(X_k, u, Df)
*
* @tparam Control Control input type.
* @tparam Dynamics Functor signature: TangentVector f(const G&, const Control&, OptionalJacobian<n,n>&)
* @param f Dynamics functor.
* @param u Control input.
* @param dt Time step.
* @param A Optional pointer to store the computed state transition Jacobian A.
* @return Predicted state X_{k+1}.
*/
template <typename Control, typename Dynamics, typename = enable_if_full_dynamics<Control, Dynamics>>
G predictMean(Dynamics&& f, const Control& u, double dt, OptionalJacobian<n, n> A = {}) const {
return predictMean([&](const G& X, OptionalJacobian<n, n> Df) { return f(X, u, Df); }, dt, A);
}
/**
* Predict step with state and control input dynamics:
* Wraps the dynamics function and calls the state-only predict.
* xi = f(X_k, u, Df)
*
* @tparam Control Control input type.
* @tparam Dynamics Functor signature: TangentVector f(const G&, const Control&, OptionalJacobian<n,n>&)
* @param f Dynamics functor.
* @param u Control input.
* @param dt Time step.
* @param Q Process noise covariance (size nxn).
*/
template <typename Control, typename Dynamics, typename = enable_if_full_dynamics<Control, Dynamics>>
void predict(Dynamics&& f, const Control& u, double dt, const Matrix& Q) {
return predict([&](const G& X, OptionalJacobian<n, n> Df) { return f(X, u, Df); }, dt, Q);
}
}; // LieGroupEKF
} // namespace gtsam

View File

@ -0,0 +1,155 @@
/* ----------------------------------------------------------------------------
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
* Atlanta, Georgia 30332-0415
* All Rights Reserved
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
* See LICENSE for the license information
* -------------------------------------------------------------------------- */
/**
* @file ManifoldEKF.h
* @brief Extended Kalman Filter base class on a generic manifold M
*
* This file defines the ManifoldEKF class template for performing prediction
* and update steps of an Extended Kalman Filter on states residing in a
* differentiable manifold. It relies on the manifold's retract and
* localCoordinates operations.
*
* @date April 24, 2025
* @authors Scott Baker, Matt Kielo, Frank Dellaert
*/
#pragma once
#include <gtsam/base/Matrix.h>
#include <gtsam/base/OptionalJacobian.h>
#include <gtsam/base/Vector.h>
#include <gtsam/base/Manifold.h> // Include for traits
#include <Eigen/Dense>
#include <type_traits>
namespace gtsam {
/**
* @class ManifoldEKF
* @brief Extended Kalman Filter on a generic manifold M
*
* @tparam M Manifold type providing:
* - static int dimension = tangent dimension
* - using TangentVector = Eigen::Vector...
* - A `retract(const TangentVector&)` method (member or static)
* - A `localCoordinates(const M&)` method (member or static)
* - `gtsam::traits<M>` specialization must exist.
*
* This filter maintains a state X in the manifold M and covariance P in the
* tangent space at X. Prediction requires providing the predicted next state
* and the state transition Jacobian F. Updates apply a measurement function h
* and correct the state using the tangent space error.
*/
template <typename M>
class ManifoldEKF {
public:
/// Manifold dimension (tangent space dimension)
static constexpr int n = traits<M>::dimension;
/// Tangent vector type for the manifold M
using TangentVector = typename traits<M>::TangentVector;
/// Square matrix of size n for covariance and Jacobians
using MatrixN = Eigen::Matrix<double, n, n>;
/// Constructor: initialize with state and covariance
ManifoldEKF(const M& X0, const MatrixN& P0) : X_(X0), P_(P0) {
static_assert(IsManifold<M>::value, "Template parameter M must be a GTSAM Manifold");
}
virtual ~ManifoldEKF() = default; // Add virtual destructor for base class
/// @return current state estimate
const M& state() const { return X_; }
/// @return current covariance estimate
const MatrixN& covariance() const { return P_; }
/**
* Basic predict step: Updates state and covariance given the predicted
* next state and the state transition Jacobian F.
* X_{k+1} = X_next
* P_{k+1} = F P_k F^T + Q
* where F = d(local(X_{k+1})) / d(local(X_k)) is the Jacobian of the
* state transition in local coordinates around X_k.
*
* @param X_next The predicted state at time k+1.
* @param F The state transition Jacobian (size nxn).
* @param Q Process noise covariance matrix in the tangent space (size nxn).
*/
void predict(const M& X_next, const MatrixN& F, const Matrix& Q) {
X_ = X_next;
P_ = F * P_ * F.transpose() + Q;
}
/**
* Measurement update: Corrects the state and covariance using a measurement.
* z_pred, H = h(X)
* y = z - z_pred (innovation, or z_pred - z depending on convention)
* S = H P H^T + R (innovation covariance)
* K = P H^T S^{-1} (Kalman gain)
* delta_xi = -K * y (correction in tangent space)
* X <- X.retract(delta_xi)
* P <- (I - K H) P
*
* @tparam Measurement Type of the measurement vector (e.g., VectorN<m>)
* @tparam Prediction Functor signature: Measurement h(const M&,
* OptionalJacobian<m,n>&)
* where m is the measurement dimension.
*
* @param h Measurement model functor returning predicted measurement z_pred
* and its Jacobian H = d(h)/d(local(X)).
* @param z Observed measurement.
* @param R Measurement noise covariance (size m x m).
*/
template <typename Measurement, typename Prediction>
void update(Prediction&& h, const Measurement& z, const Matrix& R) {
constexpr int m = traits<Measurement>::dimension;
Eigen::Matrix<double, m, n> H;
// Predict measurement and get Jacobian H = dh/dlocal(X)
Measurement z_pred = h(X_, H);
// Innovation
// Ensure consistent subtraction for manifold types if Measurement is one
Vector innovation = traits<Measurement>::Local(z, z_pred); // y = z_pred (-) z (in tangent space)
// Innovation covariance and Kalman Gain
auto S = H * P_ * H.transpose() + R;
Matrix K = P_ * H.transpose() * S.inverse(); // K = P H^T S^-1 (size n x m)
// Correction vector in tangent space
TangentVector delta_xi = -K * innovation; // delta_xi = - K * y
// Update state using retract
X_ = traits<M>::Retract(X_, delta_xi); // X <- X.retract(delta_xi)
// Update covariance using Joseph form:
MatrixN I_KH = I_n - K * H;
P_ = I_KH * P_ * I_KH.transpose() + K * R * K.transpose();
}
protected:
M X_; ///< manifold state estimate
MatrixN P_; ///< covariance in tangent space at X_
private:
/// Identity matrix of size n
static const MatrixN I_n;
};
// Define static identity I_n
template <typename M>
const typename ManifoldEKF<M>::MatrixN ManifoldEKF<M>::I_n = ManifoldEKF<M>::MatrixN::Identity();
} // namespace gtsam

View File

@ -0,0 +1,125 @@
/* ----------------------------------------------------------------------------
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
* Atlanta, Georgia 30332-0415
* All Rights Reserved
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
*
* See LICENSE for the license information
* -------------------------------------------------------------------------- */
/**
* @file testInvariantEKF_Pose2.cpp
* @brief Unit test for the InvariantEKF using Pose2 state.
* Based on the logic from IEKF_SE2Example.cpp
* @date April 26, 2025
* @authors Frank Dellaert (adapted from example by Scott Baker, Matt Kielo)
*/
#include <CppUnitLite/TestHarness.h>
#include <gtsam/base/Testable.h>
#include <gtsam/base/numericalDerivative.h>
#include <gtsam/geometry/Pose2.h>
#include <gtsam/navigation/InvariantEKF.h>
#include <iostream>
using namespace std;
using namespace gtsam;
// Create a 2D GPS measurement function that returns the predicted measurement h
// and Jacobian H. The predicted measurement h is the translation of the state X.
// (Copied from IEKF_SE2Example.cpp)
Vector2 h_gps(const Pose2& X, OptionalJacobian<2, 3> H = {}) {
return X.translation(H);
}
TEST(IEKF_Pose2, PredictUpdateSequence) {
// GIVEN: Initial state, covariance, noises, motions, measurements
// (from IEKF_SE2Example.cpp)
Pose2 X0(0.0, 0.0, 0.0);
Matrix3 P0 = Matrix3::Identity() * 0.1;
Matrix3 Q = (Vector3(0.05, 0.05, 0.001)).asDiagonal();
Matrix2 R = I_2x2 * 0.01;
Pose2 U1(1.0, 1.0, 0.5), U2(1.0, 1.0, 0.0);
Vector2 z1, z2;
z1 << 1.0, 0.0;
z2 << 1.0, 1.0;
// Create the filter
InvariantEKF<Pose2> ekf(X0, P0);
EXPECT(assert_equal(X0, ekf.state()));
EXPECT(assert_equal(P0, ekf.covariance()));
// --- First Prediction ---
ekf.predict(U1, Q);
// Calculate expected state and covariance
Pose2 X1_expected = X0.compose(U1);
Matrix3 Ad_U1_inv = U1.inverse().AdjointMap();
Matrix3 P1_expected = Ad_U1_inv * P0 * Ad_U1_inv.transpose() + Q;
// Verify
EXPECT(assert_equal(X1_expected, ekf.state(), 1e-9));
EXPECT(assert_equal(P1_expected, ekf.covariance(), 1e-9));
// --- First Update ---
ekf.update(h_gps, z1, R);
// Calculate expected state and covariance (manual Kalman steps)
Matrix H1; // H = dh/dlocal(X) -> 2x3
Vector2 z_pred1 = h_gps(X1_expected, H1);
Vector2 y1 = z_pred1 - z1; // Innovation
Matrix S1 = H1 * P1_expected * H1.transpose() + R;
Matrix K1 = P1_expected * H1.transpose() * S1.inverse(); // Gain (3x2)
Vector3 delta_xi1 = -K1 * y1; // Correction (tangent space)
Pose2 X1_updated_expected = X1_expected.retract(delta_xi1);
Matrix3 I_KH1 = Matrix3::Identity() - K1 * H1;
Matrix3 P1_updated_expected = I_KH1 * P1_expected; // Standard form P = (I-KH)P
// Verify
EXPECT(assert_equal(X1_updated_expected, ekf.state(), 1e-9));
EXPECT(assert_equal(P1_updated_expected, ekf.covariance(), 1e-9));
// --- Second Prediction ---
ekf.predict(U2, Q);
// Calculate expected state and covariance
Pose2 X2_expected = X1_updated_expected.compose(U2);
Matrix3 Ad_U2_inv = U2.inverse().AdjointMap();
Matrix3 P2_expected = Ad_U2_inv * P1_updated_expected * Ad_U2_inv.transpose() + Q;
// Verify
EXPECT(assert_equal(X2_expected, ekf.state(), 1e-9));
EXPECT(assert_equal(P2_expected, ekf.covariance(), 1e-9));
// --- Second Update ---
ekf.update(h_gps, z2, R);
// Calculate expected state and covariance (manual Kalman steps)
Matrix H2; // 2x3
Vector2 z_pred2 = h_gps(X2_expected, H2);
Vector2 y2 = z_pred2 - z2; // Innovation
Matrix S2 = H2 * P2_expected * H2.transpose() + R;
Matrix K2 = P2_expected * H2.transpose() * S2.inverse(); // Gain (3x2)
Vector3 delta_xi2 = -K2 * y2; // Correction (tangent space)
Pose2 X2_updated_expected = X2_expected.retract(delta_xi2);
Matrix3 I_KH2 = Matrix3::Identity() - K2 * H2;
Matrix3 P2_updated_expected = I_KH2 * P2_expected; // Standard form
// Verify
EXPECT(assert_equal(X2_updated_expected, ekf.state(), 1e-9));
EXPECT(assert_equal(P2_updated_expected, ekf.covariance(), 1e-9));
}
int main() {
TestResult tr;
return TestRegistry::runAllTests(tr);
}

View File

@ -0,0 +1,114 @@
/* ----------------------------------------------------------------------------
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
* Atlanta, Georgia 30332-0415
* All Rights Reserved
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
*
* See LICENSE for the license information
* -------------------------------------------------------------------------- */
/**
* @file testGroupEKF.cpp
* @brief Unit test for LieGroupEKF, as well as dynamics used in Rot3 example.
* @date April 26, 2025
* @authors Scott Baker, Matt Kielo, Frank Dellaert
*/
#include <CppUnitLite/TestHarness.h>
#include <gtsam/base/Testable.h>
#include <gtsam/base/numericalDerivative.h>
#include <gtsam/geometry/Rot3.h>
#include <gtsam/navigation/LieGroupEKF.h>
#include <gtsam/navigation/NavState.h>
using namespace gtsam;
// Duplicate the dynamics function in GEKF_Rot3Example
namespace exampleSO3 {
static constexpr double k = 0.5;
Vector3 dynamics(const Rot3& X, OptionalJacobian<3, 3> H = {}) {
// φ = Logmap(R), Dφ = ∂φ/∂δR
Matrix3 D_phi;
Vector3 phi = Rot3::Logmap(X, D_phi);
// zero out yaw
phi[2] = 0.0;
D_phi.row(2).setZero();
if (H) *H = -k * D_phi; // ∂(kφ)/∂δR
return -k * phi; // xi ∈ 𝔰𝔬(3)
}
} // namespace exampleSO3
TEST(GroupeEKF, DynamicsJacobian) {
// Construct a nontrivial state and IMU input
Rot3 R = Rot3::RzRyRx(0.1, -0.2, 0.3);
// Analytic Jacobian
Matrix3 actualH;
exampleSO3::dynamics(R, actualH);
// Numeric Jacobian w.r.t. the state X
auto f = [&](const Rot3& X_) { return exampleSO3::dynamics(X_); };
Matrix3 expectedH = numericalDerivative11<Vector3, Rot3>(f, R);
// Compare
EXPECT(assert_equal(expectedH, actualH));
}
TEST(GroupeEKF, PredictNumericState) {
// GIVEN
Rot3 R0 = Rot3::RzRyRx(0.2, -0.1, 0.3);
Matrix3 P0 = Matrix3::Identity() * 0.2;
double dt = 0.1;
// Analytic Jacobian
Matrix3 actualH;
LieGroupEKF<Rot3> ekf0(R0, P0);
ekf0.predictMean(exampleSO3::dynamics, dt, actualH);
// wrap predict into a state->state functor (mapping on SO(3))
auto g = [&](const Rot3& R) -> Rot3 {
LieGroupEKF<Rot3> ekf(R, P0);
return ekf.predictMean(exampleSO3::dynamics, dt);
};
// numeric Jacobian of g at R0
Matrix3 expectedH = numericalDerivative11<Rot3, Rot3>(g, R0);
EXPECT(assert_equal(expectedH, actualH));
}
TEST(GroupeEKF, StateAndControl) {
auto f = [](const Rot3& X, const Vector2& dummy_u,
OptionalJacobian<3, 3> H = {}) {
return exampleSO3::dynamics(X, H);
};
// GIVEN
Rot3 R0 = Rot3::RzRyRx(0.2, -0.1, 0.3);
Matrix3 P0 = Matrix3::Identity() * 0.2;
Vector2 dummy_u(1, 2);
double dt = 0.1;
Matrix3 Q = Matrix3::Zero();
// Analytic Jacobian
Matrix3 actualH;
LieGroupEKF<Rot3> ekf0(R0, P0);
ekf0.predictMean(f, dummy_u, dt, actualH);
// wrap predict into a state->state functor (mapping on SO(3))
auto g = [&](const Rot3& R) -> Rot3 {
LieGroupEKF<Rot3> ekf(R, P0);
return ekf.predictMean(f, dummy_u, dt, Q);
};
// numeric Jacobian of g at R0
Matrix3 expectedH = numericalDerivative11<Rot3, Rot3>(g, R0);
EXPECT(assert_equal(expectedH, actualH));
}
int main() {
TestResult tr;
return TestRegistry::runAllTests(tr);
}

View File

@ -0,0 +1,154 @@
/* ----------------------------------------------------------------------------
* GTSAM Copyright 2010, Georgia Tech Research Corporation,
* Atlanta, Georgia 30332-0415
* All Rights Reserved
* Authors: Frank Dellaert, et al. (see THANKS for the full author list)
*
* See LICENSE for the license information
* -------------------------------------------------------------------------- */
/**
* @file testManifoldEKF.cpp
* @brief Unit test for the ManifoldEKF base class using Unit3.
* @date April 26, 2025
* @authors Scott Baker, Matt Kielo, Frank Dellaert
*/
#include <gtsam/base/Testable.h>
#include <gtsam/base/numericalDerivative.h>
#include <gtsam/geometry/Point3.h>
#include <gtsam/geometry/Unit3.h>
#include <gtsam/navigation/ManifoldEKF.h>
#include <CppUnitLite/TestHarness.h>
#include <iostream>
using namespace gtsam;
// Define simple dynamics for Unit3: constant velocity in the tangent space
namespace exampleUnit3 {
// Predicts the next state given current state, tangent velocity, and dt
Unit3 predictNextState(const Unit3& p, const Vector2& v, double dt) {
return p.retract(v * dt);
}
// Define a measurement model: measure the z-component of the Unit3 direction
// H is the Jacobian dh/d(local(p))
Vector1 measureZ(const Unit3& p, OptionalJacobian<1, 2> H) {
if (H) {
// H = d(p.point3().z()) / d(local(p))
// Calculate numerically for simplicity in test
auto h = [](const Unit3& p_) { return Vector1(p_.point3().z()); };
*H = numericalDerivative11<Vector1, Unit3, 2>(h, p);
}
return Vector1(p.point3().z());
}
} // namespace exampleUnit3
// Test fixture for ManifoldEKF with Unit3
struct Unit3EKFTest {
Unit3 p0;
Matrix2 P0;
Vector2 velocity;
double dt;
Matrix2 Q; // Process noise
Matrix1 R; // Measurement noise
Unit3EKFTest() :
p0(Unit3(Point3(1, 0, 0))), // Start pointing along X-axis
P0(I_2x2 * 0.01),
velocity((Vector2() << 0.0, M_PI / 4.0).finished()), // Rotate towards +Z axis
dt(0.1),
Q(I_2x2 * 0.001),
R(Matrix1::Identity() * 0.01) {
}
};
TEST(ManifoldEKF_Unit3, Predict) {
Unit3EKFTest data;
// Initialize the EKF
ManifoldEKF<Unit3> ekf(data.p0, data.P0);
// --- Prepare inputs for ManifoldEKF::predict ---
// 1. Compute expected next state
Unit3 p_next_expected = exampleUnit3::predictNextState(data.p0, data.velocity, data.dt);
// 2. Compute state transition Jacobian F = d(local(p_next)) / d(local(p))
// We can compute this numerically using the predictNextState function.
// GTSAM's numericalDerivative handles derivatives *between* manifolds.
auto predict_wrapper = [&](const Unit3& p) -> Unit3 {
return exampleUnit3::predictNextState(p, data.velocity, data.dt);
};
Matrix2 F = numericalDerivative11<Unit3, Unit3>(predict_wrapper, data.p0);
// --- Perform EKF prediction ---
ekf.predict(p_next_expected, F, data.Q);
// --- Verification ---
// Check state
EXPECT(assert_equal(p_next_expected, ekf.state(), 1e-8));
// Check covariance
Matrix2 P_expected = F * data.P0 * F.transpose() + data.Q;
EXPECT(assert_equal(P_expected, ekf.covariance(), 1e-8));
// Check F manually for a simple case (e.g., zero velocity should give Identity)
Vector2 zero_velocity = Vector2::Zero();
auto predict_wrapper_zero = [&](const Unit3& p) -> Unit3 {
return exampleUnit3::predictNextState(p, zero_velocity, data.dt);
};
Matrix2 F_zero = numericalDerivative11<Unit3, Unit3>(predict_wrapper_zero, data.p0);
EXPECT(assert_equal<Matrix2>(I_2x2, F_zero, 1e-8));
}
TEST(ManifoldEKF_Unit3, Update) {
Unit3EKFTest data;
// Use a slightly different starting point and covariance for variety
Unit3 p_start = Unit3(Point3(0, 1, 0)).retract((Vector2() << 0.1, 0).finished()); // Perturb pointing along Y
Matrix2 P_start = I_2x2 * 0.05;
ManifoldEKF<Unit3> ekf(p_start, P_start);
// Simulate a measurement (e.g., true value + noise)
Vector1 z_true = exampleUnit3::measureZ(p_start, {});
Vector1 z_observed = z_true + Vector1(0.02); // Add some noise
// --- Perform EKF update ---
ekf.update(exampleUnit3::measureZ, z_observed, data.R);
// --- Verification (Manual Kalman Update Steps) ---
// 1. Predict measurement and get Jacobian H
Matrix12 H; // Note: Jacobian is 1x2 for Unit3
Vector1 z_pred = exampleUnit3::measureZ(p_start, H);
// 2. Innovation and Covariance
Vector1 y = z_pred - z_observed; // Innovation (using vector subtraction for z)
Matrix1 S = H * P_start * H.transpose() + data.R; // 1x1 matrix
// 3. Kalman Gain K
Matrix K = P_start * H.transpose() * S.inverse(); // 2x1 matrix
// 4. State Correction (in tangent space)
Vector2 delta_xi = -K * y; // 2x1 vector
// 5. Expected Updated State and Covariance
Unit3 p_updated_expected = p_start.retract(delta_xi);
Matrix2 I_KH = I_2x2 - K * H;
Matrix2 P_updated_expected = I_KH * P_start;
// --- Compare EKF result with manual calculation ---
EXPECT(assert_equal(p_updated_expected, ekf.state(), 1e-8));
EXPECT(assert_equal(P_updated_expected, ekf.covariance(), 1e-8));
}
int main() {
TestResult tr;
return TestRegistry::runAllTests(tr);
}