use crate::astro::constants::earth::{GM_EARTH_M3_S2, OMEGA_E_DOT_RAD_S};
use crate::astro::math::mat3::{mul_vec3, Mat3};
use crate::astro::math::vec3::norm3;
use crate::inertial::config::RANDOM_WALK_BIAS_TAU_S;
use crate::inertial::state::skew;
use crate::inertial::{validate_vec3, ImuSpec, NavState};
use super::state::{
identity, invalid_input, matmul, matrix_add, reproject_covariance_psd, symmetrize_in_place,
validate_covariance_matrix, validate_nonnegative, validate_positive, validate_square_matrix,
ErrorStateLayout, FusionError, ERROR_ACCEL_BIAS_INDEX, ERROR_ACCEL_SCALE_INDEX,
ERROR_ATTITUDE_INDEX, ERROR_GYRO_BIAS_INDEX, ERROR_GYRO_SCALE_INDEX, ERROR_POSITION_INDEX,
ERROR_VELOCITY_INDEX,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ErrorStateImuKinematics {
pub specific_force_body_mps2: [f64; 3],
pub angular_rate_body_rps: [f64; 3],
}
impl ErrorStateImuKinematics {
pub fn new(
specific_force_body_mps2: [f64; 3],
angular_rate_body_rps: [f64; 3],
) -> Result<Self, FusionError> {
validate_vec3(specific_force_body_mps2, "specific_force_body_mps2")
.map_err(FusionError::from)?;
validate_vec3(angular_rate_body_rps, "angular_rate_body_rps").map_err(FusionError::from)?;
Ok(Self {
specific_force_body_mps2,
angular_rate_body_rps,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ErrorStateLinearization {
pub f: Vec<Vec<f64>>,
pub phi: Vec<Vec<f64>>,
pub q_d: Vec<Vec<f64>>,
pub dt_s: f64,
pub specific_force_ecef_mps2: [f64; 3],
}
pub fn error_state_system_matrix_ecef(
state: &NavState,
kinematics: ErrorStateImuKinematics,
imu_spec: &ImuSpec,
layout: ErrorStateLayout,
) -> Result<Vec<Vec<f64>>, FusionError> {
state.validate()?;
imu_spec.validate()?;
let dimension = layout.dimension();
let mut f = vec![vec![0.0; dimension]; dimension];
let c_b_e = state.attitude_body_to_ecef;
let specific_force_ecef = mul_vec3(&c_b_e, kinematics.specific_force_body_mps2);
for axis in 0..3 {
f[ERROR_POSITION_INDEX + axis][ERROR_VELOCITY_INDEX + axis] = 1.0;
}
let gravity_gradient = gravity_gradient_prompt_ecef(state.position_ecef_m)?;
add_mat3_block(
&mut f,
ERROR_VELOCITY_INDEX,
ERROR_POSITION_INDEX,
&gravity_gradient,
);
let omega = skew([0.0, 0.0, OMEGA_E_DOT_RAD_S]);
for row in 0..3 {
for col in 0..3 {
f[ERROR_VELOCITY_INDEX + row][ERROR_VELOCITY_INDEX + col] = -2.0 * omega[row][col];
f[ERROR_ATTITUDE_INDEX + row][ERROR_ATTITUDE_INDEX + col] = -omega[row][col];
}
}
let specific_force_skew = skew(specific_force_ecef);
for row in 0..3 {
for col in 0..3 {
f[ERROR_VELOCITY_INDEX + row][ERROR_ATTITUDE_INDEX + col] =
-specific_force_skew[row][col];
f[ERROR_VELOCITY_INDEX + row][ERROR_ACCEL_BIAS_INDEX + col] = c_b_e[row][col];
f[ERROR_ATTITUDE_INDEX + row][ERROR_GYRO_BIAS_INDEX + col] = -c_b_e[row][col];
}
}
fill_bias_decay(&mut f, ERROR_ACCEL_BIAS_INDEX, imu_spec.accel_bias_tau_s);
fill_bias_decay(&mut f, ERROR_GYRO_BIAS_INDEX, imu_spec.gyro_bias_tau_s);
if layout.includes_scale_factors() {
for row in 0..3 {
for col in 0..3 {
f[ERROR_VELOCITY_INDEX + row][ERROR_ACCEL_SCALE_INDEX + col] =
c_b_e[row][col] * kinematics.specific_force_body_mps2[col];
f[ERROR_ATTITUDE_INDEX + row][ERROR_GYRO_SCALE_INDEX + col] =
-c_b_e[row][col] * kinematics.angular_rate_body_rps[col];
}
}
}
Ok(f)
}
pub fn error_state_transition_matrix(
f: &[Vec<f64>],
dt_s: f64,
) -> Result<Vec<Vec<f64>>, FusionError> {
validate_nonnegative(dt_s, "dt_s")?;
let dimension = f.len();
if dimension != ErrorStateLayout::Fifteen.dimension()
&& dimension != ErrorStateLayout::TwentyOne.dimension()
{
return Err(invalid_input("f", "dimension must be 15 or 21"));
}
validate_square_matrix(f, dimension, "f")?;
let mut fdt = vec![vec![0.0; dimension]; dimension];
for row in 0..dimension {
for col in 0..dimension {
fdt[row][col] = f[row][col] * dt_s;
}
}
let fdt2 = matmul(&fdt, &fdt)?;
let mut phi = identity(dimension);
for row in 0..dimension {
for col in 0..dimension {
phi[row][col] += fdt[row][col] + 0.5 * fdt2[row][col];
}
}
Ok(phi)
}
pub fn error_state_process_noise_discrete(
imu_spec: &ImuSpec,
dt_s: f64,
layout: ErrorStateLayout,
) -> Result<Vec<Vec<f64>>, FusionError> {
imu_spec.validate()?;
validate_nonnegative(dt_s, "dt_s")?;
let dimension = layout.dimension();
let mut q_d = vec![vec![0.0; dimension]; dimension];
let q_accel = imu_spec.accel_vrw_mps_sqrt_s * imu_spec.accel_vrw_mps_sqrt_s;
let q_gyro = imu_spec.gyro_arw_rad_sqrt_s * imu_spec.gyro_arw_rad_sqrt_s;
let dt = dt_s.abs();
let dt2 = dt * dt;
let dt3 = dt2 * dt;
for axis in 0..3 {
q_d[ERROR_POSITION_INDEX + axis][ERROR_POSITION_INDEX + axis] = q_accel * dt3 / 3.0;
q_d[ERROR_POSITION_INDEX + axis][ERROR_VELOCITY_INDEX + axis] = q_accel * dt2 / 2.0;
q_d[ERROR_VELOCITY_INDEX + axis][ERROR_POSITION_INDEX + axis] = q_accel * dt2 / 2.0;
q_d[ERROR_VELOCITY_INDEX + axis][ERROR_VELOCITY_INDEX + axis] = q_accel * dt;
q_d[ERROR_ATTITUDE_INDEX + axis][ERROR_ATTITUDE_INDEX + axis] = q_gyro * dt;
q_d[ERROR_ACCEL_BIAS_INDEX + axis][ERROR_ACCEL_BIAS_INDEX + axis] =
imu_spec.accel_bias_variance_increment(dt_s)?;
q_d[ERROR_GYRO_BIAS_INDEX + axis][ERROR_GYRO_BIAS_INDEX + axis] =
imu_spec.gyro_bias_variance_increment(dt_s)?;
}
if layout.includes_scale_factors() {
let accel_scale = imu_spec.accel_scale_instab_ppm.unwrap_or(0.0) * 1.0e-6;
let gyro_scale = imu_spec.gyro_scale_instab_ppm.unwrap_or(0.0) * 1.0e-6;
let accel_scale_q = accel_scale * accel_scale;
let gyro_scale_q = gyro_scale * gyro_scale;
for axis in 0..3 {
q_d[ERROR_ACCEL_SCALE_INDEX + axis][ERROR_ACCEL_SCALE_INDEX + axis] =
accel_scale_q * dt;
q_d[ERROR_GYRO_SCALE_INDEX + axis][ERROR_GYRO_SCALE_INDEX + axis] = gyro_scale_q * dt;
}
}
reproject_covariance_psd(&mut q_d, "process_noise")?;
Ok(q_d)
}
pub fn linearize_error_state_ecef(
state: &NavState,
kinematics: ErrorStateImuKinematics,
imu_spec: &ImuSpec,
dt_s: f64,
layout: ErrorStateLayout,
) -> Result<ErrorStateLinearization, FusionError> {
let f = error_state_system_matrix_ecef(state, kinematics, imu_spec, layout)?;
let phi = error_state_transition_matrix(&f, dt_s)?;
let q_d = error_state_process_noise_discrete(imu_spec, dt_s, layout)?;
Ok(ErrorStateLinearization {
f,
phi,
q_d,
dt_s,
specific_force_ecef_mps2: mul_vec3(
&state.attitude_body_to_ecef,
kinematics.specific_force_body_mps2,
),
})
}
pub fn predict_error_state_covariance(
covariance: &mut Vec<Vec<f64>>,
phi: &[Vec<f64>],
q_d: &[Vec<f64>],
) -> Result<(), FusionError> {
let dimension = covariance.len();
if dimension != ErrorStateLayout::Fifteen.dimension()
&& dimension != ErrorStateLayout::TwentyOne.dimension()
{
return Err(invalid_input("covariance", "dimension must be 15 or 21"));
}
validate_covariance_matrix(covariance, dimension, "covariance")?;
validate_square_matrix(phi, dimension, "phi")?;
validate_covariance_matrix(q_d, dimension, "q_d")?;
let mut temp = vec![vec![0.0; dimension]; dimension];
for i in 0..dimension {
for j in 0..dimension {
for k in 0..dimension {
temp[i][j] += phi[i][k] * covariance[k][j];
}
}
}
let mut propagated = vec![vec![0.0; dimension]; dimension];
for i in 0..dimension {
for j in 0..dimension {
for k in 0..dimension {
propagated[i][j] += temp[i][k] * phi[j][k];
}
}
}
let propagated = matrix_add(&propagated, q_d)?;
*covariance = propagated;
symmetrize_in_place(covariance);
reproject_covariance_psd(covariance, "covariance")
}
pub(crate) fn gravity_gradient_prompt_ecef(position_ecef_m: [f64; 3]) -> Result<Mat3, FusionError> {
validate_vec3(position_ecef_m, "position_ecef_m").map_err(FusionError::from)?;
let radius_m = norm3(position_ecef_m);
validate_positive(radius_m, "position_radius_m")?;
let radius3 = radius_m * radius_m * radius_m;
let scale = -GM_EARTH_M3_S2 / radius3;
let r_hat = [
position_ecef_m[0] / radius_m,
position_ecef_m[1] / radius_m,
position_ecef_m[2] / radius_m,
];
let omega = skew([0.0, 0.0, OMEGA_E_DOT_RAD_S]);
let omega2 = [
[
omega[0][0] * omega[0][0] + omega[0][1] * omega[1][0] + omega[0][2] * omega[2][0],
omega[0][0] * omega[0][1] + omega[0][1] * omega[1][1] + omega[0][2] * omega[2][1],
omega[0][0] * omega[0][2] + omega[0][1] * omega[1][2] + omega[0][2] * omega[2][2],
],
[
omega[1][0] * omega[0][0] + omega[1][1] * omega[1][0] + omega[1][2] * omega[2][0],
omega[1][0] * omega[0][1] + omega[1][1] * omega[1][1] + omega[1][2] * omega[2][1],
omega[1][0] * omega[0][2] + omega[1][1] * omega[1][2] + omega[1][2] * omega[2][2],
],
[
omega[2][0] * omega[0][0] + omega[2][1] * omega[1][0] + omega[2][2] * omega[2][0],
omega[2][0] * omega[0][1] + omega[2][1] * omega[1][1] + omega[2][2] * omega[2][1],
omega[2][0] * omega[0][2] + omega[2][1] * omega[1][2] + omega[2][2] * omega[2][2],
],
];
let mut gradient = [[0.0; 3]; 3];
for row in 0..3 {
for col in 0..3 {
let identity = if row == col { 1.0 } else { 0.0 };
gradient[row][col] =
scale * (identity - 3.0 * r_hat[row] * r_hat[col]) - omega2[row][col];
}
}
Ok(gradient)
}
fn fill_bias_decay(f: &mut [Vec<f64>], index: usize, tau_s: f64) {
if tau_s != RANDOM_WALK_BIAS_TAU_S && tau_s.is_finite() {
for axis in 0..3 {
f[index + axis][index + axis] = -1.0 / tau_s;
}
}
}
fn add_mat3_block(matrix: &mut [Vec<f64>], row0: usize, col0: usize, block: &Mat3) {
for row in 0..3 {
for col in 0..3 {
matrix[row0 + row][col0 + col] += block[row][col];
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::constants::earth::WGS84_A_M;
use crate::astro::math::mat3::{inline_rxr, inline_tr};
use crate::inertial::mechanization::mechanize_ecef;
use crate::inertial::state::{mat3_identity, reorthonormalize_dcm};
use crate::inertial::{CorrectedImuIncrement, MechanizationConfig};
use nalgebra::DMatrix;
fn assert_close(actual: f64, expected: f64, tolerance: f64) {
assert!(
(actual - expected).abs() <= tolerance,
"actual {actual:.17e}, expected {expected:.17e}, tolerance {tolerance:.17e}"
);
}
fn reference_state() -> NavState {
NavState::new(
0.0,
[WGS84_A_M + 1000.0, 25.0, -40.0],
[3.0, -2.0, 1.0],
mat3_identity(),
)
.expect("reference state")
}
fn reference_imu() -> ErrorStateImuKinematics {
ErrorStateImuKinematics::new([0.12, -0.05, 9.72], [0.004, -0.002, 0.001])
.expect("imu kinematics")
}
fn reference_spec() -> ImuSpec {
ImuSpec::datasheet(
0.02,
0.003,
0.004,
0.0002,
3600.0,
7200.0,
Some(25.0),
Some(30.0),
)
}
fn reference_increment(imu: ErrorStateImuKinematics, dt_s: f64) -> CorrectedImuIncrement {
CorrectedImuIncrement {
t_j2000_s: dt_s,
delta_velocity_mps: [
imu.specific_force_body_mps2[0] * dt_s,
imu.specific_force_body_mps2[1] * dt_s,
imu.specific_force_body_mps2[2] * dt_s,
],
delta_theta_rad: [
imu.angular_rate_body_rps[0] * dt_s,
imu.angular_rate_body_rps[1] * dt_s,
imu.angular_rate_body_rps[2] * dt_s,
],
dt_s,
}
}
fn attitude_error_ecef(perturbed: &Mat3, base: &Mat3) -> [f64; 3] {
let delta = inline_rxr(perturbed, &inline_tr(base));
[
0.5 * (delta[1][2] - delta[2][1]),
0.5 * (delta[2][0] - delta[0][2]),
0.5 * (delta[0][1] - delta[1][0]),
]
}
#[test]
fn gravity_gradient_matches_published_ecef_point_mass_plus_centrifugal_bits() {
let radius_m = WGS84_A_M + 1000.0;
let gradient = gravity_gradient_prompt_ecef([radius_m, 0.0, 0.0]).expect("gradient");
let radius3 = radius_m * radius_m * radius_m;
let point = GM_EARTH_M3_S2 / radius3;
let omega2 = OMEGA_E_DOT_RAD_S * OMEGA_E_DOT_RAD_S;
assert_eq!(gradient[0][0].to_bits(), (2.0 * point + omega2).to_bits());
assert_eq!(gradient[1][1].to_bits(), (-point + omega2).to_bits());
assert_eq!(gradient[2][2].to_bits(), (-point).to_bits());
for (row, values) in gradient.iter().enumerate() {
for (col, value) in values.iter().enumerate() {
if row != col {
assert_eq!(*value, 0.0);
}
}
}
}
#[test]
fn phi_matches_mechanization_finite_difference_for_kinematic_block() {
let state = reference_state();
let imu = reference_imu();
let spec = reference_spec();
let dt_s = 1.0e-5;
let epsilon = 1.0;
let linear =
linearize_error_state_ecef(&state, imu, &spec, dt_s, ErrorStateLayout::Fifteen)
.expect("linearization");
let increment = reference_increment(imu, dt_s);
let base_next =
mechanize_ecef(&state, &increment, MechanizationConfig::default()).expect("base step");
for col in 0..6 {
let mut perturbed = state;
if col < 3 {
perturbed.position_ecef_m[col] += epsilon;
} else {
perturbed.velocity_ecef_mps[col - 3] += epsilon;
}
let next = mechanize_ecef(&perturbed, &increment, MechanizationConfig::default())
.expect("perturbed step");
for row in 0..3 {
let fd = (next.position_ecef_m[row] - base_next.position_ecef_m[row]) / epsilon;
assert_close(fd, linear.phi[row][col], 2.0e-8);
}
for row in 0..3 {
let fd = (next.velocity_ecef_mps[row] - base_next.velocity_ecef_mps[row]) / epsilon;
assert_close(fd, linear.phi[ERROR_VELOCITY_INDEX + row][col], 5.0e-10);
}
}
}
#[test]
fn phi_matches_mechanization_finite_difference_for_attitude_error_block() {
let state = reference_state();
let imu = reference_imu();
let spec = reference_spec();
let dt_s = 1.0e-4;
let epsilon = 1.0e-6;
let linear =
linearize_error_state_ecef(&state, imu, &spec, dt_s, ErrorStateLayout::Fifteen)
.expect("linearization");
let increment = reference_increment(imu, dt_s);
let base_next =
mechanize_ecef(&state, &increment, MechanizationConfig::default()).expect("base step");
for col in 0..3 {
let mut psi = [0.0; 3];
psi[col] = epsilon;
let psi_skew = skew(psi);
let mut correction = mat3_identity();
for row in 0..3 {
for col in 0..3 {
correction[row][col] += psi_skew[row][col];
}
}
let mut perturbed = state;
perturbed.attitude_body_to_ecef =
reorthonormalize_dcm(&inline_rxr(&correction, &state.attitude_body_to_ecef))
.expect("perturbed attitude");
let next = mechanize_ecef(&perturbed, &increment, MechanizationConfig::default())
.expect("perturbed step");
let raw_attitude_error = attitude_error_ecef(
&next.attitude_body_to_ecef,
&base_next.attitude_body_to_ecef,
);
let psi_next = [
-raw_attitude_error[0],
-raw_attitude_error[1],
-raw_attitude_error[2],
];
for (row, value) in psi_next.iter().enumerate() {
let fd = *value / epsilon;
assert_close(
fd,
linear.phi[ERROR_ATTITUDE_INDEX + row][ERROR_ATTITUDE_INDEX + col],
2.0e-9,
);
}
for row in 0..3 {
let fd = (next.velocity_ecef_mps[row] - base_next.velocity_ecef_mps[row]) / epsilon;
assert_close(
fd,
linear.phi[ERROR_VELOCITY_INDEX + row][ERROR_ATTITUDE_INDEX + col],
5.0e-7,
);
}
}
}
#[test]
fn qd_position_velocity_block_matches_closed_form_bits() {
let dt_s = 0.125_f64;
let q_accel = 0.02_f64 * 0.02_f64;
let spec = ImuSpec::datasheet(
0.02,
0.0,
0.0,
0.0,
RANDOM_WALK_BIAS_TAU_S,
RANDOM_WALK_BIAS_TAU_S,
None,
None,
);
let q_d = error_state_process_noise_discrete(&spec, dt_s, ErrorStateLayout::Fifteen)
.expect("process noise");
let dt = dt_s.abs();
let dt2 = dt * dt;
let dt3 = dt2 * dt;
for axis in 0..3 {
assert_eq!(q_d[axis][axis].to_bits(), (q_accel * dt3 / 3.0).to_bits());
assert_eq!(
q_d[axis][ERROR_VELOCITY_INDEX + axis].to_bits(),
(q_accel * dt2 / 2.0).to_bits()
);
assert_eq!(
q_d[ERROR_VELOCITY_INDEX + axis][axis].to_bits(),
(q_accel * dt2 / 2.0).to_bits()
);
assert_eq!(
q_d[ERROR_VELOCITY_INDEX + axis][ERROR_VELOCITY_INDEX + axis].to_bits(),
(q_accel * dt).to_bits()
);
}
}
#[test]
fn propagate_only_logdet_grows_monotonically_with_process_noise() {
let state = reference_state();
let imu = reference_imu();
let spec = ImuSpec::datasheet(0.2, 0.03, 0.02, 0.002, 600.0, 900.0, None, None);
let mut covariance = vec![vec![0.0; 15]; 15];
for (idx, row) in covariance.iter_mut().enumerate() {
row[idx] = 1.0e-3;
}
let mut previous = logdet(&covariance);
for _ in 0..12 {
let linear =
linearize_error_state_ecef(&state, imu, &spec, 0.02, ErrorStateLayout::Fifteen)
.expect("linearization");
predict_error_state_covariance(&mut covariance, &linear.phi, &linear.q_d)
.expect("predict covariance");
let current = logdet(&covariance);
assert!(
current > previous,
"current {current:.17e}, previous {previous:.17e}"
);
previous = current;
}
}
fn logdet(covariance: &[Vec<f64>]) -> f64 {
let flat = covariance
.iter()
.flat_map(|row| row.iter().copied())
.collect::<Vec<_>>();
let matrix = DMatrix::from_row_slice(covariance.len(), covariance.len(), &flat);
let cholesky = matrix.cholesky().expect("test covariance is SPD");
2.0 * (0..covariance.len())
.map(|idx| cholesky.l()[(idx, idx)].ln())
.sum::<f64>()
}
}