use crate::astro::math::mat3::{inline_rxr, inline_tr};
use crate::astro::math::vec3::{add3, scale3, sub3};
use super::config::{gauss_markov_bias_decay, gauss_markov_bias_variance_increment, ImuSpec};
use super::frames::gravity_ecef_mps2;
use super::imu::{
apply_calibration_error_vector, CorrectedImuIncrement, ImuBias, ImuCalibration, ImuSample,
};
use super::mechanization::{
earth_rate_cross, earth_rotation_first_order, mid_interval_dcm, validate_increment,
};
use super::state::{mat3_mul_vec, reorthonormalize_dcm, NavState};
use super::{
invalid_input, validate_finite, validate_nonnegative, validate_positive, InertialError,
};
pub const DEFAULT_IMU_SIM_SEED: u64 = 0x4d59_5df4_d0f3_3173;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ImuSimulationOutput {
Rate,
#[default]
Increment,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImuRateRandomWalk {
pub accel_mps2_sqrt_s: f64,
pub gyro_rps_sqrt_s: f64,
}
impl ImuRateRandomWalk {
pub const fn new(accel_mps2_sqrt_s: f64, gyro_rps_sqrt_s: f64) -> Self {
Self {
accel_mps2_sqrt_s,
gyro_rps_sqrt_s,
}
}
pub fn validate(&self) -> Result<(), InertialError> {
validate_nonnegative(self.accel_mps2_sqrt_s, "accel_mps2_sqrt_s")?;
validate_nonnegative(self.gyro_rps_sqrt_s, "gyro_rps_sqrt_s")
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImuSimulationOptions {
pub output: ImuSimulationOutput,
pub seed: u64,
pub initial_bias: ImuBias,
pub calibration: ImuCalibration,
pub rate_random_walk: Option<ImuRateRandomWalk>,
}
impl Default for ImuSimulationOptions {
fn default() -> Self {
Self {
output: ImuSimulationOutput::Increment,
seed: DEFAULT_IMU_SIM_SEED,
initial_bias: ImuBias::default(),
calibration: ImuCalibration::default(),
rate_random_walk: None,
}
}
}
impl ImuSimulationOptions {
pub fn validate(&self) -> Result<(), InertialError> {
self.initial_bias.validate()?;
self.calibration.validate()?;
if let Some(rate_random_walk) = self.rate_random_walk {
rate_random_walk.validate()?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SimulatedImuSequence {
pub samples: Vec<ImuSample>,
pub bias_history: Vec<ImuBias>,
pub rate_random_walk_history: Vec<ImuBias>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImuSimulator {
spec: ImuSpec,
options: ImuSimulationOptions,
rng: SplitMix64,
bias: ImuBias,
rate_random_walk: ImuBias,
}
impl ImuSimulator {
pub fn new(spec: ImuSpec, options: ImuSimulationOptions) -> Result<Self, InertialError> {
spec.validate()?;
options.validate()?;
Ok(Self {
spec,
options,
rng: SplitMix64::new(options.seed),
bias: options.initial_bias,
rate_random_walk: ImuBias::default(),
})
}
pub const fn bias(&self) -> ImuBias {
self.bias
}
pub const fn rate_random_walk(&self) -> ImuBias {
self.rate_random_walk
}
pub fn sample_increment(
&mut self,
truth: &CorrectedImuIncrement,
) -> Result<ImuSample, InertialError> {
validate_increment(truth)?;
let dt_s = truth.dt_s;
self.bias = propagate_bias(self.bias, &self.spec, dt_s, &mut self.rng)?;
self.rate_random_walk = propagate_rate_random_walk(
self.rate_random_walk,
self.options.rate_random_walk,
dt_s,
&mut self.rng,
)?;
match self.options.output {
ImuSimulationOutput::Rate => Ok(self.rate_sample(truth)),
ImuSimulationOutput::Increment => Ok(self.increment_sample(truth)),
}
}
fn rate_sample(&mut self, truth: &CorrectedImuIncrement) -> ImuSample {
let dt_s = truth.dt_s;
let inv_dt = 1.0 / dt_s;
let accel_noise = noise_vec(self.spec.accel_vrw_mps_sqrt_s / dt_s.sqrt(), &mut self.rng);
let gyro_noise = noise_vec(self.spec.gyro_arw_rad_sqrt_s / dt_s.sqrt(), &mut self.rng);
let accel_rate_truth = scale3(truth.delta_velocity_mps, inv_dt);
let gyro_rate_truth = scale3(truth.delta_theta_rad, inv_dt);
let accel_rate = add_output_errors(
apply_accel_calibration(accel_rate_truth, self.options.calibration),
self.rate_random_walk.accel_mps2,
accel_noise,
self.bias.accel_mps2,
);
let gyro_rate = add_output_errors(
apply_gyro_calibration(gyro_rate_truth, self.options.calibration),
self.rate_random_walk.gyro_rps,
gyro_noise,
self.bias.gyro_rps,
);
ImuSample::rate(truth.t_j2000_s, accel_rate, gyro_rate)
}
fn increment_sample(&mut self, truth: &CorrectedImuIncrement) -> ImuSample {
let dt_s = truth.dt_s;
let accel_noise = noise_vec(self.spec.accel_vrw_mps_sqrt_s * dt_s.sqrt(), &mut self.rng);
let gyro_noise = noise_vec(self.spec.gyro_arw_rad_sqrt_s * dt_s.sqrt(), &mut self.rng);
let delta_velocity = add_output_errors(
apply_accel_calibration(truth.delta_velocity_mps, self.options.calibration),
scale3(self.rate_random_walk.accel_mps2, dt_s),
accel_noise,
scale3(self.bias.accel_mps2, dt_s),
);
let delta_theta = add_output_errors(
apply_gyro_calibration(truth.delta_theta_rad, self.options.calibration),
scale3(self.rate_random_walk.gyro_rps, dt_s),
gyro_noise,
scale3(self.bias.gyro_rps, dt_s),
);
ImuSample::increment(truth.t_j2000_s, delta_velocity, delta_theta, dt_s)
}
}
pub fn simulate_imu_samples(
trajectory: &[NavState],
spec: ImuSpec,
options: ImuSimulationOptions,
) -> Result<SimulatedImuSequence, InertialError> {
if trajectory.len() < 2 {
return Err(invalid_input(
"trajectory",
"must contain at least two states",
));
}
for state in trajectory {
state.validate()?;
}
let mut increments = Vec::with_capacity(trajectory.len() - 1);
for pair in trajectory.windows(2) {
increments.push(true_imu_increment_between(&pair[0], &pair[1])?);
}
simulate_imu_samples_from_increments(&increments, spec, options)
}
pub fn simulate_imu_samples_from_increments(
truth_increments: &[CorrectedImuIncrement],
spec: ImuSpec,
options: ImuSimulationOptions,
) -> Result<SimulatedImuSequence, InertialError> {
validate_increment_sequence(truth_increments)?;
let mut simulator = ImuSimulator::new(spec, options)?;
let mut samples = Vec::with_capacity(truth_increments.len());
let mut bias_history = Vec::with_capacity(truth_increments.len());
let mut rate_random_walk_history = Vec::with_capacity(truth_increments.len());
for truth in truth_increments {
let sample = simulator.sample_increment(truth)?;
samples.push(sample);
bias_history.push(simulator.bias());
rate_random_walk_history.push(simulator.rate_random_walk());
}
Ok(SimulatedImuSequence {
samples,
bias_history,
rate_random_walk_history,
})
}
pub fn true_imu_increment_between(
start: &NavState,
end: &NavState,
) -> Result<CorrectedImuIncrement, InertialError> {
start.validate()?;
end.validate()?;
let dt_s = end.t_j2000_s - start.t_j2000_s;
validate_positive(dt_s, "dt_s")?;
validate_position_consistency(start, end, dt_s)?;
let delta_theta_rad = delta_theta_from_attitude_change(start, end, dt_s)?;
let c_avg = mid_interval_dcm(&start.attitude_body_to_ecef, delta_theta_rad, dt_s)?;
let c_avg_t = inline_tr(&c_avg);
let gravity = gravity_ecef_mps2(start.position_ecef_m)?;
let coriolis = scale3(earth_rate_cross(start.velocity_ecef_mps), -2.0);
let acceleration = add3(gravity, coriolis);
let required_ecef = sub3(
sub3(end.velocity_ecef_mps, start.velocity_ecef_mps),
scale3(acceleration, dt_s),
);
let increment = CorrectedImuIncrement {
t_j2000_s: end.t_j2000_s,
delta_velocity_mps: mat3_mul_vec(&c_avg_t, required_ecef),
delta_theta_rad,
dt_s,
};
validate_increment(&increment)?;
Ok(increment)
}
fn validate_position_consistency(
start: &NavState,
end: &NavState,
dt_s: f64,
) -> Result<(), InertialError> {
let average_velocity = scale3(add3(start.velocity_ecef_mps, end.velocity_ecef_mps), 0.5);
let expected_position = add3(start.position_ecef_m, scale3(average_velocity, dt_s));
for (axis, expected) in expected_position.iter().enumerate() {
let actual = end.position_ecef_m[axis];
let scale = expected.abs().max(actual.abs()).max(1.0);
let tolerance = 64.0 * f64::EPSILON * scale;
if (actual - expected).abs() > tolerance {
return Err(invalid_input(
"end.position_ecef_m",
"must match trapezoidal position implied by endpoint velocities",
));
}
}
Ok(())
}
fn validate_increment_sequence(
truth_increments: &[CorrectedImuIncrement],
) -> Result<(), InertialError> {
let mut previous_end_t: Option<f64> = None;
for increment in truth_increments {
validate_increment(increment)?;
let start_t = increment.t_j2000_s - increment.dt_s;
validate_finite(start_t, "increment.start_t_j2000_s")?;
if let Some(previous) = previous_end_t {
let tolerance = 1.0e-12_f64.max(1.0e-12 * increment.dt_s.abs());
if (start_t - previous).abs() > tolerance {
return Err(invalid_input(
"truth_increments",
"must be contiguous in time",
));
}
}
previous_end_t = Some(increment.t_j2000_s);
}
Ok(())
}
fn propagate_bias(
bias: ImuBias,
spec: &ImuSpec,
dt_s: f64,
rng: &mut SplitMix64,
) -> Result<ImuBias, InertialError> {
Ok(ImuBias {
accel_mps2: [
propagate_gauss_markov_axis(
bias.accel_mps2[0],
spec.accel_bias_instab_mps2,
dt_s,
spec.accel_bias_tau_s,
rng,
)?,
propagate_gauss_markov_axis(
bias.accel_mps2[1],
spec.accel_bias_instab_mps2,
dt_s,
spec.accel_bias_tau_s,
rng,
)?,
propagate_gauss_markov_axis(
bias.accel_mps2[2],
spec.accel_bias_instab_mps2,
dt_s,
spec.accel_bias_tau_s,
rng,
)?,
],
gyro_rps: [
propagate_gauss_markov_axis(
bias.gyro_rps[0],
spec.gyro_bias_instab_rps,
dt_s,
spec.gyro_bias_tau_s,
rng,
)?,
propagate_gauss_markov_axis(
bias.gyro_rps[1],
spec.gyro_bias_instab_rps,
dt_s,
spec.gyro_bias_tau_s,
rng,
)?,
propagate_gauss_markov_axis(
bias.gyro_rps[2],
spec.gyro_bias_instab_rps,
dt_s,
spec.gyro_bias_tau_s,
rng,
)?,
],
})
}
fn propagate_gauss_markov_axis(
previous: f64,
instability: f64,
dt_s: f64,
tau_s: f64,
rng: &mut SplitMix64,
) -> Result<f64, InertialError> {
let phi = gauss_markov_bias_decay(dt_s, tau_s)?;
let variance = gauss_markov_bias_variance_increment(instability, dt_s, tau_s)?;
if variance == 0.0 {
Ok(phi * previous)
} else {
Ok(phi * previous + variance.sqrt() * rng.standard_normal())
}
}
fn propagate_rate_random_walk(
current: ImuBias,
model: Option<ImuRateRandomWalk>,
dt_s: f64,
rng: &mut SplitMix64,
) -> Result<ImuBias, InertialError> {
let Some(model) = model else {
return Ok(current);
};
model.validate()?;
let sqrt_dt = dt_s.sqrt();
Ok(ImuBias {
accel_mps2: [
current.accel_mps2[0] + noise_scalar(model.accel_mps2_sqrt_s * sqrt_dt, rng),
current.accel_mps2[1] + noise_scalar(model.accel_mps2_sqrt_s * sqrt_dt, rng),
current.accel_mps2[2] + noise_scalar(model.accel_mps2_sqrt_s * sqrt_dt, rng),
],
gyro_rps: [
current.gyro_rps[0] + noise_scalar(model.gyro_rps_sqrt_s * sqrt_dt, rng),
current.gyro_rps[1] + noise_scalar(model.gyro_rps_sqrt_s * sqrt_dt, rng),
current.gyro_rps[2] + noise_scalar(model.gyro_rps_sqrt_s * sqrt_dt, rng),
],
})
}
fn noise_vec(std: f64, rng: &mut SplitMix64) -> [f64; 3] {
if std == 0.0 {
[0.0; 3]
} else {
[
noise_scalar(std, rng),
noise_scalar(std, rng),
noise_scalar(std, rng),
]
}
}
fn noise_scalar(std: f64, rng: &mut SplitMix64) -> f64 {
if std == 0.0 {
0.0
} else {
std * rng.standard_normal()
}
}
fn add_output_errors(
calibrated_truth: [f64; 3],
rate_random_walk: [f64; 3],
white_noise: [f64; 3],
bias: [f64; 3],
) -> [f64; 3] {
let mut value = calibrated_truth;
if !is_zero3(rate_random_walk) {
value = add3(value, rate_random_walk);
}
if !is_zero3(white_noise) {
value = add3(value, white_noise);
}
if !is_zero3(bias) {
value = add3(value, bias);
}
value
}
fn is_zero3(value: [f64; 3]) -> bool {
value
.iter()
.all(|component| component.to_bits() == 0.0_f64.to_bits())
}
fn apply_accel_calibration(value: [f64; 3], calibration: ImuCalibration) -> [f64; 3] {
if calibration.accel_scale_misalignment == [[0.0; 3]; 3] {
value
} else {
apply_calibration_error_vector(value, &calibration.accel_scale_misalignment)
}
}
fn apply_gyro_calibration(value: [f64; 3], calibration: ImuCalibration) -> [f64; 3] {
if calibration.gyro_scale_misalignment == [[0.0; 3]; 3] {
value
} else {
apply_calibration_error_vector(value, &calibration.gyro_scale_misalignment)
}
}
fn delta_theta_from_attitude_change(
start: &NavState,
end: &NavState,
dt_s: f64,
) -> Result<[f64; 3], InertialError> {
let earth_inv = earth_rotation_first_order_inverse(dt_s)?;
let previous_t = inline_tr(&start.attitude_body_to_ecef);
let relative_raw = inline_rxr(
&inline_rxr(&previous_t, &earth_inv),
&end.attitude_body_to_ecef,
);
let relative = reorthonormalize_dcm(&relative_raw)?;
rotation_vector_from_dcm(&relative)
}
fn earth_rotation_first_order_inverse(dt_s: f64) -> Result<[[f64; 3]; 3], InertialError> {
let earth = earth_rotation_first_order(dt_s);
let a = earth[0][1];
let denominator = 1.0 + a * a;
validate_positive(denominator, "earth_rotation_denominator")?;
Ok([
[1.0 / denominator, -a / denominator, 0.0],
[a / denominator, 1.0 / denominator, 0.0],
[0.0, 0.0, 1.0],
])
}
fn rotation_vector_from_dcm(dcm: &[[f64; 3]; 3]) -> Result<[f64; 3], InertialError> {
let trace = dcm[0][0] + dcm[1][1] + dcm[2][2];
let cos_angle = bounded_rotation_cosine(0.5 * (trace - 1.0))?;
let angle = cos_angle.acos();
let skew_part = [
dcm[2][1] - dcm[1][2],
dcm[0][2] - dcm[2][0],
dcm[1][0] - dcm[0][1],
];
if angle < 1.0e-8 {
Ok(scale3(skew_part, 0.5))
} else if core::f64::consts::PI - angle < 1.0e-6 {
rotation_vector_near_pi(dcm, angle)
} else {
Ok(scale3(skew_part, angle / (2.0 * angle.sin())))
}
}
fn bounded_rotation_cosine(value: f64) -> Result<f64, InertialError> {
validate_finite(value, "rotation_cosine")?;
let tolerance = 16.0 * f64::EPSILON;
if value > 1.0 {
if value - 1.0 <= tolerance {
Ok(1.0)
} else {
Err(InertialError::DegenerateAttitude)
}
} else if value < -1.0 {
if -1.0 - value <= tolerance {
Ok(-1.0)
} else {
Err(InertialError::DegenerateAttitude)
}
} else {
Ok(value)
}
}
fn rotation_vector_near_pi(dcm: &[[f64; 3]; 3], angle: f64) -> Result<[f64; 3], InertialError> {
let x2 = nonnegative_roundoff(0.5 * (dcm[0][0] + 1.0))?;
let y2 = nonnegative_roundoff(0.5 * (dcm[1][1] + 1.0))?;
let z2 = nonnegative_roundoff(0.5 * (dcm[2][2] + 1.0))?;
let axis = if x2 >= y2 && x2 >= z2 {
let x = x2.sqrt();
if x == 0.0 {
return Err(InertialError::DegenerateAttitude);
}
[
x,
(dcm[0][1] + dcm[1][0]) / (4.0 * x),
(dcm[0][2] + dcm[2][0]) / (4.0 * x),
]
} else if y2 >= z2 {
let y = y2.sqrt();
if y == 0.0 {
return Err(InertialError::DegenerateAttitude);
}
[
(dcm[0][1] + dcm[1][0]) / (4.0 * y),
y,
(dcm[1][2] + dcm[2][1]) / (4.0 * y),
]
} else {
let z = z2.sqrt();
if z == 0.0 {
return Err(InertialError::DegenerateAttitude);
}
[
(dcm[0][2] + dcm[2][0]) / (4.0 * z),
(dcm[1][2] + dcm[2][1]) / (4.0 * z),
z,
]
};
Ok(scale3(axis, angle))
}
fn nonnegative_roundoff(value: f64) -> Result<f64, InertialError> {
validate_finite(value, "rotation_axis_component")?;
let tolerance = 16.0 * f64::EPSILON;
if value >= 0.0 {
Ok(value)
} else if value.abs() <= tolerance {
Ok(0.0)
} else {
Err(InertialError::DegenerateAttitude)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct SplitMix64 {
state: u64,
spare_normal: Option<f64>,
}
impl SplitMix64 {
fn new(seed: u64) -> Self {
Self {
state: seed,
spare_normal: None,
}
}
fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
fn unit_f64(&mut self) -> f64 {
let bits = 0x3ff0_0000_0000_0000 | (self.next_u64() >> 12);
f64::from_bits(bits) - 1.0
}
fn standard_normal(&mut self) -> f64 {
if let Some(value) = self.spare_normal.take() {
return value;
}
loop {
let u = 2.0 * self.unit_f64() - 1.0;
let v = 2.0 * self.unit_f64() - 1.0;
let s = u * u + v * v;
if s > 0.0 && s < 1.0 {
let scale = (-2.0 * s.ln() / s).sqrt();
self.spare_normal = Some(v * scale);
return u * scale;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::constants::earth::WGS84_A_M;
use crate::clock_stability::{overlapping_adev, AllanSeries};
use crate::inertial::config::RANDOM_WALK_BIAS_TAU_S;
use crate::inertial::mechanization::mechanize_ecef;
use crate::inertial::state::mat3_identity;
use crate::inertial::{
ImuBias, ImuErrorModel, ImuSampleKind, MechanizationConfig, StrapdownMechanizer,
};
fn zero_truth_increments(count: usize, dt_s: f64) -> Vec<CorrectedImuIncrement> {
(1..=count)
.map(|idx| CorrectedImuIncrement {
t_j2000_s: idx as f64 * dt_s,
delta_velocity_mps: [0.0; 3],
delta_theta_rad: [0.0; 3],
dt_s,
})
.collect()
}
fn bit_hash_samples(samples: &[ImuSample]) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for sample in samples {
hash_bits(&mut hash, sample.t_j2000_s.to_bits());
match sample.kind {
ImuSampleKind::Rate {
specific_force_mps2,
angular_rate_rps,
} => {
hash_bits(&mut hash, 0);
for value in specific_force_mps2 {
hash_bits(&mut hash, value.to_bits());
}
for value in angular_rate_rps {
hash_bits(&mut hash, value.to_bits());
}
}
ImuSampleKind::Increment {
delta_velocity_mps,
delta_theta_rad,
dt_s,
} => {
hash_bits(&mut hash, 1);
for value in delta_velocity_mps {
hash_bits(&mut hash, value.to_bits());
}
for value in delta_theta_rad {
hash_bits(&mut hash, value.to_bits());
}
hash_bits(&mut hash, dt_s.to_bits());
}
}
}
hash
}
fn hash_bits(hash: &mut u64, bits: u64) {
*hash ^= bits;
*hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
fn autocorrelation_lag1(values: &[f64], burn_in: usize) -> f64 {
let retained = &values[burn_in..];
let mean = retained.iter().sum::<f64>() / retained.len() as f64;
let mut numerator = 0.0;
let mut denominator = 0.0;
for pair in retained.windows(2) {
numerator += (pair[0] - mean) * (pair[1] - mean);
denominator += (pair[0] - mean) * (pair[0] - mean);
}
numerator / denominator
}
fn collect_bias_axis(dt_s: f64, count: usize) -> Vec<f64> {
let spec = ImuSpec::datasheet(0.0, 0.0, 0.2, 0.0, 0.75, 0.75, None, None);
let mut simulator = ImuSimulator::new(
spec,
ImuSimulationOptions {
output: ImuSimulationOutput::Rate,
seed: 0x0123_4567_89ab_cdef,
..ImuSimulationOptions::default()
},
)
.expect("simulator");
let truth = CorrectedImuIncrement {
t_j2000_s: dt_s,
delta_velocity_mps: [0.0; 3],
delta_theta_rad: [0.0; 3],
dt_s,
};
let mut values = Vec::with_capacity(count);
for idx in 1..=count {
let mut step = truth;
step.t_j2000_s = idx as f64 * dt_s;
simulator.sample_increment(&step).expect("sample");
values.push(simulator.bias().accel_mps2[0]);
}
values
}
#[test]
fn gauss_markov_discretization_tracks_closed_form_at_two_rates() {
let count = 450_000;
let burn_in = 2_000;
for dt_s in [0.25_f64, 0.5_f64] {
let values = collect_bias_axis(dt_s, count);
let rho = autocorrelation_lag1(&values, burn_in);
let expected = (-dt_s / 0.75_f64).exp();
assert!(
(rho - expected).abs() <= 1.2e-3,
"dt {dt_s}, rho {rho:.17e}, expected {expected:.17e}"
);
let phi = gauss_markov_bias_decay(dt_s, 0.75).expect("phi");
assert_eq!(phi.to_bits(), expected.to_bits());
}
}
#[test]
fn white_noise_density_matches_allan_oracle() {
let dt_s = 0.02;
let count = 1_000_000;
let accel_density = 0.032;
let gyro_density = 0.0045;
let spec = ImuSpec::datasheet(accel_density, gyro_density, 0.0, 0.0, 1.0, 1.0, None, None);
let options = ImuSimulationOptions {
output: ImuSimulationOutput::Rate,
seed: 0xfeed_cafe_1357_2468,
..ImuSimulationOptions::default()
};
let mut simulator = ImuSimulator::new(spec, options).expect("simulator");
let mut accel = Vec::with_capacity(count);
let mut gyro = Vec::with_capacity(count);
for idx in 1..=count {
let truth = CorrectedImuIncrement {
t_j2000_s: idx as f64 * dt_s,
delta_velocity_mps: [0.0; 3],
delta_theta_rad: [0.0; 3],
dt_s,
};
let sample = simulator.sample_increment(&truth).expect("sample");
match sample.kind {
ImuSampleKind::Rate {
specific_force_mps2,
angular_rate_rps,
} => {
accel.push(specific_force_mps2[0]);
gyro.push(angular_rate_rps[0]);
}
ImuSampleKind::Increment { .. } => unreachable!(),
}
}
let m = [1usize];
let accel_adev =
overlapping_adev(AllanSeries::FractionalFrequency(&accel), dt_s, &m).expect("accel");
let gyro_adev =
overlapping_adev(AllanSeries::FractionalFrequency(&gyro), dt_s, &m).expect("gyro");
let accel_recovered = accel_adev.deviation[0] * dt_s.sqrt();
let gyro_recovered = gyro_adev.deviation[0] * dt_s.sqrt();
assert!(
(accel_recovered - accel_density).abs() <= 7.0e-5,
"accel {accel_recovered:.17e}, expected {accel_density:.17e}"
);
assert!(
(gyro_recovered - gyro_density).abs() <= 1.1e-5,
"gyro {gyro_recovered:.17e}, expected {gyro_density:.17e}"
);
}
#[test]
fn zero_noise_increments_round_trip_through_mechanization_bits() {
let initial = NavState::new(
0.0,
[WGS84_A_M, 0.0, 0.0],
[0.0, 7.0, 0.25],
mat3_identity(),
)
.expect("initial");
let truth = [
CorrectedImuIncrement {
t_j2000_s: 0.125,
delta_velocity_mps: [0.001, -0.002, 0.0005],
delta_theta_rad: [0.0002, -0.0003, 0.0004],
dt_s: 0.125,
},
CorrectedImuIncrement {
t_j2000_s: 0.25,
delta_velocity_mps: [-0.0007, 0.0015, -0.0002],
delta_theta_rad: [-0.0001, 0.00025, -0.00035],
dt_s: 0.125,
},
];
let mut expected = Vec::new();
let mut state = initial;
for increment in truth {
state =
mechanize_ecef(&state, &increment, MechanizationConfig::default()).expect("truth");
expected.push(state);
}
let generated = simulate_imu_samples_from_increments(
&truth,
ImuSpec::datasheet(0.0, 0.0, 0.0, 0.0, 1.0, 1.0, None, None),
ImuSimulationOptions::default(),
)
.expect("generated");
let mut mechanizer = StrapdownMechanizer::new(initial).expect("mechanizer");
for (sample, expected_state) in generated.samples.into_iter().zip(expected) {
let actual = *mechanizer.propagate(sample).expect("propagated");
assert_eq!(actual, expected_state);
}
}
#[test]
fn trajectory_position_must_match_mechanization_position_update() {
let start = NavState::new(
10.0,
[WGS84_A_M, 0.0, 0.0],
[1.0, 0.0, 0.0],
mat3_identity(),
)
.expect("start");
let end = NavState::new(
11.0,
[WGS84_A_M + 2.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
mat3_identity(),
)
.expect("end");
assert_eq!(
true_imu_increment_between(&start, &end),
Err(InertialError::InvalidInput {
field: "end.position_ecef_m",
reason: "must match trapezoidal position implied by endpoint velocities",
})
);
}
#[test]
fn scale_misalignment_is_applied_before_correction() {
let calibration = ImuCalibration {
accel_scale_misalignment: [[100.0e-6, 20.0e-6, 0.0], [0.0, -50.0e-6, 0.0], [0.0; 3]],
gyro_scale_misalignment: [[10.0e-6, 0.0, 0.0], [30.0e-6, -20.0e-6, 0.0], [0.0; 3]],
};
let truth = [CorrectedImuIncrement {
t_j2000_s: 2.0,
delta_velocity_mps: [1.0, -2.0, 0.5],
delta_theta_rad: [0.1, -0.2, 0.05],
dt_s: 2.0,
}];
let generated = simulate_imu_samples_from_increments(
&truth,
ImuSpec::datasheet(0.0, 0.0, 0.0, 0.0, 1.0, 1.0, None, None),
ImuSimulationOptions {
calibration,
..ImuSimulationOptions::default()
},
)
.expect("generated");
match generated.samples[0].kind {
ImuSampleKind::Increment {
delta_velocity_mps,
delta_theta_rad,
..
} => {
assert_eq!(
delta_velocity_mps[0].to_bits(),
(1.0_f64 * (1.0 + 100.0e-6) + -2.0 * 20.0e-6).to_bits()
);
assert_eq!(
delta_velocity_mps[1].to_bits(),
(-2.0_f64 * (1.0 - 50.0e-6)).to_bits()
);
assert_eq!(
delta_theta_rad[0].to_bits(),
(0.1_f64 * (1.0 + 10.0e-6)).to_bits()
);
assert_eq!(
delta_theta_rad[1].to_bits(),
(0.1_f64 * 30.0e-6 + -0.2 * (1.0 - 20.0e-6)).to_bits()
);
}
ImuSampleKind::Rate { .. } => unreachable!(),
}
}
#[test]
fn stochastic_output_errors_are_not_scale_misaligned() {
let calibration = ImuCalibration {
accel_scale_misalignment: [
[200.0e-6, -40.0e-6, 15.0e-6],
[25.0e-6, -80.0e-6, 0.0],
[0.0; 3],
],
gyro_scale_misalignment: [
[-30.0e-6, 12.0e-6, 0.0],
[50.0e-6, 90.0e-6, -8.0e-6],
[0.0; 3],
],
};
let truth = [CorrectedImuIncrement {
t_j2000_s: 8.0,
delta_velocity_mps: [1.25, -0.75, 0.2],
delta_theta_rad: [0.03, -0.015, 0.004],
dt_s: 0.5,
}];
let spec = ImuSpec::datasheet(
0.04,
0.005,
0.0,
0.0,
RANDOM_WALK_BIAS_TAU_S,
RANDOM_WALK_BIAS_TAU_S,
None,
None,
);
let options = ImuSimulationOptions {
seed: 0x77aa_19f0_cafe_babe,
rate_random_walk: Some(ImuRateRandomWalk::new(0.002, 0.0003)),
..ImuSimulationOptions::default()
};
let base = simulate_imu_samples_from_increments(&truth, spec, options).expect("base");
let calibrated = simulate_imu_samples_from_increments(
&truth,
spec,
ImuSimulationOptions {
calibration,
..options
},
)
.expect("calibrated");
let (
ImuSampleKind::Increment {
delta_velocity_mps: base_delta_velocity,
delta_theta_rad: base_delta_theta,
..
},
ImuSampleKind::Increment {
delta_velocity_mps: calibrated_delta_velocity,
delta_theta_rad: calibrated_delta_theta,
..
},
) = (base.samples[0].kind, calibrated.samples[0].kind)
else {
unreachable!()
};
let expected_accel_delta = [
200.0e-6 * truth[0].delta_velocity_mps[0]
+ -40.0e-6 * truth[0].delta_velocity_mps[1]
+ 15.0e-6 * truth[0].delta_velocity_mps[2],
25.0e-6 * truth[0].delta_velocity_mps[0] + -80.0e-6 * truth[0].delta_velocity_mps[1],
0.0,
];
let expected_gyro_delta = [
-30.0e-6 * truth[0].delta_theta_rad[0] + 12.0e-6 * truth[0].delta_theta_rad[1],
50.0e-6 * truth[0].delta_theta_rad[0]
+ 90.0e-6 * truth[0].delta_theta_rad[1]
+ -8.0e-6 * truth[0].delta_theta_rad[2],
0.0,
];
for axis in 0..3 {
assert!(
(calibrated_delta_velocity[axis]
- base_delta_velocity[axis]
- expected_accel_delta[axis])
.abs()
<= 3.0e-16,
"axis {axis} accel actual {:.17e}, expected {:.17e}",
calibrated_delta_velocity[axis] - base_delta_velocity[axis],
expected_accel_delta[axis]
);
assert!(
(calibrated_delta_theta[axis] - base_delta_theta[axis] - expected_gyro_delta[axis])
.abs()
<= 3.0e-17,
"axis {axis} gyro actual {:.17e}, expected {:.17e}",
calibrated_delta_theta[axis] - base_delta_theta[axis],
expected_gyro_delta[axis]
);
}
}
#[test]
fn calibration_and_bias_are_inverse_of_existing_correction_model() {
let calibration = ImuCalibration {
accel_scale_misalignment: [[100.0e-6, 0.0, 0.0], [0.0; 3], [0.0; 3]],
gyro_scale_misalignment: [[50.0e-6, 0.0, 0.0], [0.0; 3], [0.0; 3]],
};
let bias = ImuBias {
accel_mps2: [0.1, 0.0, 0.0],
gyro_rps: [0.01, 0.0, 0.0],
};
let truth = [CorrectedImuIncrement {
t_j2000_s: 2.0,
delta_velocity_mps: [1.0, -2.0, 0.5],
delta_theta_rad: [0.1, -0.2, 0.05],
dt_s: 2.0,
}];
let generated = simulate_imu_samples_from_increments(
&truth,
ImuSpec::datasheet(
0.0,
0.0,
0.0,
0.0,
RANDOM_WALK_BIAS_TAU_S,
RANDOM_WALK_BIAS_TAU_S,
None,
None,
),
ImuSimulationOptions {
initial_bias: bias,
calibration,
..ImuSimulationOptions::default()
},
)
.expect("generated");
let model = ImuErrorModel { bias, calibration };
let corrected = model
.correct_sample(&generated.samples[0], 0.0)
.expect("corrected");
for axis in 0..3 {
assert!(
(corrected.delta_velocity_mps[axis] - truth[0].delta_velocity_mps[axis]).abs()
<= 2.0e-15,
"axis {axis} accel actual {:.17e}, expected {:.17e}",
corrected.delta_velocity_mps[axis],
truth[0].delta_velocity_mps[axis]
);
assert!(
(corrected.delta_theta_rad[axis] - truth[0].delta_theta_rad[axis]).abs() <= 2.0e-16,
"axis {axis} gyro actual {:.17e}, expected {:.17e}",
corrected.delta_theta_rad[axis],
truth[0].delta_theta_rad[axis]
);
}
}
#[test]
fn seeded_generation_has_exact_golden_hash() {
let truth = zero_truth_increments(32, 0.01);
let spec = ImuSpec::datasheet(0.04, 0.005, 0.003, 0.0002, 12.0, 18.0, None, None);
let options = ImuSimulationOptions {
output: ImuSimulationOutput::Increment,
seed: 0x1111_2222_3333_4444,
rate_random_walk: Some(ImuRateRandomWalk::new(0.0003, 0.00004)),
..ImuSimulationOptions::default()
};
let first =
simulate_imu_samples_from_increments(&truth, spec, options).expect("first sequence");
let second =
simulate_imu_samples_from_increments(&truth, spec, options).expect("second sequence");
assert_eq!(first.samples, second.samples);
assert_eq!(bit_hash_samples(&first.samples), 0xb4bb_aa05_b75e_5097);
}
}