1use crate::astro::constants::earth::{GM_EARTH_M3_S2, OMEGA_E_DOT_RAD_S};
4use crate::astro::math::mat3::{inline_tr, mul_vec3, Mat3};
5use crate::astro::math::vec3::norm3;
6use crate::inertial::config::RANDOM_WALK_BIAS_TAU_S;
7use crate::inertial::state::{mat3_identity, skew, validate_dcm_orthonormal};
8use crate::inertial::{validate_vec3, ImuSpec, NavState};
9
10use super::state::{
11 identity, invalid_input, matmul, matrix_add, reproject_covariance_psd, symmetrize_in_place,
12 validate_covariance_matrix, validate_nonnegative, validate_positive, validate_square_matrix,
13 ErrorStateLayout, FusionError, ERROR_ACCEL_BIAS_INDEX, ERROR_ACCEL_SCALE_INDEX,
14 ERROR_ATTITUDE_INDEX, ERROR_GYRO_BIAS_INDEX, ERROR_GYRO_SCALE_INDEX, ERROR_POSITION_INDEX,
15 ERROR_VELOCITY_INDEX,
16};
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct ErrorStateImuKinematics {
21 pub specific_force_body_mps2: [f64; 3],
23 pub angular_rate_body_rps: [f64; 3],
25}
26
27impl ErrorStateImuKinematics {
28 pub fn new(
30 specific_force_body_mps2: [f64; 3],
31 angular_rate_body_rps: [f64; 3],
32 ) -> Result<Self, FusionError> {
33 validate_vec3(specific_force_body_mps2, "specific_force_body_mps2")
34 .map_err(FusionError::from)?;
35 validate_vec3(angular_rate_body_rps, "angular_rate_body_rps").map_err(FusionError::from)?;
36 Ok(Self {
37 specific_force_body_mps2,
38 angular_rate_body_rps,
39 })
40 }
41}
42
43#[derive(Debug, Clone, PartialEq)]
45pub struct ErrorStateLinearization {
46 pub f: Vec<Vec<f64>>,
48 pub phi: Vec<Vec<f64>>,
50 pub q_d: Vec<Vec<f64>>,
52 pub dt_s: f64,
54 pub specific_force_ecef_mps2: [f64; 3],
56}
57
58pub fn error_state_system_matrix_ecef(
60 state: &NavState,
61 kinematics: ErrorStateImuKinematics,
62 imu_spec: &ImuSpec,
63 layout: ErrorStateLayout,
64) -> Result<Vec<Vec<f64>>, FusionError> {
65 error_state_system_matrix_ecef_with_imu_to_body(
66 state,
67 kinematics,
68 imu_spec,
69 layout,
70 mat3_identity(),
71 )
72}
73
74pub fn error_state_system_matrix_ecef_with_imu_to_body(
76 state: &NavState,
77 kinematics: ErrorStateImuKinematics,
78 imu_spec: &ImuSpec,
79 layout: ErrorStateLayout,
80 imu_to_body_dcm: Mat3,
81) -> Result<Vec<Vec<f64>>, FusionError> {
82 state.validate()?;
83 imu_spec.validate()?;
84 validate_dcm_orthonormal(&imu_to_body_dcm, "imu_to_body_dcm").map_err(FusionError::from)?;
85 let dimension = layout.dimension();
86 let mut f = vec![vec![0.0; dimension]; dimension];
87 let c_b_e = state.attitude_body_to_ecef;
88 let c_imu_e = crate::astro::math::mat3::inline_rxr(&c_b_e, &imu_to_body_dcm);
89 let specific_force_ecef = mul_vec3(&c_b_e, kinematics.specific_force_body_mps2);
90 let body_to_imu_dcm = inline_tr(&imu_to_body_dcm);
91 let specific_force_imu = mul_vec3(&body_to_imu_dcm, kinematics.specific_force_body_mps2);
92 let angular_rate_imu = mul_vec3(&body_to_imu_dcm, kinematics.angular_rate_body_rps);
93
94 for axis in 0..3 {
95 f[ERROR_POSITION_INDEX + axis][ERROR_VELOCITY_INDEX + axis] = 1.0;
96 }
97
98 let gravity_gradient = gravity_gradient_prompt_ecef(state.position_ecef_m)?;
99 add_mat3_block(
100 &mut f,
101 ERROR_VELOCITY_INDEX,
102 ERROR_POSITION_INDEX,
103 &gravity_gradient,
104 );
105
106 let omega = skew([0.0, 0.0, OMEGA_E_DOT_RAD_S]);
107 for row in 0..3 {
108 for col in 0..3 {
109 f[ERROR_VELOCITY_INDEX + row][ERROR_VELOCITY_INDEX + col] = -2.0 * omega[row][col];
110 f[ERROR_ATTITUDE_INDEX + row][ERROR_ATTITUDE_INDEX + col] = -omega[row][col];
111 }
112 }
113
114 let specific_force_skew = skew(specific_force_ecef);
115 for row in 0..3 {
116 for col in 0..3 {
117 f[ERROR_VELOCITY_INDEX + row][ERROR_ATTITUDE_INDEX + col] =
118 -specific_force_skew[row][col];
119 f[ERROR_VELOCITY_INDEX + row][ERROR_ACCEL_BIAS_INDEX + col] = c_imu_e[row][col];
120 f[ERROR_ATTITUDE_INDEX + row][ERROR_GYRO_BIAS_INDEX + col] = -c_imu_e[row][col];
121 }
122 }
123
124 fill_bias_decay(&mut f, ERROR_ACCEL_BIAS_INDEX, imu_spec.accel_bias_tau_s);
125 fill_bias_decay(&mut f, ERROR_GYRO_BIAS_INDEX, imu_spec.gyro_bias_tau_s);
126
127 if layout.includes_scale_factors() {
128 for row in 0..3 {
129 for col in 0..3 {
130 f[ERROR_VELOCITY_INDEX + row][ERROR_ACCEL_SCALE_INDEX + col] =
131 c_imu_e[row][col] * specific_force_imu[col];
132 f[ERROR_ATTITUDE_INDEX + row][ERROR_GYRO_SCALE_INDEX + col] =
133 -c_imu_e[row][col] * angular_rate_imu[col];
134 }
135 }
136 }
137
138 Ok(f)
139}
140
141pub fn error_state_transition_matrix(
143 f: &[Vec<f64>],
144 dt_s: f64,
145) -> Result<Vec<Vec<f64>>, FusionError> {
146 validate_nonnegative(dt_s, "dt_s")?;
147 let dimension = f.len();
148 if dimension != ErrorStateLayout::Fifteen.dimension()
149 && dimension != ErrorStateLayout::TwentyOne.dimension()
150 {
151 return Err(invalid_input("f", "dimension must be 15 or 21"));
152 }
153 validate_square_matrix(f, dimension, "f")?;
154
155 let mut fdt = vec![vec![0.0; dimension]; dimension];
156 for row in 0..dimension {
157 for col in 0..dimension {
158 fdt[row][col] = f[row][col] * dt_s;
159 }
160 }
161 let fdt2 = matmul(&fdt, &fdt)?;
162 let mut phi = identity(dimension);
163 for row in 0..dimension {
164 for col in 0..dimension {
165 phi[row][col] += fdt[row][col] + 0.5 * fdt2[row][col];
166 }
167 }
168 Ok(phi)
169}
170
171pub fn error_state_process_noise_discrete(
173 imu_spec: &ImuSpec,
174 dt_s: f64,
175 layout: ErrorStateLayout,
176) -> Result<Vec<Vec<f64>>, FusionError> {
177 imu_spec.validate()?;
178 validate_nonnegative(dt_s, "dt_s")?;
179 let dimension = layout.dimension();
180 let mut q_d = vec![vec![0.0; dimension]; dimension];
181 let q_accel = imu_spec.accel_vrw_mps_sqrt_s * imu_spec.accel_vrw_mps_sqrt_s;
182 let q_gyro = imu_spec.gyro_arw_rad_sqrt_s * imu_spec.gyro_arw_rad_sqrt_s;
183 let dt = dt_s.abs();
184 let dt2 = dt * dt;
185 let dt3 = dt2 * dt;
186
187 for axis in 0..3 {
188 q_d[ERROR_POSITION_INDEX + axis][ERROR_POSITION_INDEX + axis] = q_accel * dt3 / 3.0;
189 q_d[ERROR_POSITION_INDEX + axis][ERROR_VELOCITY_INDEX + axis] = q_accel * dt2 / 2.0;
190 q_d[ERROR_VELOCITY_INDEX + axis][ERROR_POSITION_INDEX + axis] = q_accel * dt2 / 2.0;
191 q_d[ERROR_VELOCITY_INDEX + axis][ERROR_VELOCITY_INDEX + axis] = q_accel * dt;
192 q_d[ERROR_ATTITUDE_INDEX + axis][ERROR_ATTITUDE_INDEX + axis] = q_gyro * dt;
193 q_d[ERROR_ACCEL_BIAS_INDEX + axis][ERROR_ACCEL_BIAS_INDEX + axis] =
194 imu_spec.accel_bias_variance_increment(dt_s)?;
195 q_d[ERROR_GYRO_BIAS_INDEX + axis][ERROR_GYRO_BIAS_INDEX + axis] =
196 imu_spec.gyro_bias_variance_increment(dt_s)?;
197 }
198
199 if layout.includes_scale_factors() {
200 let accel_scale = imu_spec.accel_scale_instab_ppm.unwrap_or(0.0) * 1.0e-6;
201 let gyro_scale = imu_spec.gyro_scale_instab_ppm.unwrap_or(0.0) * 1.0e-6;
202 let accel_scale_q = accel_scale * accel_scale;
203 let gyro_scale_q = gyro_scale * gyro_scale;
204 for axis in 0..3 {
205 q_d[ERROR_ACCEL_SCALE_INDEX + axis][ERROR_ACCEL_SCALE_INDEX + axis] =
206 accel_scale_q * dt;
207 q_d[ERROR_GYRO_SCALE_INDEX + axis][ERROR_GYRO_SCALE_INDEX + axis] = gyro_scale_q * dt;
208 }
209 }
210
211 reproject_covariance_psd(&mut q_d, "process_noise")?;
212 Ok(q_d)
213}
214
215pub fn linearize_error_state_ecef(
217 state: &NavState,
218 kinematics: ErrorStateImuKinematics,
219 imu_spec: &ImuSpec,
220 dt_s: f64,
221 layout: ErrorStateLayout,
222) -> Result<ErrorStateLinearization, FusionError> {
223 linearize_error_state_ecef_with_imu_to_body(
224 state,
225 kinematics,
226 imu_spec,
227 dt_s,
228 layout,
229 mat3_identity(),
230 )
231}
232
233pub fn linearize_error_state_ecef_with_imu_to_body(
235 state: &NavState,
236 kinematics: ErrorStateImuKinematics,
237 imu_spec: &ImuSpec,
238 dt_s: f64,
239 layout: ErrorStateLayout,
240 imu_to_body_dcm: Mat3,
241) -> Result<ErrorStateLinearization, FusionError> {
242 let f = error_state_system_matrix_ecef_with_imu_to_body(
243 state,
244 kinematics,
245 imu_spec,
246 layout,
247 imu_to_body_dcm,
248 )?;
249 let phi = error_state_transition_matrix(&f, dt_s)?;
250 let q_d = error_state_process_noise_discrete(imu_spec, dt_s, layout)?;
251 Ok(ErrorStateLinearization {
252 f,
253 phi,
254 q_d,
255 dt_s,
256 specific_force_ecef_mps2: mul_vec3(
257 &state.attitude_body_to_ecef,
258 kinematics.specific_force_body_mps2,
259 ),
260 })
261}
262
263pub fn predict_error_state_covariance(
265 covariance: &mut Vec<Vec<f64>>,
266 phi: &[Vec<f64>],
267 q_d: &[Vec<f64>],
268) -> Result<(), FusionError> {
269 let dimension = covariance.len();
270 if dimension != ErrorStateLayout::Fifteen.dimension()
271 && dimension != ErrorStateLayout::TwentyOne.dimension()
272 {
273 return Err(invalid_input("covariance", "dimension must be 15 or 21"));
274 }
275 validate_covariance_matrix(covariance, dimension, "covariance")?;
276 validate_square_matrix(phi, dimension, "phi")?;
277 validate_covariance_matrix(q_d, dimension, "q_d")?;
278
279 let mut temp = vec![vec![0.0; dimension]; dimension];
280 for i in 0..dimension {
281 for j in 0..dimension {
282 for k in 0..dimension {
283 temp[i][j] += phi[i][k] * covariance[k][j];
284 }
285 }
286 }
287
288 let mut propagated = vec![vec![0.0; dimension]; dimension];
289 for i in 0..dimension {
290 for j in 0..dimension {
291 for k in 0..dimension {
292 propagated[i][j] += temp[i][k] * phi[j][k];
293 }
294 }
295 }
296 let propagated = matrix_add(&propagated, q_d)?;
297 *covariance = propagated;
298 symmetrize_in_place(covariance);
299 reproject_covariance_psd(covariance, "covariance")
300}
301
302pub(crate) fn gravity_gradient_prompt_ecef(position_ecef_m: [f64; 3]) -> Result<Mat3, FusionError> {
303 validate_vec3(position_ecef_m, "position_ecef_m").map_err(FusionError::from)?;
304 let radius_m = norm3(position_ecef_m);
305 validate_positive(radius_m, "position_radius_m")?;
306 let radius3 = radius_m * radius_m * radius_m;
307 let scale = -GM_EARTH_M3_S2 / radius3;
308 let r_hat = [
309 position_ecef_m[0] / radius_m,
310 position_ecef_m[1] / radius_m,
311 position_ecef_m[2] / radius_m,
312 ];
313 let omega = skew([0.0, 0.0, OMEGA_E_DOT_RAD_S]);
314 let omega2 = [
315 [
316 omega[0][0] * omega[0][0] + omega[0][1] * omega[1][0] + omega[0][2] * omega[2][0],
317 omega[0][0] * omega[0][1] + omega[0][1] * omega[1][1] + omega[0][2] * omega[2][1],
318 omega[0][0] * omega[0][2] + omega[0][1] * omega[1][2] + omega[0][2] * omega[2][2],
319 ],
320 [
321 omega[1][0] * omega[0][0] + omega[1][1] * omega[1][0] + omega[1][2] * omega[2][0],
322 omega[1][0] * omega[0][1] + omega[1][1] * omega[1][1] + omega[1][2] * omega[2][1],
323 omega[1][0] * omega[0][2] + omega[1][1] * omega[1][2] + omega[1][2] * omega[2][2],
324 ],
325 [
326 omega[2][0] * omega[0][0] + omega[2][1] * omega[1][0] + omega[2][2] * omega[2][0],
327 omega[2][0] * omega[0][1] + omega[2][1] * omega[1][1] + omega[2][2] * omega[2][1],
328 omega[2][0] * omega[0][2] + omega[2][1] * omega[1][2] + omega[2][2] * omega[2][2],
329 ],
330 ];
331 let mut gradient = [[0.0; 3]; 3];
332 for row in 0..3 {
333 for col in 0..3 {
334 let identity = if row == col { 1.0 } else { 0.0 };
335 gradient[row][col] =
336 scale * (identity - 3.0 * r_hat[row] * r_hat[col]) - omega2[row][col];
337 }
338 }
339 Ok(gradient)
340}
341
342fn fill_bias_decay(f: &mut [Vec<f64>], index: usize, tau_s: f64) {
343 if tau_s != RANDOM_WALK_BIAS_TAU_S && tau_s.is_finite() {
344 for axis in 0..3 {
345 f[index + axis][index + axis] = -1.0 / tau_s;
346 }
347 }
348}
349
350fn add_mat3_block(matrix: &mut [Vec<f64>], row0: usize, col0: usize, block: &Mat3) {
351 for row in 0..3 {
352 for col in 0..3 {
353 matrix[row0 + row][col0 + col] += block[row][col];
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
370 use crate::astro::constants::earth::WGS84_A_M;
371 use crate::astro::math::mat3::{inline_rxr, inline_tr, mul_vec3};
372 use crate::inertial::mechanization::mechanize_ecef;
373 use crate::inertial::state::{mat3_identity, reorthonormalize_dcm};
374 use crate::inertial::{CorrectedImuIncrement, MechanizationConfig};
375 use nalgebra::DMatrix;
376
377 fn assert_close(actual: f64, expected: f64, tolerance: f64) {
378 assert!(
379 (actual - expected).abs() <= tolerance,
380 "actual {actual:.17e}, expected {expected:.17e}, tolerance {tolerance:.17e}"
381 );
382 }
383
384 fn reference_state() -> NavState {
385 NavState::new(
386 0.0,
387 [WGS84_A_M + 1000.0, 25.0, -40.0],
388 [3.0, -2.0, 1.0],
389 mat3_identity(),
390 )
391 .expect("reference state")
392 }
393
394 fn reference_imu() -> ErrorStateImuKinematics {
395 ErrorStateImuKinematics::new([0.12, -0.05, 9.72], [0.004, -0.002, 0.001])
396 .expect("imu kinematics")
397 }
398
399 fn reference_spec() -> ImuSpec {
400 ImuSpec::datasheet(
401 0.02,
402 0.003,
403 0.004,
404 0.0002,
405 3600.0,
406 7200.0,
407 Some(25.0),
408 Some(30.0),
409 )
410 }
411
412 fn reference_increment(imu: ErrorStateImuKinematics, dt_s: f64) -> CorrectedImuIncrement {
413 CorrectedImuIncrement {
414 t_j2000_s: dt_s,
415 delta_velocity_mps: [
416 imu.specific_force_body_mps2[0] * dt_s,
417 imu.specific_force_body_mps2[1] * dt_s,
418 imu.specific_force_body_mps2[2] * dt_s,
419 ],
420 delta_theta_rad: [
421 imu.angular_rate_body_rps[0] * dt_s,
422 imu.angular_rate_body_rps[1] * dt_s,
423 imu.angular_rate_body_rps[2] * dt_s,
424 ],
425 dt_s,
426 }
427 }
428
429 fn attitude_error_ecef(perturbed: &Mat3, base: &Mat3) -> [f64; 3] {
430 let delta = inline_rxr(perturbed, &inline_tr(base));
431 [
432 0.5 * (delta[1][2] - delta[2][1]),
433 0.5 * (delta[2][0] - delta[0][2]),
434 0.5 * (delta[0][1] - delta[1][0]),
435 ]
436 }
437
438 #[test]
439 fn gravity_gradient_matches_published_ecef_point_mass_plus_centrifugal_bits() {
440 let radius_m = WGS84_A_M + 1000.0;
441 let gradient = gravity_gradient_prompt_ecef([radius_m, 0.0, 0.0]).expect("gradient");
442 let radius3 = radius_m * radius_m * radius_m;
443 let point = GM_EARTH_M3_S2 / radius3;
444 let omega2 = OMEGA_E_DOT_RAD_S * OMEGA_E_DOT_RAD_S;
445
446 assert_eq!(gradient[0][0].to_bits(), (2.0 * point + omega2).to_bits());
447 assert_eq!(gradient[1][1].to_bits(), (-point + omega2).to_bits());
448 assert_eq!(gradient[2][2].to_bits(), (-point).to_bits());
449 for (row, values) in gradient.iter().enumerate() {
450 for (col, value) in values.iter().enumerate() {
451 if row != col {
452 assert_eq!(*value, 0.0);
453 }
454 }
455 }
456 }
457
458 #[test]
459 fn phi_matches_mechanization_finite_difference_for_kinematic_block() {
460 let state = reference_state();
461 let imu = reference_imu();
462 let spec = reference_spec();
463 let dt_s = 1.0e-5;
464 let epsilon = 1.0;
465 let linear =
466 linearize_error_state_ecef(&state, imu, &spec, dt_s, ErrorStateLayout::Fifteen)
467 .expect("linearization");
468 let increment = reference_increment(imu, dt_s);
469 let base_next =
470 mechanize_ecef(&state, &increment, MechanizationConfig::default()).expect("base step");
471
472 for col in 0..6 {
473 let mut perturbed = state;
474 if col < 3 {
475 perturbed.position_ecef_m[col] += epsilon;
476 } else {
477 perturbed.velocity_ecef_mps[col - 3] += epsilon;
478 }
479 let next = mechanize_ecef(&perturbed, &increment, MechanizationConfig::default())
480 .expect("perturbed step");
481 for row in 0..3 {
482 let fd = (next.position_ecef_m[row] - base_next.position_ecef_m[row]) / epsilon;
483 assert_close(fd, linear.phi[row][col], 2.0e-8);
484 }
485 for row in 0..3 {
486 let fd = (next.velocity_ecef_mps[row] - base_next.velocity_ecef_mps[row]) / epsilon;
487 assert_close(fd, linear.phi[ERROR_VELOCITY_INDEX + row][col], 5.0e-10);
488 }
489 }
490 }
491
492 #[test]
493 fn phi_matches_mechanization_finite_difference_for_attitude_error_block() {
494 let state = reference_state();
495 let imu = reference_imu();
496 let spec = reference_spec();
497 let dt_s = 1.0e-4;
498 let epsilon = 1.0e-6;
499 let linear =
500 linearize_error_state_ecef(&state, imu, &spec, dt_s, ErrorStateLayout::Fifteen)
501 .expect("linearization");
502 let increment = reference_increment(imu, dt_s);
503 let base_next =
504 mechanize_ecef(&state, &increment, MechanizationConfig::default()).expect("base step");
505
506 for col in 0..3 {
507 let mut psi = [0.0; 3];
508 psi[col] = epsilon;
509 let psi_skew = skew(psi);
510 let mut correction = mat3_identity();
511 for row in 0..3 {
512 for col in 0..3 {
513 correction[row][col] += psi_skew[row][col];
514 }
515 }
516 let mut perturbed = state;
517 perturbed.attitude_body_to_ecef =
518 reorthonormalize_dcm(&inline_rxr(&correction, &state.attitude_body_to_ecef))
519 .expect("perturbed attitude");
520 let next = mechanize_ecef(&perturbed, &increment, MechanizationConfig::default())
521 .expect("perturbed step");
522 let raw_attitude_error = attitude_error_ecef(
523 &next.attitude_body_to_ecef,
524 &base_next.attitude_body_to_ecef,
525 );
526 let psi_next = [
527 -raw_attitude_error[0],
528 -raw_attitude_error[1],
529 -raw_attitude_error[2],
530 ];
531
532 for (row, value) in psi_next.iter().enumerate() {
533 let fd = *value / epsilon;
534 assert_close(
535 fd,
536 linear.phi[ERROR_ATTITUDE_INDEX + row][ERROR_ATTITUDE_INDEX + col],
537 2.0e-9,
538 );
539 }
540 for row in 0..3 {
541 let fd = (next.velocity_ecef_mps[row] - base_next.velocity_ecef_mps[row]) / epsilon;
542 assert_close(
543 fd,
544 linear.phi[ERROR_VELOCITY_INDEX + row][ERROR_ATTITUDE_INDEX + col],
545 5.0e-7,
546 );
547 }
548 }
549 }
550
551 #[test]
552 fn qd_position_velocity_block_matches_closed_form_bits() {
553 let dt_s = 0.125_f64;
554 let q_accel = 0.02_f64 * 0.02_f64;
555 let spec = ImuSpec::datasheet(
556 0.02,
557 0.0,
558 0.0,
559 0.0,
560 RANDOM_WALK_BIAS_TAU_S,
561 RANDOM_WALK_BIAS_TAU_S,
562 None,
563 None,
564 );
565 let q_d = error_state_process_noise_discrete(&spec, dt_s, ErrorStateLayout::Fifteen)
566 .expect("process noise");
567 let dt = dt_s.abs();
568 let dt2 = dt * dt;
569 let dt3 = dt2 * dt;
570 for axis in 0..3 {
571 assert_eq!(q_d[axis][axis].to_bits(), (q_accel * dt3 / 3.0).to_bits());
572 assert_eq!(
573 q_d[axis][ERROR_VELOCITY_INDEX + axis].to_bits(),
574 (q_accel * dt2 / 2.0).to_bits()
575 );
576 assert_eq!(
577 q_d[ERROR_VELOCITY_INDEX + axis][axis].to_bits(),
578 (q_accel * dt2 / 2.0).to_bits()
579 );
580 assert_eq!(
581 q_d[ERROR_VELOCITY_INDEX + axis][ERROR_VELOCITY_INDEX + axis].to_bits(),
582 (q_accel * dt).to_bits()
583 );
584 }
585 }
586
587 #[test]
588 fn imu_to_body_dcm_rotates_bias_and_scale_jacobians() {
589 let state = reference_state();
590 let imu = ErrorStateImuKinematics::new([2.0, 3.0, 4.0], [0.1, 0.2, 0.3]).expect("imu");
591 let imu_to_body = [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
592 let body_to_imu = inline_tr(&imu_to_body);
593 let specific_force_imu = mul_vec3(&body_to_imu, imu.specific_force_body_mps2);
594 let angular_rate_imu = mul_vec3(&body_to_imu, imu.angular_rate_body_rps);
595 let f = error_state_system_matrix_ecef_with_imu_to_body(
596 &state,
597 imu,
598 &reference_spec(),
599 ErrorStateLayout::TwentyOne,
600 imu_to_body,
601 )
602 .expect("system matrix");
603
604 for row in 0..3 {
605 for col in 0..3 {
606 assert_eq!(
607 f[ERROR_VELOCITY_INDEX + row][ERROR_ACCEL_BIAS_INDEX + col].to_bits(),
608 imu_to_body[row][col].to_bits()
609 );
610 assert_eq!(
611 f[ERROR_ATTITUDE_INDEX + row][ERROR_GYRO_BIAS_INDEX + col].to_bits(),
612 (-imu_to_body[row][col]).to_bits()
613 );
614 assert_eq!(
615 f[ERROR_VELOCITY_INDEX + row][ERROR_ACCEL_SCALE_INDEX + col].to_bits(),
616 (imu_to_body[row][col] * specific_force_imu[col]).to_bits()
617 );
618 assert_eq!(
619 f[ERROR_ATTITUDE_INDEX + row][ERROR_GYRO_SCALE_INDEX + col].to_bits(),
620 (-imu_to_body[row][col] * angular_rate_imu[col]).to_bits()
621 );
622 }
623 }
624 }
625
626 #[test]
627 fn imu_to_body_dcm_matches_identity_mount_in_imu_frame_with_nonidentity_attitude() {
628 let body_to_ecef = [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
629 let imu_to_body = [[0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
630 let imu_to_ecef = inline_rxr(&body_to_ecef, &imu_to_body);
631 let body_state = NavState::new(
632 25.0,
633 [WGS84_A_M + 750.0, -125.0, 80.0],
634 [4.0, -3.0, 2.0],
635 body_to_ecef,
636 )
637 .expect("body-frame state");
638 let imu_frame_state = NavState::new(
639 body_state.t_j2000_s,
640 body_state.position_ecef_m,
641 body_state.velocity_ecef_mps,
642 imu_to_ecef,
643 )
644 .expect("imu-frame state");
645 let body_kinematics = ErrorStateImuKinematics::new([1.5, -0.25, 9.6], [0.03, -0.02, 0.01])
646 .expect("body kinematics");
647 let body_to_imu = inline_tr(&imu_to_body);
648 let imu_frame_kinematics = ErrorStateImuKinematics::new(
649 mul_vec3(&body_to_imu, body_kinematics.specific_force_body_mps2),
650 mul_vec3(&body_to_imu, body_kinematics.angular_rate_body_rps),
651 )
652 .expect("imu-frame kinematics");
653
654 let mounted = error_state_system_matrix_ecef_with_imu_to_body(
655 &body_state,
656 body_kinematics,
657 &reference_spec(),
658 ErrorStateLayout::TwentyOne,
659 imu_to_body,
660 )
661 .expect("mounted system matrix");
662 let identity_in_imu_frame = error_state_system_matrix_ecef(
663 &imu_frame_state,
664 imu_frame_kinematics,
665 &reference_spec(),
666 ErrorStateLayout::TwentyOne,
667 )
668 .expect("identity system matrix");
669
670 for row in 0..mounted.len() {
671 for col in 0..mounted[row].len() {
672 assert_close(mounted[row][col], identity_in_imu_frame[row][col], 1.0e-15);
673 }
674 }
675 }
676
677 #[test]
678 fn propagate_only_logdet_grows_monotonically_with_process_noise() {
679 let state = reference_state();
680 let imu = reference_imu();
681 let spec = ImuSpec::datasheet(0.2, 0.03, 0.02, 0.002, 600.0, 900.0, None, None);
682 let mut covariance = vec![vec![0.0; 15]; 15];
683 for (idx, row) in covariance.iter_mut().enumerate() {
684 row[idx] = 1.0e-3;
685 }
686 let mut previous = logdet(&covariance);
687 for _ in 0..12 {
688 let linear =
689 linearize_error_state_ecef(&state, imu, &spec, 0.02, ErrorStateLayout::Fifteen)
690 .expect("linearization");
691 predict_error_state_covariance(&mut covariance, &linear.phi, &linear.q_d)
692 .expect("predict covariance");
693 let current = logdet(&covariance);
694 assert!(
695 current > previous,
696 "current {current:.17e}, previous {previous:.17e}"
697 );
698 previous = current;
699 }
700 }
701
702 fn logdet(covariance: &[Vec<f64>]) -> f64 {
703 let flat = covariance
704 .iter()
705 .flat_map(|row| row.iter().copied())
706 .collect::<Vec<_>>();
707 let matrix = DMatrix::from_row_slice(covariance.len(), covariance.len(), &flat);
708 let cholesky = matrix.cholesky().expect("test covariance is SPD");
709 2.0 * (0..covariance.len())
710 .map(|idx| cholesky.l()[(idx, idx)].ln())
711 .sum::<f64>()
712 }
713}