use apex_manifolds::se3::SE3;
use apex_manifolds::se23::{SE23, SE23Tangent};
use apex_manifolds::{LieGroup, Tangent, rn::Rn};
use nalgebra::{DMatrix, DVector, Vector3};
use super::factors::{CombinedImuFactor, ImuFactor};
use crate::core::variable::{ManifoldVariable, Variable};
use crate::factors::Factor;
use crate::factors::imu::preintegration::ImuPreintegration;
use crate::factors::imu::types::{
ImuMeasurement, ImuParameters, ImuSensorReadings, SpeedAndBias, SpeedAndBiasExt,
};
const FD_EPS: f64 = 1e-5;
const FD_TOL: f64 = 1e-3;
fn euroc_params() -> ImuParameters {
ImuParameters {
sigma_g_c: 1.6968e-04,
sigma_a_c: 2.0000e-03,
sigma_gw_c: 1.9393e-05,
sigma_aw_c: 3.0000e-03,
g: 9.81,
..ImuParameters::default()
}
}
fn scenario() -> (ImuPreintegration, SE3, SpeedAndBias) {
let params = euroc_params();
let g = params.g;
let dt_step = 0.005_f64;
let n = 201_usize;
let t1 = (n - 1) as f64 * dt_step;
let measurements: Vec<_> = (0..n)
.map(|i| {
ImuMeasurement::new(
i as f64 * dt_step,
ImuSensorReadings {
gyroscopes: Vector3::new(0.0, 0.0, 0.1),
accelerometers: Vector3::new(0.0, 0.0, g),
},
)
})
.collect();
let sb_zero = SpeedAndBias::zeros();
let preint = ImuPreintegration::new(measurements.clone(), params.clone(), 0.0, t1, &sb_zero);
let mut pose_j = SE3::identity();
let mut sb_j = SpeedAndBias::zeros();
ImuPreintegration::propagation(&measurements, ¶ms, &mut pose_j, &mut sb_j, 0.0, t1);
(preint, pose_j, sb_j)
}
fn state_of(pose: &SE3, v: Vector3<f64>) -> DVector<f64> {
let s = SE23::new(pose.translation(), v, pose.rotation_quaternion());
DVector::from_column_slice(s.as_param_slice())
}
fn residual_of<F: Factor>(f: &F, params: &[&[f64]]) -> Vec<f64> {
let mut r = vec![0.0f64; f.residual_dim()];
f.linearize(params, &mut r, None);
r
}
fn perturb(block: &[f64], k: usize, eps: f64, is_state: bool) -> DVector<f64> {
if is_state {
let mut t = [0.0f64; 9];
t[k] = eps;
let moved =
SE23::from_param_slice(block).right_plus(&SE23Tangent::from_slice(&t), None, None);
DVector::from_column_slice(moved.as_param_slice())
} else {
let mut out = DVector::from_column_slice(block);
out[k] += eps;
out
}
}
fn check_jacobian<F: Factor>(f: &F, blocks: &[DVector<f64>], state_blocks: &[bool], label: &str) {
let params: Vec<&[f64]> = blocks.iter().map(|b| b.as_slice()).collect();
let (rows, cols) = f.jacobian_shape();
let mut r = vec![0.0; rows];
let mut buf = vec![0.0; rows * cols];
let jm = faer::mat::MatMut::from_column_major_slice_mut(&mut buf, rows, cols);
f.linearize(¶ms, &mut r, Some(jm));
let analytic = DMatrix::from_column_slice(rows, cols, &buf);
let mut col = 0usize;
for (b, &is_state) in state_blocks.iter().enumerate() {
let dof = if is_state { 9 } else { blocks[b].len() };
for k in 0..dof {
let mut plus = blocks.to_vec();
plus[b] = perturb(blocks[b].as_slice(), k, FD_EPS, is_state);
let mut minus = blocks.to_vec();
minus[b] = perturb(blocks[b].as_slice(), k, -FD_EPS, is_state);
let rp = residual_of(f, &plus.iter().map(|v| v.as_slice()).collect::<Vec<_>>());
let rm = residual_of(f, &minus.iter().map(|v| v.as_slice()).collect::<Vec<_>>());
for row in 0..rows {
let fd = (rp[row] - rm[row]) / (2.0 * FD_EPS);
let a = analytic[(row, col)];
let scale = 1.0 + a.abs().max(fd.abs());
assert!(
(a - fd).abs() / scale < FD_TOL,
"{label}: block {b} dof {k} (col {col}) row {row}: \
analytic={a:.8} fd={fd:.8}"
);
}
col += 1;
}
}
assert_eq!(col, cols, "{label}: column count");
}
fn perturbed(pose_j: &SE3, sb_j: &SpeedAndBias) -> (DVector<f64>, DVector<f64>, DVector<f64>) {
let pose_i = SE3::from_param_slice(&[0.05, -0.02, 0.01, 0.99875, 0.03, 0.02, 0.02]);
(
state_of(&pose_i, Vector3::new(0.03, -0.01, 0.02)),
state_of(pose_j, sb_j.velocity() + Vector3::new(0.01, -0.02, 0.005)),
DVector::from_vec(vec![1e-3, -2e-3, 5e-4, 1e-2, -5e-3, 2e-3]),
)
}
#[test]
fn factor_shapes() {
let (preint, _, _) = scenario();
let imu = ImuFactor::new(preint.clone());
assert_eq!(imu.residual_dim(), 9);
assert_eq!(imu.jacobian_shape(), (9, 24));
let combined = CombinedImuFactor::new(preint);
assert_eq!(combined.residual_dim(), 15);
assert_eq!(combined.jacobian_shape(), (15, 30));
}
#[test]
fn imu_factor_residual_vanishes_at_ground_truth() {
let (preint, pose_j, sb_j) = scenario();
let factor = ImuFactor::new(preint);
let state_i = state_of(&SE3::identity(), Vector3::zeros());
let state_j = state_of(&pose_j, sb_j.velocity());
let bias = DVector::zeros(6);
let r = residual_of(
&factor,
&[state_i.as_slice(), state_j.as_slice(), bias.as_slice()],
);
assert_eq!(r.len(), 9);
for (i, v) in r.iter().enumerate() {
assert!(v.abs() < 1e-6, "residual[{i}] = {v:.3e}");
}
}
#[test]
fn combined_imu_factor_residual_vanishes_at_ground_truth() {
let (preint, pose_j, sb_j) = scenario();
let factor = CombinedImuFactor::new(preint);
let state_i = state_of(&SE3::identity(), Vector3::zeros());
let state_j = state_of(&pose_j, sb_j.velocity());
let bias = DVector::zeros(6);
let r = residual_of(
&factor,
&[
state_i.as_slice(),
bias.as_slice(),
state_j.as_slice(),
bias.as_slice(),
],
);
assert_eq!(r.len(), 15);
for (i, v) in r.iter().enumerate() {
assert!(v.abs() < 1e-6, "residual[{i}] = {v:.3e}");
}
}
#[test]
fn combined_factor_penalizes_a_bias_step() {
let (preint, pose_j, sb_j) = scenario();
let factor = CombinedImuFactor::new(preint);
let state_i = state_of(&SE3::identity(), Vector3::zeros());
let state_j = state_of(&pose_j, sb_j.velocity());
let bias_i = DVector::zeros(6);
let mut bias_j = DVector::zeros(6);
bias_j[0] = 1e-3;
let r = residual_of(
&factor,
&[
state_i.as_slice(),
bias_i.as_slice(),
state_j.as_slice(),
bias_j.as_slice(),
],
);
let walk_rows: f64 = r[9..15].iter().map(|v| v * v).sum();
assert!(
walk_rows > 0.0,
"a bias step between keyframes must show in the walk rows"
);
}
#[test]
fn imu_factor_jacobians_match_finite_differences() {
let (preint, pose_j, sb_j) = scenario();
let factor = ImuFactor::new(preint);
let (si, sj, bias) = perturbed(&pose_j, &sb_j);
check_jacobian(
&factor,
&[si, sj, bias],
&[true, true, false],
"se23::ImuFactor",
);
}
#[test]
fn combined_imu_factor_jacobians_match_finite_differences() {
let (preint, pose_j, sb_j) = scenario();
let factor = CombinedImuFactor::new(preint);
let (si, sj, bias_i) = perturbed(&pose_j, &sb_j);
let bias_j = DVector::from_vec(vec![-5e-4, 1e-3, 2e-3, -8e-3, 4e-3, 1e-3]);
check_jacobian(
&factor,
&[si, bias_i, sj, bias_j],
&[true, false, true, false],
"se23::CombinedImuFactor",
);
}
#[test]
fn kinematic_information_excludes_the_bias_random_walk() {
let (preint, _, _) = scenario();
let kinematic = preint.kinematic_square_root_information();
let combined = preint.square_root_information();
assert!(kinematic.iter().all(|v| v.is_finite()));
assert!(
kinematic.norm() > combined.fixed_view::<9, 9>(0, 0).norm(),
"kinematic-only information should exceed the combined block"
);
}
#[test]
fn factors_validate_their_layouts() {
let (preint, _, _) = scenario();
let state = Variable::new(SE23::identity());
let bias = Variable::new(Rn::new(DVector::zeros(6)));
let pose = Variable::new(SE3::identity());
let imu = ImuFactor::new(preint.clone());
let good: Vec<&dyn ManifoldVariable> = vec![&state, &state, &bias];
assert!(imu.validate_variables(&good).is_ok());
let wrong: Vec<&dyn ManifoldVariable> = vec![&pose, &state, &bias];
assert!(imu.validate_variables(&wrong).is_err());
let wrong: Vec<&dyn ManifoldVariable> = vec![&state, &bias, &state, &bias];
assert!(imu.validate_variables(&wrong).is_err());
let combined = CombinedImuFactor::new(preint);
let good: Vec<&dyn ManifoldVariable> = vec![&state, &bias, &state, &bias];
assert!(combined.validate_variables(&good).is_ok());
let wrong: Vec<&dyn ManifoldVariable> = vec![&state, &state, &bias];
assert!(combined.validate_variables(&wrong).is_err());
}