use crate::StrapdownState;
use crate::earth::{self, vector_to_skew_symmetric};
use nalgebra::{DMatrix, DVector, Rotation3, Vector3};
pub fn state_transition_jacobian(
state: &StrapdownState,
imu_accel: &Vector3<f64>,
_imu_gyro: &Vector3<f64>,
dt: f64,
) -> DMatrix<f64> {
let mut f = DMatrix::<f64>::identity(9, 9);
let lat = state.latitude;
let alt = state.altitude;
let v_n = state.velocity_north;
let v_e = state.velocity_east;
let v_d = state.velocity_vertical;
let vel = Vector3::new(v_n, v_e, v_d);
let c_bn = state.attitude.matrix();
let (r_n, r_e, _) = earth::principal_radii(&lat, &alt);
let _g = earth::gravity(&lat.to_degrees(), &alt); let omega_ie = earth::earth_rate_lla(&lat.to_degrees());
let omega_en = earth::transport_rate(&lat.to_degrees(), &alt, &vel);
let omega_ie_skew = vector_to_skew_symmetric(&omega_ie);
let omega_en_skew = vector_to_skew_symmetric(&omega_en);
let sin_lat = lat.sin();
let cos_lat = lat.cos().max(1e-6);
let sin2_lat = sin_lat * sin_lat;
let e2 = earth::ECCENTRICITY_SQUARED;
let k = earth::K;
let ge = earth::GE;
let numerator = 1.0 + k * sin2_lat;
let denominator_sqrt = (1.0 - e2 * sin2_lat).sqrt();
let dnumerator_dphi = 2.0 * k * sin_lat * cos_lat;
let ddenominator_sqrt_dphi = -e2 * sin_lat * cos_lat / denominator_sqrt;
let dgravity_dlat = ge
* (dnumerator_dphi * denominator_sqrt - numerator * ddenominator_sqrt_dphi)
/ (denominator_sqrt * denominator_sqrt);
let f_bn = c_bn * imu_accel;
f[(0, 3)] = 1.0 / (r_n + alt) * dt;
f[(1, 0)] += v_e / ((r_e + alt) * cos_lat.powi(2)) * sin_lat * dt;
f[(1, 4)] = 1.0 / ((r_e + alt) * cos_lat) * dt;
f[(2, 5)] = dt;
let coriolis_transport = -(2.0 * omega_ie_skew + omega_en_skew);
for i in 0..3 {
for j in 0..3 {
f[(3 + i, 3 + j)] += coriolis_transport[(i, j)] * dt;
}
}
if state.is_enu {
f[(5, 0)] += -dgravity_dlat * dt; } else {
f[(5, 0)] += dgravity_dlat * dt; }
let dgravity_dalt = -3.08e-6; if state.is_enu {
f[(5, 2)] += -dgravity_dalt * dt;
} else {
f[(5, 2)] += dgravity_dalt * dt;
}
let f_bn_skew = vector_to_skew_symmetric(&f_bn);
for i in 0..3 {
for j in 0..3 {
f[(3 + i, 6 + j)] += -f_bn_skew[(i, j)] * dt;
}
}
let omega_in_skew = omega_ie_skew + omega_en_skew;
for i in 0..3 {
for j in 0..3 {
f[(6 + i, 6 + j)] += -omega_in_skew[(i, j)] * dt;
}
}
f[(6, 4)] += 1.0 / (r_e + alt) * dt; f[(7, 3)] += -1.0 / (r_n + alt) * dt; f[(8, 4)] += -lat.tan() / (r_e + alt) * dt;
f
}
pub fn error_state_transition_jacobian(
state: &StrapdownState,
imu_accel: &Vector3<f64>,
imu_gyro: &Vector3<f64>,
dt: f64,
) -> DMatrix<f64> {
let mut f = DMatrix::<f64>::identity(15, 15);
let c_bn = state.attitude.matrix();
let lat = state.latitude;
let h = state.altitude;
let lat_deg = lat.to_degrees();
let (r_n, r_e, _r_p) = earth::principal_radii(&lat_deg, &h);
f[(0, 3)] = dt / r_n; f[(1, 4)] = dt / (r_e * lat.cos()); f[(2, 5)] = dt;
let f_n = c_bn * imu_accel; let f_skew = vector_to_skew_symmetric(&f_n);
for i in 0..3 {
for j in 0..3 {
f[(3 + i, 6 + j)] = -f_skew[(i, j)] * dt;
}
}
for i in 0..3 {
for j in 0..3 {
f[(3 + i, 9 + j)] = -c_bn[(i, j)] * dt;
}
}
let omega_skew = vector_to_skew_symmetric(imu_gyro);
for i in 0..3 {
for j in 0..3 {
f[(6 + i, 6 + j)] = -omega_skew[(i, j)] * dt;
}
}
f[(6, 12)] = -dt;
f[(7, 13)] = -dt;
f[(8, 14)] = -dt;
f
}
pub fn process_noise_jacobian(state: &StrapdownState, dt: f64) -> DMatrix<f64> {
let mut g = DMatrix::<f64>::zeros(9, 6);
let c_bn = state.attitude.matrix();
for i in 0..3 {
for j in 0..3 {
g[(3 + i, j)] = c_bn[(i, j)] * dt;
}
}
for i in 0..3 {
for j in 0..3 {
g[(6 + i, 3 + j)] = -c_bn[(i, j)] * dt;
}
}
g
}
pub fn gps_position_jacobian(_state: &StrapdownState) -> DMatrix<f64> {
let mut h = DMatrix::<f64>::zeros(3, 9);
h[(0, 0)] = 1.0; h[(1, 1)] = 1.0; h[(2, 2)] = 1.0; h
}
pub fn gps_velocity_jacobian(_state: &StrapdownState) -> DMatrix<f64> {
let mut h = DMatrix::<f64>::zeros(3, 9);
h[(0, 3)] = 1.0; h[(1, 4)] = 1.0; h[(2, 5)] = 1.0; h
}
pub fn gps_position_velocity_jacobian(_state: &StrapdownState) -> DMatrix<f64> {
let mut h = DMatrix::<f64>::zeros(5, 9);
h[(0, 0)] = 1.0; h[(1, 1)] = 1.0; h[(2, 2)] = 1.0; h[(3, 3)] = 1.0; h[(4, 4)] = 1.0; h
}
pub fn relative_altitude_jacobian(_state: &StrapdownState) -> DMatrix<f64> {
let mut h = DMatrix::<f64>::zeros(1, 9);
h[(0, 2)] = 1.0; h
}
pub fn gravity_anomaly_jacobian(_state: &StrapdownState) -> DMatrix<f64> {
DMatrix::<f64>::zeros(1, 9)
}
pub fn magnetic_anomaly_jacobian(_state: &StrapdownState) -> DMatrix<f64> {
DMatrix::<f64>::zeros(1, 9)
}
pub fn magnetometer_yaw_jacobian(
state: &StrapdownState,
mag_x: f64,
mag_y: f64,
mag_z: f64,
) -> DMatrix<f64> {
let mut h = DMatrix::<f64>::zeros(1, 9);
let (roll, pitch, _yaw) = state.attitude.euler_angles();
let (sin_roll, cos_roll) = roll.sin_cos();
let (sin_pitch, cos_pitch) = pitch.sin_cos();
let mag_x_h = mag_x * cos_pitch + mag_y * sin_roll * sin_pitch + mag_z * cos_roll * sin_pitch;
let mag_y_h = mag_y * cos_roll - mag_z * sin_roll;
let denom = mag_x_h.powi(2) + mag_y_h.powi(2);
if denom > 1e-10 {
let d_mx_h_d_roll = mag_y * cos_roll * sin_pitch - mag_z * sin_roll * sin_pitch;
let d_my_h_d_roll = -mag_y * sin_roll - mag_z * cos_roll;
let d_mx_h_d_pitch =
-mag_x * sin_pitch + mag_y * sin_roll * cos_pitch + mag_z * cos_roll * cos_pitch;
let d_my_h_d_pitch = 0.0;
h[(0, 6)] = (mag_x_h * d_my_h_d_roll - mag_y_h * d_mx_h_d_roll) / denom;
h[(0, 7)] = (mag_x_h * d_my_h_d_pitch - mag_y_h * d_mx_h_d_pitch) / denom;
}
h[(0, 8)] = 1.0;
h
}
pub fn apply_eskf_correction(state: &mut StrapdownState, delta_x: &DVector<f64>) {
assert!(
delta_x.len() >= 9,
"Error state must have at least 9 elements, got {}",
delta_x.len()
);
state.latitude += delta_x[0];
state.longitude += delta_x[1];
state.altitude += delta_x[2];
state.velocity_north += delta_x[3];
state.velocity_east += delta_x[4];
state.velocity_vertical += delta_x[5];
let delta_roll = delta_x[6];
let delta_pitch = delta_x[7];
let delta_yaw = delta_x[8];
let delta_rotation = Rotation3::from_euler_angles(delta_roll, delta_pitch, delta_yaw);
state.attitude = delta_rotation * state.attitude;
}
pub fn apply_eskf_correction_with_biases(
state: &mut StrapdownState,
delta_x: &DVector<f64>,
) -> Option<(Vector3<f64>, Vector3<f64>)> {
apply_eskf_correction(state, delta_x);
if delta_x.len() >= 15 {
let accel_bias = Vector3::new(delta_x[9], delta_x[10], delta_x[11]);
let gyro_bias = Vector3::new(delta_x[12], delta_x[13], delta_x[14]);
Some((accel_bias, gyro_bias))
} else {
None
}
}
pub fn assemble_error_state(
position_error: &Vector3<f64>,
conditional_mean: &DVector<f64>,
) -> DVector<f64> {
assert_eq!(
conditional_mean.len(),
12,
"Conditional mean must have 12 elements, got {}",
conditional_mean.len()
);
DVector::from_vec(vec![
position_error[0], position_error[1], position_error[2], conditional_mean[0], conditional_mean[1], conditional_mean[2], conditional_mean[3], conditional_mean[4], conditional_mean[5], conditional_mean[6], conditional_mean[7], conditional_mean[8], conditional_mean[9], conditional_mean[10], conditional_mean[11], ])
}
#[cfg(test)]
mod tests {
use super::*;
use assert_approx_eq::assert_approx_eq;
use nalgebra::Rotation3;
fn numerical_state_jacobian(
state: &StrapdownState,
imu_accel: &Vector3<f64>,
imu_gyro: &Vector3<f64>,
dt: f64,
epsilon: f64,
) -> DMatrix<f64> {
let mut jac = DMatrix::<f64>::zeros(9, 9);
let x0: Vec<f64> = state.into();
let mut state_nominal = *state;
crate::forward(
&mut state_nominal,
crate::IMUData {
accel: *imu_accel,
gyro: *imu_gyro,
},
dt,
);
let f0: Vec<f64> = (&state_nominal).into();
for j in 0..9 {
let mut x_pert = x0.clone();
x_pert[j] += epsilon;
let mut state_pert = StrapdownState::try_from(x_pert.as_slice()).unwrap();
state_pert.is_enu = state.is_enu;
crate::forward(
&mut state_pert,
crate::IMUData {
accel: *imu_accel,
gyro: *imu_gyro,
},
dt,
);
let f_pert: Vec<f64> = (&state_pert).into();
for i in 0..9 {
jac[(i, j)] = (f_pert[i] - f0[i]) / epsilon;
}
}
jac
}
#[test]
fn test_state_transition_jacobian_stationary() {
let state = StrapdownState::new(
45.0,
-122.0,
100.0,
0.0,
0.0,
0.0,
Rotation3::identity(),
true, Some(true), );
let accel = Vector3::new(0.0, 0.0, 9.81); let gyro = Vector3::zeros();
let dt = 0.0001;
let f_analytic = state_transition_jacobian(&state, &accel, &gyro, dt);
let f_numeric = numerical_state_jacobian(&state, &accel, &gyro, dt, 1e-6);
let diff = &f_analytic - &f_numeric;
let mut max_error = 0.0;
let mut max_i = 0;
let mut max_j = 0;
for i in 0..9 {
for j in 0..9 {
let err = diff[(i, j)].abs();
if err > max_error {
max_error = err;
max_i = i;
max_j = j;
}
}
}
if max_error >= 1e-6 {
eprintln!(
"Largest error at ({}, {}): analytic={:.10e}, numeric={:.10e}, diff={:.10e}",
max_i,
max_j,
f_analytic[(max_i, max_j)],
f_numeric[(max_i, max_j)],
max_error
);
let mut errors: Vec<(usize, usize, f64)> = Vec::new();
for i in 0..9 {
for j in 0..9 {
let err = diff[(i, j)].abs();
if err > 1e-7 {
errors.push((i, j, err));
}
}
}
errors.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap());
eprintln!("Top 5 errors:");
for (i, j, err) in errors.iter().take(5) {
eprintln!(" ({}, {}): {:.10e}", i, j, err);
}
}
let max_error = (&f_analytic - &f_numeric).abs().max();
assert!(
max_error < 1e-6,
"Max error {} exceeds threshold for stationary state",
max_error
);
}
#[test]
fn test_state_transition_jacobian_moving() {
let state = StrapdownState::new(
45.0,
-122.0,
100.0,
5.0,
3.0,
-0.5, Rotation3::from_euler_angles(0.01, 0.01, 0.01), true,
Some(true),
);
let accel = Vector3::new(0.1, -0.1, 9.81); let gyro = Vector3::new(0.001, -0.001, 0.002); let dt = 0.0001;
let f_analytic = state_transition_jacobian(&state, &accel, &gyro, dt);
let f_numeric = numerical_state_jacobian(&state, &accel, &gyro, dt, 1e-6);
let max_error = (&f_analytic - &f_numeric).abs().max();
assert!(
max_error < 1e-5,
"Max error {} exceeds threshold for moving state. Note: errors ~1e-5 are expected due to nonlinear coupling in trapezoidal integration.",
max_error
);
}
#[test]
fn test_state_transition_jacobian_multiple_states() {
use rand::Rng;
let mut rng = rand::rng();
for _ in 0..10 {
let lat = rng.random_range(-80.0..80.0);
let lon = rng.random_range(-180.0..180.0);
let alt = rng.random_range(0.0..5000.0);
let v_n = rng.random_range(-10.0..10.0); let v_e = rng.random_range(-10.0..10.0);
let v_d = rng.random_range(-2.0..2.0);
let roll = rng.random_range(-0.1..0.1); let pitch = rng.random_range(-0.1..0.1);
let yaw = rng.random_range(-0.5..0.5);
let state = StrapdownState::new(
lat,
lon,
alt,
v_n,
v_e,
v_d,
Rotation3::from_euler_angles(roll, pitch, yaw),
true,
Some(true),
);
let accel = Vector3::new(
rng.random_range(-0.5..0.5), rng.random_range(-0.5..0.5),
rng.random_range(9.0..10.5),
);
let gyro = Vector3::new(
rng.random_range(-0.01..0.01), rng.random_range(-0.01..0.01),
rng.random_range(-0.01..0.01),
);
let dt = 0.0001;
let f_analytic = state_transition_jacobian(&state, &accel, &gyro, dt);
let f_numeric = numerical_state_jacobian(&state, &accel, &gyro, dt, 1e-6);
let max_error = (&f_analytic - &f_numeric).abs().max();
assert!(
max_error < 5e-4,
"Max error {} exceeds threshold for random state {:?}. Note: first-order Jacobian has O(dt²) errors from nonlinear coupling.",
max_error,
(lat, lon, alt, v_n, v_e, v_d, roll, pitch, yaw)
);
}
}
#[test]
fn test_process_noise_jacobian_dimensions() {
let state = StrapdownState::default();
let dt = 0.01;
let g = process_noise_jacobian(&state, dt);
assert_eq!(g.nrows(), 9);
assert_eq!(g.ncols(), 6);
}
#[test]
fn test_process_noise_jacobian_structure() {
let state = StrapdownState::new(
45.0,
-122.0,
100.0,
0.0,
0.0,
0.0,
Rotation3::identity(),
true,
Some(true),
);
let dt = 0.01;
let g = process_noise_jacobian(&state, dt);
for i in 0..3 {
for j in 0..6 {
assert_approx_eq!(g[(i, j)], 0.0, 1e-10);
}
}
for i in 3..6 {
assert!(
g[(i, i - 3)].abs() > 0.0,
"g[{}, {}] should be non-zero",
i,
i - 3
);
}
for i in 6..9 {
assert!(
g[(i, i - 3)].abs() > 0.0,
"g[{}, {}] should be non-zero",
i,
i - 3
);
}
}
#[test]
fn test_gps_position_jacobian() {
let state = StrapdownState::default();
let h = gps_position_jacobian(&state);
assert_eq!(h.nrows(), 3);
assert_eq!(h.ncols(), 9);
assert_approx_eq!(h[(0, 0)], 1.0, 1e-10);
assert_approx_eq!(h[(1, 1)], 1.0, 1e-10);
assert_approx_eq!(h[(2, 2)], 1.0, 1e-10);
for i in 0..3 {
for j in 3..9 {
assert_approx_eq!(h[(i, j)], 0.0, 1e-10);
}
}
}
#[test]
fn test_gps_velocity_jacobian() {
let state = StrapdownState::default();
let h = gps_velocity_jacobian(&state);
assert_eq!(h.nrows(), 3);
assert_eq!(h.ncols(), 9);
assert_approx_eq!(h[(0, 3)], 1.0, 1e-10);
assert_approx_eq!(h[(1, 4)], 1.0, 1e-10);
assert_approx_eq!(h[(2, 5)], 1.0, 1e-10);
for i in 0..3 {
for j in 0..3 {
assert_approx_eq!(h[(i, j)], 0.0, 1e-10);
}
}
}
#[test]
fn test_gps_position_velocity_jacobian() {
let state = StrapdownState::default();
let h = gps_position_velocity_jacobian(&state);
assert_eq!(h.nrows(), 5);
assert_eq!(h.ncols(), 9);
assert_approx_eq!(h[(0, 0)], 1.0, 1e-10);
assert_approx_eq!(h[(1, 1)], 1.0, 1e-10);
assert_approx_eq!(h[(2, 2)], 1.0, 1e-10);
assert_approx_eq!(h[(3, 3)], 1.0, 1e-10);
assert_approx_eq!(h[(4, 4)], 1.0, 1e-10);
for i in 0..5 {
assert_approx_eq!(h[(i, 5)], 0.0, 1e-10);
}
}
#[test]
fn test_relative_altitude_jacobian() {
let state = StrapdownState::default();
let h = relative_altitude_jacobian(&state);
assert_eq!(h.nrows(), 1);
assert_eq!(h.ncols(), 9);
assert_approx_eq!(h[(0, 2)], 1.0, 1e-10);
for j in 0..9 {
if j != 2 {
assert_approx_eq!(h[(0, j)], 0.0, 1e-10);
}
}
}
#[test]
fn test_measurement_jacobians_consistency() {
let state = StrapdownState::default();
let h_pos = gps_position_jacobian(&state);
let h_vel = gps_velocity_jacobian(&state);
let h_combined = gps_position_velocity_jacobian(&state);
for i in 0..3 {
for j in 0..9 {
assert_approx_eq!(h_combined[(i, j)], h_pos[(i, j)], 1e-10);
}
}
for i in 0..2 {
for j in 0..9 {
assert_approx_eq!(h_combined[(3 + i, j)], h_vel[(i, j)], 1e-10);
}
}
}
#[test]
fn test_apply_eskf_correction_position() {
let mut state = StrapdownState::new(
45.0,
-122.0,
100.0,
0.0,
0.0,
0.0,
Rotation3::identity(),
true,
Some(true),
);
let initial_lat = state.latitude;
let initial_lon = state.longitude;
let initial_alt = state.altitude;
let delta_x = DVector::from_vec(vec![
0.0001, 0.0002, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ]);
apply_eskf_correction(&mut state, &delta_x);
assert_approx_eq!(state.latitude, initial_lat + 0.0001, 1e-10);
assert_approx_eq!(state.longitude, initial_lon + 0.0002, 1e-10);
assert_approx_eq!(state.altitude, initial_alt + 5.0, 1e-10);
}
#[test]
fn test_apply_eskf_correction_velocity() {
let mut state = StrapdownState::new(
45.0,
-122.0,
100.0,
10.0,
5.0,
-1.0,
Rotation3::identity(),
true,
Some(true),
);
let delta_x = DVector::from_vec(vec![
0.0, 0.0, 0.0, 0.5, -0.3, 0.1, 0.0, 0.0, 0.0, ]);
apply_eskf_correction(&mut state, &delta_x);
assert_approx_eq!(state.velocity_north, 10.5, 1e-10);
assert_approx_eq!(state.velocity_east, 4.7, 1e-10);
assert_approx_eq!(state.velocity_vertical, -0.9, 1e-10);
}
#[test]
fn test_apply_eskf_correction_attitude() {
let mut state = StrapdownState::new(
45.0,
-122.0,
100.0,
0.0,
0.0,
0.0,
Rotation3::identity(),
true,
Some(true),
);
let delta_roll = 0.01; let delta_pitch = 0.02;
let delta_yaw = 0.03;
let delta_x = DVector::from_vec(vec![
0.0,
0.0,
0.0, 0.0,
0.0,
0.0, delta_roll,
delta_pitch,
delta_yaw,
]);
apply_eskf_correction(&mut state, &delta_x);
let (roll, pitch, yaw) = state.attitude.euler_angles();
assert_approx_eq!(roll, delta_roll, 1e-6);
assert_approx_eq!(pitch, delta_pitch, 1e-6);
assert_approx_eq!(yaw, delta_yaw, 1e-6);
}
#[test]
fn test_apply_eskf_correction_with_biases() {
let mut state = StrapdownState::default();
let delta_x = DVector::from_vec(vec![
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01, 0.02, 0.03, 0.001, 0.002, 0.003, ]);
let biases = apply_eskf_correction_with_biases(&mut state, &delta_x);
assert!(biases.is_some());
let (accel_bias, gyro_bias) = biases.unwrap();
assert_approx_eq!(accel_bias[0], 0.01, 1e-10);
assert_approx_eq!(accel_bias[1], 0.02, 1e-10);
assert_approx_eq!(accel_bias[2], 0.03, 1e-10);
assert_approx_eq!(gyro_bias[0], 0.001, 1e-10);
assert_approx_eq!(gyro_bias[1], 0.002, 1e-10);
assert_approx_eq!(gyro_bias[2], 0.003, 1e-10);
}
#[test]
fn test_apply_eskf_correction_with_biases_returns_none_for_9_state() {
let mut state = StrapdownState::default();
let delta_x = DVector::from_vec(vec![
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ]);
let biases = apply_eskf_correction_with_biases(&mut state, &delta_x);
assert!(biases.is_none());
}
#[test]
fn test_assemble_error_state() {
let dr = Vector3::new(0.0001, 0.0002, 5.0);
let mu = DVector::from_vec(vec![
0.1, 0.2, 0.3, 0.01, 0.02, 0.03, 0.001, 0.002, 0.003, 0.0001, 0.0002, 0.0003, ]);
let delta_x = assemble_error_state(&dr, &mu);
assert_eq!(delta_x.len(), 15);
assert_approx_eq!(delta_x[0], 0.0001, 1e-10);
assert_approx_eq!(delta_x[1], 0.0002, 1e-10);
assert_approx_eq!(delta_x[2], 5.0, 1e-10);
assert_approx_eq!(delta_x[3], 0.1, 1e-10);
assert_approx_eq!(delta_x[4], 0.2, 1e-10);
assert_approx_eq!(delta_x[5], 0.3, 1e-10);
assert_approx_eq!(delta_x[6], 0.01, 1e-10);
assert_approx_eq!(delta_x[7], 0.02, 1e-10);
assert_approx_eq!(delta_x[8], 0.03, 1e-10);
assert_approx_eq!(delta_x[9], 0.001, 1e-10);
assert_approx_eq!(delta_x[10], 0.002, 1e-10);
assert_approx_eq!(delta_x[11], 0.003, 1e-10);
assert_approx_eq!(delta_x[12], 0.0001, 1e-10);
assert_approx_eq!(delta_x[13], 0.0002, 1e-10);
assert_approx_eq!(delta_x[14], 0.0003, 1e-10);
}
}