gtsam/gtsam/navigation/tests/testManifoldEKF.cpp

154 lines
5.1 KiB
C++

/* ----------------------------------------------------------------------------
* 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);
}