use crate::astro::math::mat3::{mul_vec3, Mat3};
use crate::astro::math::vec3::{scale3, sub3};
use super::{invalid_input, validate_finite, validate_mat3, validate_vec3, InertialError};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImuSample {
pub t_j2000_s: f64,
pub kind: ImuSampleKind,
}
impl ImuSample {
pub const fn rate(
t_j2000_s: f64,
specific_force_mps2: [f64; 3],
angular_rate_rps: [f64; 3],
) -> Self {
Self {
t_j2000_s,
kind: ImuSampleKind::Rate {
specific_force_mps2,
angular_rate_rps,
},
}
}
pub const fn increment(
t_j2000_s: f64,
delta_velocity_mps: [f64; 3],
delta_theta_rad: [f64; 3],
dt_s: f64,
) -> Self {
Self {
t_j2000_s,
kind: ImuSampleKind::Increment {
delta_velocity_mps,
delta_theta_rad,
dt_s,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ImuSampleKind {
Rate {
specific_force_mps2: [f64; 3],
angular_rate_rps: [f64; 3],
},
Increment {
delta_velocity_mps: [f64; 3],
delta_theta_rad: [f64; 3],
dt_s: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImuBias {
pub accel_mps2: [f64; 3],
pub gyro_rps: [f64; 3],
}
impl Default for ImuBias {
fn default() -> Self {
Self {
accel_mps2: [0.0; 3],
gyro_rps: [0.0; 3],
}
}
}
impl ImuBias {
pub fn validate(&self) -> Result<(), InertialError> {
validate_vec3(self.accel_mps2, "accel_mps2")?;
validate_vec3(self.gyro_rps, "gyro_rps")
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImuCalibration {
pub accel_scale_misalignment: Mat3,
pub gyro_scale_misalignment: Mat3,
}
impl Default for ImuCalibration {
fn default() -> Self {
Self {
accel_scale_misalignment: [[0.0; 3]; 3],
gyro_scale_misalignment: [[0.0; 3]; 3],
}
}
}
impl ImuCalibration {
pub fn from_scale_ppm(
accel_scale_ppm: [f64; 3],
gyro_scale_ppm: [f64; 3],
) -> Result<Self, InertialError> {
validate_vec3(accel_scale_ppm, "accel_scale_ppm")?;
validate_vec3(gyro_scale_ppm, "gyro_scale_ppm")?;
let mut accel = [[0.0; 3]; 3];
let mut gyro = [[0.0; 3]; 3];
for i in 0..3 {
accel[i][i] = accel_scale_ppm[i] * 1.0e-6;
gyro[i][i] = gyro_scale_ppm[i] * 1.0e-6;
}
Ok(Self {
accel_scale_misalignment: accel,
gyro_scale_misalignment: gyro,
})
}
pub fn validate(&self) -> Result<(), InertialError> {
validate_mat3(&self.accel_scale_misalignment, "accel_scale_misalignment")?;
validate_mat3(&self.gyro_scale_misalignment, "gyro_scale_misalignment")?;
inverse3(&identity_plus(&self.accel_scale_misalignment))?;
inverse3(&identity_plus(&self.gyro_scale_misalignment))?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct ImuErrorModel {
pub bias: ImuBias,
pub calibration: ImuCalibration,
}
impl ImuErrorModel {
pub fn correct_sample(
&self,
sample: &ImuSample,
previous_t_j2000_s: f64,
) -> Result<CorrectedImuIncrement, InertialError> {
validate_finite(previous_t_j2000_s, "previous_t_j2000_s")?;
validate_finite(sample.t_j2000_s, "t_j2000_s")?;
self.bias.validate()?;
self.calibration.validate()?;
let epoch_dt_s = sample.t_j2000_s - previous_t_j2000_s;
if epoch_dt_s <= 0.0 {
return Err(InertialError::NonMonotonicSample);
}
match sample.kind {
ImuSampleKind::Rate {
specific_force_mps2,
angular_rate_rps,
} => {
validate_vec3(specific_force_mps2, "specific_force_mps2")?;
validate_vec3(angular_rate_rps, "angular_rate_rps")?;
let f_corr = correct_rate_vector(
specific_force_mps2,
self.bias.accel_mps2,
&self.calibration.accel_scale_misalignment,
)?;
let omega_corr = correct_rate_vector(
angular_rate_rps,
self.bias.gyro_rps,
&self.calibration.gyro_scale_misalignment,
)?;
Ok(CorrectedImuIncrement {
t_j2000_s: sample.t_j2000_s,
delta_velocity_mps: scale3(f_corr, epoch_dt_s),
delta_theta_rad: scale3(omega_corr, epoch_dt_s),
dt_s: epoch_dt_s,
})
}
ImuSampleKind::Increment {
delta_velocity_mps,
delta_theta_rad,
dt_s,
} => {
validate_vec3(delta_velocity_mps, "delta_velocity_mps")?;
validate_vec3(delta_theta_rad, "delta_theta_rad")?;
validate_finite(dt_s, "dt_s")?;
if dt_s <= 0.0 {
return Err(invalid_input("dt_s", "must be positive"));
}
let tolerance = 1.0e-12_f64.max(1.0e-12 * dt_s.abs());
if (epoch_dt_s - dt_s).abs() > tolerance {
return Err(invalid_input("dt_s", "must match sample time step"));
}
let f_meas = scale3(delta_velocity_mps, 1.0 / dt_s);
let omega_meas = scale3(delta_theta_rad, 1.0 / dt_s);
let f_corr = correct_rate_vector(
f_meas,
self.bias.accel_mps2,
&self.calibration.accel_scale_misalignment,
)?;
let omega_corr = correct_rate_vector(
omega_meas,
self.bias.gyro_rps,
&self.calibration.gyro_scale_misalignment,
)?;
Ok(CorrectedImuIncrement {
t_j2000_s: sample.t_j2000_s,
delta_velocity_mps: scale3(f_corr, dt_s),
delta_theta_rad: scale3(omega_corr, dt_s),
dt_s,
})
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CorrectedImuIncrement {
pub t_j2000_s: f64,
pub delta_velocity_mps: [f64; 3],
pub delta_theta_rad: [f64; 3],
pub dt_s: f64,
}
fn correct_rate_vector(
measured: [f64; 3],
bias: [f64; 3],
scale_misalignment: &Mat3,
) -> Result<[f64; 3], InertialError> {
let inverse = inverse3(&identity_plus(scale_misalignment))?;
Ok(mul_vec3(&inverse, sub3(measured, bias)))
}
pub(crate) fn apply_calibration_error_vector(
value: [f64; 3],
scale_misalignment: &Mat3,
) -> [f64; 3] {
mul_vec3(&identity_plus(scale_misalignment), value)
}
fn identity_plus(m: &Mat3) -> Mat3 {
[
[1.0 + m[0][0], m[0][1], m[0][2]],
[m[1][0], 1.0 + m[1][1], m[1][2]],
[m[2][0], m[2][1], 1.0 + m[2][2]],
]
}
fn inverse3(m: &Mat3) -> Result<Mat3, InertialError> {
let c00 = m[1][1] * m[2][2] - m[1][2] * m[2][1];
let c01 = -(m[1][0] * m[2][2] - m[1][2] * m[2][0]);
let c02 = m[1][0] * m[2][1] - m[1][1] * m[2][0];
let c10 = -(m[0][1] * m[2][2] - m[0][2] * m[2][1]);
let c11 = m[0][0] * m[2][2] - m[0][2] * m[2][0];
let c12 = -(m[0][0] * m[2][1] - m[0][1] * m[2][0]);
let c20 = m[0][1] * m[1][2] - m[0][2] * m[1][1];
let c21 = -(m[0][0] * m[1][2] - m[0][2] * m[1][0]);
let c22 = m[0][0] * m[1][1] - m[0][1] * m[1][0];
let det = m[0][0] * c00 + m[0][1] * c01 + m[0][2] * c02;
if !det.is_finite() || det == 0.0 {
return Err(InertialError::SingularCalibration);
}
let inv_det = 1.0 / det;
Ok([
[c00 * inv_det, c10 * inv_det, c20 * inv_det],
[c01 * inv_det, c11 * inv_det, c21 * inv_det],
[c02 * inv_det, c12 * inv_det, c22 * inv_det],
])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn increment_correction_applies_bias_and_scale_exact_formula() {
let calibration = ImuCalibration::from_scale_ppm([100.0, -50.0, 25.0], [10.0, -20.0, 40.0])
.expect("calibration");
let model = ImuErrorModel {
bias: ImuBias {
accel_mps2: [0.1, -0.2, 0.05],
gyro_rps: [0.01, -0.02, 0.03],
},
calibration,
};
let sample = ImuSample::increment(11.0, [1.1, -2.4, 0.7], [0.11, -0.24, 0.37], 1.0);
let corrected = model.correct_sample(&sample, 10.0).expect("corrected");
let expected_accel_x = (1.1_f64 - 0.1) / (1.0 + 100.0e-6);
let expected_accel_y = (-2.4_f64 + 0.2) / (1.0 - 50.0e-6);
let expected_accel_z = (0.7_f64 - 0.05) / (1.0 + 25.0e-6);
assert_eq!(
corrected.delta_velocity_mps[0].to_bits(),
expected_accel_x.to_bits()
);
assert!(
(corrected.delta_velocity_mps[1] - expected_accel_y).abs() <= 5.0e-16,
"actual {:.17e}, expected {:.17e}",
corrected.delta_velocity_mps[1],
expected_accel_y
);
assert_eq!(
corrected.delta_velocity_mps[2].to_bits(),
expected_accel_z.to_bits()
);
let expected_gyro_x = (0.11_f64 - 0.01) / (1.0 + 10.0e-6);
let expected_gyro_y = (-0.24_f64 + 0.02) / (1.0 - 20.0e-6);
let expected_gyro_z = (0.37_f64 - 0.03) / (1.0 + 40.0e-6);
assert_eq!(
corrected.delta_theta_rad[0].to_bits(),
expected_gyro_x.to_bits()
);
assert!(
(corrected.delta_theta_rad[1] - expected_gyro_y).abs() <= 1.0e-16,
"actual {:.17e}, expected {:.17e}",
corrected.delta_theta_rad[1],
expected_gyro_y
);
assert!(
(corrected.delta_theta_rad[2] - expected_gyro_z).abs() <= 1.0e-16,
"actual {:.17e}, expected {:.17e}",
corrected.delta_theta_rad[2],
expected_gyro_z
);
}
#[test]
fn singular_calibration_is_flagged() {
let calibration = ImuCalibration {
accel_scale_misalignment: [[-1.0, 0.0, 0.0], [0.0; 3], [0.0; 3]],
gyro_scale_misalignment: [[0.0; 3]; 3],
};
assert_eq!(
calibration.validate(),
Err(InertialError::SingularCalibration)
);
}
}