1use crate::astro::math::mat3::{mul_vec3, Mat3};
4use crate::astro::math::vec3::{scale3, sub3};
5
6use super::{invalid_input, validate_finite, validate_mat3, validate_vec3, InertialError};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct ImuSample {
11 pub t_j2000_s: f64,
13 pub kind: ImuSampleKind,
15}
16
17impl ImuSample {
18 pub const fn rate(
20 t_j2000_s: f64,
21 specific_force_mps2: [f64; 3],
22 angular_rate_rps: [f64; 3],
23 ) -> Self {
24 Self {
25 t_j2000_s,
26 kind: ImuSampleKind::Rate {
27 specific_force_mps2,
28 angular_rate_rps,
29 },
30 }
31 }
32
33 pub const fn increment(
35 t_j2000_s: f64,
36 delta_velocity_mps: [f64; 3],
37 delta_theta_rad: [f64; 3],
38 dt_s: f64,
39 ) -> Self {
40 Self {
41 t_j2000_s,
42 kind: ImuSampleKind::Increment {
43 delta_velocity_mps,
44 delta_theta_rad,
45 dt_s,
46 },
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq)]
53pub enum ImuSampleKind {
54 Rate {
57 specific_force_mps2: [f64; 3],
59 angular_rate_rps: [f64; 3],
61 },
62 Increment {
64 delta_velocity_mps: [f64; 3],
66 delta_theta_rad: [f64; 3],
68 dt_s: f64,
70 },
71}
72
73#[derive(Debug, Clone, Copy, PartialEq)]
75pub struct ImuBias {
76 pub accel_mps2: [f64; 3],
78 pub gyro_rps: [f64; 3],
80}
81
82impl Default for ImuBias {
83 fn default() -> Self {
84 Self {
85 accel_mps2: [0.0; 3],
86 gyro_rps: [0.0; 3],
87 }
88 }
89}
90
91impl ImuBias {
92 pub fn validate(&self) -> Result<(), InertialError> {
94 validate_vec3(self.accel_mps2, "accel_mps2")?;
95 validate_vec3(self.gyro_rps, "gyro_rps")
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq)]
103pub struct ImuCalibration {
104 pub accel_scale_misalignment: Mat3,
106 pub gyro_scale_misalignment: Mat3,
108}
109
110impl Default for ImuCalibration {
111 fn default() -> Self {
112 Self {
113 accel_scale_misalignment: [[0.0; 3]; 3],
114 gyro_scale_misalignment: [[0.0; 3]; 3],
115 }
116 }
117}
118
119impl ImuCalibration {
120 pub fn from_scale_ppm(
122 accel_scale_ppm: [f64; 3],
123 gyro_scale_ppm: [f64; 3],
124 ) -> Result<Self, InertialError> {
125 validate_vec3(accel_scale_ppm, "accel_scale_ppm")?;
126 validate_vec3(gyro_scale_ppm, "gyro_scale_ppm")?;
127 let mut accel = [[0.0; 3]; 3];
128 let mut gyro = [[0.0; 3]; 3];
129 for i in 0..3 {
130 accel[i][i] = accel_scale_ppm[i] * 1.0e-6;
131 gyro[i][i] = gyro_scale_ppm[i] * 1.0e-6;
132 }
133 Ok(Self {
134 accel_scale_misalignment: accel,
135 gyro_scale_misalignment: gyro,
136 })
137 }
138
139 pub fn validate(&self) -> Result<(), InertialError> {
141 validate_mat3(&self.accel_scale_misalignment, "accel_scale_misalignment")?;
142 validate_mat3(&self.gyro_scale_misalignment, "gyro_scale_misalignment")?;
143 inverse3(&identity_plus(&self.accel_scale_misalignment))?;
144 inverse3(&identity_plus(&self.gyro_scale_misalignment))?;
145 Ok(())
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Default)]
151pub struct ImuErrorModel {
152 pub bias: ImuBias,
154 pub calibration: ImuCalibration,
156}
157
158impl ImuErrorModel {
159 pub fn correct_sample(
161 &self,
162 sample: &ImuSample,
163 previous_t_j2000_s: f64,
164 ) -> Result<CorrectedImuIncrement, InertialError> {
165 validate_finite(previous_t_j2000_s, "previous_t_j2000_s")?;
166 validate_finite(sample.t_j2000_s, "t_j2000_s")?;
167 self.bias.validate()?;
168 self.calibration.validate()?;
169 let epoch_dt_s = sample.t_j2000_s - previous_t_j2000_s;
170 if epoch_dt_s <= 0.0 {
171 return Err(InertialError::NonMonotonicSample);
172 }
173
174 match sample.kind {
175 ImuSampleKind::Rate {
176 specific_force_mps2,
177 angular_rate_rps,
178 } => {
179 validate_vec3(specific_force_mps2, "specific_force_mps2")?;
180 validate_vec3(angular_rate_rps, "angular_rate_rps")?;
181 let f_corr = correct_rate_vector(
182 specific_force_mps2,
183 self.bias.accel_mps2,
184 &self.calibration.accel_scale_misalignment,
185 )?;
186 let omega_corr = correct_rate_vector(
187 angular_rate_rps,
188 self.bias.gyro_rps,
189 &self.calibration.gyro_scale_misalignment,
190 )?;
191 Ok(CorrectedImuIncrement {
192 t_j2000_s: sample.t_j2000_s,
193 delta_velocity_mps: scale3(f_corr, epoch_dt_s),
194 delta_theta_rad: scale3(omega_corr, epoch_dt_s),
195 dt_s: epoch_dt_s,
196 })
197 }
198 ImuSampleKind::Increment {
199 delta_velocity_mps,
200 delta_theta_rad,
201 dt_s,
202 } => {
203 validate_vec3(delta_velocity_mps, "delta_velocity_mps")?;
204 validate_vec3(delta_theta_rad, "delta_theta_rad")?;
205 validate_finite(dt_s, "dt_s")?;
206 if dt_s <= 0.0 {
207 return Err(invalid_input("dt_s", "must be positive"));
208 }
209 let tolerance = 1.0e-12_f64.max(1.0e-12 * dt_s.abs());
210 if (epoch_dt_s - dt_s).abs() > tolerance {
211 return Err(invalid_input("dt_s", "must match sample time step"));
212 }
213 let f_meas = scale3(delta_velocity_mps, 1.0 / dt_s);
214 let omega_meas = scale3(delta_theta_rad, 1.0 / dt_s);
215 let f_corr = correct_rate_vector(
216 f_meas,
217 self.bias.accel_mps2,
218 &self.calibration.accel_scale_misalignment,
219 )?;
220 let omega_corr = correct_rate_vector(
221 omega_meas,
222 self.bias.gyro_rps,
223 &self.calibration.gyro_scale_misalignment,
224 )?;
225 Ok(CorrectedImuIncrement {
226 t_j2000_s: sample.t_j2000_s,
227 delta_velocity_mps: scale3(f_corr, dt_s),
228 delta_theta_rad: scale3(omega_corr, dt_s),
229 dt_s,
230 })
231 }
232 }
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq)]
238pub struct CorrectedImuIncrement {
239 pub t_j2000_s: f64,
241 pub delta_velocity_mps: [f64; 3],
243 pub delta_theta_rad: [f64; 3],
245 pub dt_s: f64,
247}
248
249fn correct_rate_vector(
250 measured: [f64; 3],
251 bias: [f64; 3],
252 scale_misalignment: &Mat3,
253) -> Result<[f64; 3], InertialError> {
254 let inverse = inverse3(&identity_plus(scale_misalignment))?;
255 Ok(mul_vec3(&inverse, sub3(measured, bias)))
256}
257
258pub(crate) fn apply_calibration_error_vector(
259 value: [f64; 3],
260 scale_misalignment: &Mat3,
261) -> [f64; 3] {
262 mul_vec3(&identity_plus(scale_misalignment), value)
263}
264
265fn identity_plus(m: &Mat3) -> Mat3 {
266 [
267 [1.0 + m[0][0], m[0][1], m[0][2]],
268 [m[1][0], 1.0 + m[1][1], m[1][2]],
269 [m[2][0], m[2][1], 1.0 + m[2][2]],
270 ]
271}
272
273fn inverse3(m: &Mat3) -> Result<Mat3, InertialError> {
274 let c00 = m[1][1] * m[2][2] - m[1][2] * m[2][1];
275 let c01 = -(m[1][0] * m[2][2] - m[1][2] * m[2][0]);
276 let c02 = m[1][0] * m[2][1] - m[1][1] * m[2][0];
277 let c10 = -(m[0][1] * m[2][2] - m[0][2] * m[2][1]);
278 let c11 = m[0][0] * m[2][2] - m[0][2] * m[2][0];
279 let c12 = -(m[0][0] * m[2][1] - m[0][1] * m[2][0]);
280 let c20 = m[0][1] * m[1][2] - m[0][2] * m[1][1];
281 let c21 = -(m[0][0] * m[1][2] - m[0][2] * m[1][0]);
282 let c22 = m[0][0] * m[1][1] - m[0][1] * m[1][0];
283 let det = m[0][0] * c00 + m[0][1] * c01 + m[0][2] * c02;
284 if !det.is_finite() || det == 0.0 {
285 return Err(InertialError::SingularCalibration);
286 }
287 let inv_det = 1.0 / det;
288 Ok([
289 [c00 * inv_det, c10 * inv_det, c20 * inv_det],
290 [c01 * inv_det, c11 * inv_det, c21 * inv_det],
291 [c02 * inv_det, c12 * inv_det, c22 * inv_det],
292 ])
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
303
304 #[test]
305 fn increment_correction_applies_bias_and_scale_exact_formula() {
306 let calibration = ImuCalibration::from_scale_ppm([100.0, -50.0, 25.0], [10.0, -20.0, 40.0])
307 .expect("calibration");
308 let model = ImuErrorModel {
309 bias: ImuBias {
310 accel_mps2: [0.1, -0.2, 0.05],
311 gyro_rps: [0.01, -0.02, 0.03],
312 },
313 calibration,
314 };
315 let sample = ImuSample::increment(11.0, [1.1, -2.4, 0.7], [0.11, -0.24, 0.37], 1.0);
316 let corrected = model.correct_sample(&sample, 10.0).expect("corrected");
317
318 let expected_accel_x = (1.1_f64 - 0.1) / (1.0 + 100.0e-6);
319 let expected_accel_y = (-2.4_f64 + 0.2) / (1.0 - 50.0e-6);
320 let expected_accel_z = (0.7_f64 - 0.05) / (1.0 + 25.0e-6);
321 assert_eq!(
322 corrected.delta_velocity_mps[0].to_bits(),
323 expected_accel_x.to_bits()
324 );
325 assert!(
326 (corrected.delta_velocity_mps[1] - expected_accel_y).abs() <= 5.0e-16,
327 "actual {:.17e}, expected {:.17e}",
328 corrected.delta_velocity_mps[1],
329 expected_accel_y
330 );
331 assert_eq!(
332 corrected.delta_velocity_mps[2].to_bits(),
333 expected_accel_z.to_bits()
334 );
335
336 let expected_gyro_x = (0.11_f64 - 0.01) / (1.0 + 10.0e-6);
337 let expected_gyro_y = (-0.24_f64 + 0.02) / (1.0 - 20.0e-6);
338 let expected_gyro_z = (0.37_f64 - 0.03) / (1.0 + 40.0e-6);
339 assert_eq!(
340 corrected.delta_theta_rad[0].to_bits(),
341 expected_gyro_x.to_bits()
342 );
343 assert!(
344 (corrected.delta_theta_rad[1] - expected_gyro_y).abs() <= 1.0e-16,
345 "actual {:.17e}, expected {:.17e}",
346 corrected.delta_theta_rad[1],
347 expected_gyro_y
348 );
349 assert!(
350 (corrected.delta_theta_rad[2] - expected_gyro_z).abs() <= 1.0e-16,
351 "actual {:.17e}, expected {:.17e}",
352 corrected.delta_theta_rad[2],
353 expected_gyro_z
354 );
355 }
356
357 #[test]
358 fn singular_calibration_is_flagged() {
359 let calibration = ImuCalibration {
360 accel_scale_misalignment: [[-1.0, 0.0, 0.0], [0.0; 3], [0.0; 3]],
361 gyro_scale_misalignment: [[0.0; 3]; 3],
362 };
363 assert_eq!(
364 calibration.validate(),
365 Err(InertialError::SingularCalibration)
366 );
367 }
368}