1use nalgebra as na;
20
21mod conv;
22
23use conv::{mat3, unvec3, vec3};
24pub use conv::{Mat3, Mat4, Vec3, IDENTITY3, ZERO3, ZERO_VEC3};
27
28pub mod allan;
29pub mod estimate;
30
31#[cfg(feature = "kalibr")]
32pub mod kalibr;
33
34#[derive(Debug, Clone, PartialEq, Default)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct ImuMsg {
38 pub orientation: [f64; 4],
40 pub orientation_covariance: [f64; 9],
42 pub angular_velocity: [f64; 3],
44 pub angular_velocity_covariance: [f64; 9],
46 pub linear_acceleration: [f64; 3],
48 pub linear_acceleration_covariance: [f64; 9],
50}
51
52impl ImuMsg {
53 pub fn new(linear_accel: &[f64; 3], angular_vel: &[f64; 3]) -> Self {
55 Self {
56 linear_acceleration: *linear_accel,
57 angular_velocity: *angular_vel,
58 ..Default::default()
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
67pub enum ImuModel {
68 #[default]
70 Calibrated,
71 ScaleMisalignment,
73 ScaleMisalignmentSizeEffect,
76}
77
78impl ImuModel {
79 pub fn as_str(&self) -> &'static str {
81 match self {
82 ImuModel::Calibrated => "calibrated",
83 ImuModel::ScaleMisalignment => "scale-misalignment",
84 ImuModel::ScaleMisalignmentSizeEffect => "scale-misalignment-size-effect",
85 }
86 }
87
88 pub fn from_str_kalibr(s: &str) -> anyhow::Result<Self> {
90 match s {
91 "calibrated" => Ok(ImuModel::Calibrated),
92 "scale-misalignment" => Ok(ImuModel::ScaleMisalignment),
93 "scale-misalignment-size-effect" => Ok(ImuModel::ScaleMisalignmentSizeEffect),
94 other => Err(anyhow::anyhow!("unknown Kalibr IMU model `{other}`")),
95 }
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq)]
106pub struct ImuNoise {
107 pub accel_noise_density: f64,
109 pub accel_random_walk: f64,
111 pub gyro_noise_density: f64,
113 pub gyro_random_walk: f64,
115 pub update_rate: f64,
117}
118
119impl Default for ImuNoise {
120 fn default() -> Self {
121 Self {
122 accel_noise_density: 0.0,
123 accel_random_walk: 0.0,
124 gyro_noise_density: 0.0,
125 gyro_random_walk: 0.0,
126 update_rate: 200.0,
127 }
128 }
129}
130
131impl ImuNoise {
132 pub fn accel_noise_discrete(&self) -> f64 {
134 self.accel_noise_density * self.update_rate.sqrt()
135 }
136
137 pub fn gyro_noise_discrete(&self) -> f64 {
139 self.gyro_noise_density * self.update_rate.sqrt()
140 }
141
142 pub fn accel_bias_discrete(&self) -> f64 {
144 self.accel_random_walk / self.update_rate.sqrt()
145 }
146
147 pub fn gyro_bias_discrete(&self) -> f64 {
149 self.gyro_random_walk / self.update_rate.sqrt()
150 }
151}
152
153#[derive(Debug, Clone)]
159pub struct ImuIntrinsics {
160 pub model: ImuModel,
162
163 pub accel_m: Mat3,
167
168 pub accel_bias: Vec3,
173
174 pub gyro_m: Mat3,
178
179 pub c_gyro_i: Mat3,
183
184 pub gyro_a: Mat3,
188
189 pub gyro_bias: Vec3,
191
192 pub noise: ImuNoise,
194
195 pub rostopic: Option<String>,
197
198 pub t_i_b: Option<Mat4>,
202
203 pub time_offset: f64,
205
206 pub accel_lever_arms: Option<[Vec3; 3]>,
209}
210
211impl Default for ImuIntrinsics {
212 fn default() -> Self {
213 Self::identity()
214 }
215}
216
217impl ImuIntrinsics {
218 pub fn identity() -> Self {
220 Self {
221 model: ImuModel::Calibrated,
222 accel_m: IDENTITY3,
223 accel_bias: ZERO_VEC3,
224 gyro_m: IDENTITY3,
225 c_gyro_i: IDENTITY3,
226 gyro_a: ZERO3,
227 gyro_bias: ZERO_VEC3,
228 noise: ImuNoise::default(),
229 rostopic: None,
230 t_i_b: None,
231 time_offset: 0.0,
232 accel_lever_arms: None,
233 }
234 }
235
236 pub fn corrector(&self) -> anyhow::Result<ImuCorrector> {
241 ImuCorrector::new(self)
242 }
243
244 pub fn accel_scale(&self) -> [f64; 3] {
246 [self.accel_m[0][0], self.accel_m[1][1], self.accel_m[2][2]]
247 }
248
249 pub fn gyro_scale(&self) -> [f64; 3] {
251 [self.gyro_m[0][0], self.gyro_m[1][1], self.gyro_m[2][2]]
252 }
253}
254
255#[derive(Debug, Clone)]
260pub struct ImuCorrector {
261 accel_inv: na::Matrix3<f64>,
263 gyro_inv: na::Matrix3<f64>,
265 gyro_accel_coupling: na::Matrix3<f64>,
267 accel_bias: na::Vector3<f64>,
268 gyro_bias: na::Vector3<f64>,
269}
270
271impl ImuCorrector {
272 pub fn new(intrinsics: &ImuIntrinsics) -> anyhow::Result<Self> {
274 let accel_inv = mat3(&intrinsics.accel_m)
275 .try_inverse()
276 .ok_or_else(|| anyhow::anyhow!("accelerometer matrix M_a is singular"))?;
277 let gyro_m_inv = mat3(&intrinsics.gyro_m)
278 .try_inverse()
279 .ok_or_else(|| anyhow::anyhow!("gyroscope matrix M_g is singular"))?;
280 let c_gyro_i = mat3(&intrinsics.c_gyro_i);
281
282 Ok(Self {
283 accel_inv,
284 gyro_inv: c_gyro_i.transpose() * gyro_m_inv,
285 gyro_accel_coupling: mat3(&intrinsics.gyro_a) * c_gyro_i,
286 accel_bias: vec3(&intrinsics.accel_bias),
287 gyro_bias: vec3(&intrinsics.gyro_bias),
288 })
289 }
290
291 pub fn identity() -> Self {
293 Self {
294 accel_inv: na::Matrix3::identity(),
295 gyro_inv: na::Matrix3::identity(),
296 gyro_accel_coupling: na::Matrix3::zeros(),
297 accel_bias: na::Vector3::zeros(),
298 gyro_bias: na::Vector3::zeros(),
299 }
300 }
301
302 pub fn correct_accel(&self, raw_accel: [f64; 3]) -> [f64; 3] {
304 let a = self.accel_inv * (na::Vector3::from(raw_accel) - self.accel_bias);
305 unvec3(&a)
306 }
307
308 pub fn correct_gyro(&self, raw_gyro: [f64; 3], corrected_accel: [f64; 3]) -> [f64; 3] {
311 let w_raw = na::Vector3::from(raw_gyro);
312 let a = na::Vector3::from(corrected_accel);
313 let w = self.gyro_inv * (w_raw - self.gyro_bias - self.gyro_accel_coupling * a);
314 unvec3(&w)
315 }
316
317 pub fn correct(&self, raw_accel: [f64; 3], raw_gyro: [f64; 3]) -> ([f64; 3], [f64; 3]) {
319 let a = self.correct_accel(raw_accel);
320 let w = self.correct_gyro(raw_gyro, a);
321 (a, w)
322 }
323
324 pub fn gyro_bias_from_static(
337 &self,
338 mean_raw_accel: [f64; 3],
339 mean_raw_gyro: [f64; 3],
340 ) -> [f64; 3] {
341 let a = na::Vector3::from(self.correct_accel(mean_raw_accel));
342 let b = na::Vector3::from(mean_raw_gyro) - self.gyro_accel_coupling * a;
343 unvec3(&b)
344 }
345
346 pub fn correct_msg_in_place(&self, imu: &mut ImuMsg) {
348 let (a, w) = self.correct(imu.linear_acceleration, imu.angular_velocity);
349 imu.linear_acceleration = a;
350 imu.angular_velocity = w;
351 }
352
353 pub fn correct_msg(&self, mut imu: ImuMsg) -> ImuMsg {
355 self.correct_msg_in_place(&mut imu);
356 imu
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use conv::{unmat3, unvec3};
364
365 const EPS: f64 = 1e-12;
366
367 fn small_rotation() -> na::Matrix3<f64> {
369 na::Rotation3::from_euler_angles(0.001, -0.002, 0.0015)
370 .matrix()
371 .to_owned()
372 }
373
374 fn assert_close(a: [f64; 3], b: [f64; 3], eps: f64) {
375 for i in 0..3 {
376 assert!((a[i] - b[i]).abs() < eps, "{a:?} != {b:?}");
377 }
378 }
379
380 #[test]
381 fn identity_passes_through() {
382 let c = ImuIntrinsics::identity().corrector().unwrap();
383 let (a, g) = c.correct([1.0, 2.0, 3.0], [0.1, 0.2, 0.3]);
384 assert_close(a, [1.0, 2.0, 3.0], EPS);
385 assert_close(g, [0.1, 0.2, 0.3], EPS);
386 }
387
388 #[test]
389 fn correction_inverts_the_kalibr_forward_model() {
390 let accel_m = na::Matrix3::new(1.01, 0.0, 0.0, 0.004, 0.99, 0.0, -0.002, 0.003, 1.02);
391 let accel_bias = na::Vector3::new(0.05, -0.1, 0.2);
392 let gyro_m = na::Matrix3::new(0.98, 0.0, 0.0, 0.003, 1.01, 0.0, -0.001, 0.002, 0.99);
393 let gyro_bias = na::Vector3::new(0.01, -0.02, 0.005);
394 let gyro_a = na::Matrix3::new(
395 1e-3, 2e-4, -3e-4, -5e-4, 7e-4, 1e-4, 2e-4, -1e-4, 6e-4,
398 );
399 let c_gyro_i = small_rotation();
400
401 let mut intr = ImuIntrinsics::identity();
402 intr.accel_m = unmat3(&accel_m);
403 intr.accel_bias = unvec3(&accel_bias);
404 intr.gyro_m = unmat3(&gyro_m);
405 intr.gyro_bias = unvec3(&gyro_bias);
406 intr.gyro_a = unmat3(&gyro_a);
407 intr.c_gyro_i = unmat3(&c_gyro_i);
408
409 let a_ideal = na::Vector3::new(0.3, -1.2, 9.7);
410 let w_ideal = na::Vector3::new(0.4, -0.15, 0.9);
411
412 let a_raw = accel_m * a_ideal + accel_bias;
414 let w_raw = gyro_m * (c_gyro_i * w_ideal) + gyro_a * (c_gyro_i * a_ideal) + gyro_bias;
415
416 let c = intr.corrector().unwrap();
417 let (a, w) = c.correct(a_raw.into(), w_raw.into());
418
419 assert_close(a, a_ideal.into(), 1e-12);
420 assert_close(w, w_ideal.into(), 1e-12);
421 }
422
423 #[test]
424 fn static_gyro_bias_is_recovered() {
425 let accel_m = na::Matrix3::new(1.01, 0.0, 0.0, 0.004, 0.99, 0.0, -0.002, 0.003, 1.02);
426 let accel_bias = na::Vector3::new(0.05, -0.1, 0.2);
427 let gyro_a = na::Matrix3::new(
428 1e-3, 2e-4, -3e-4, -5e-4, 7e-4, 1e-4, 2e-4, -1e-4, 6e-4,
431 );
432 let c_gyro_i = small_rotation();
433 let true_bias = na::Vector3::new(0.011, -0.023, 0.006);
434
435 let mut intr = ImuIntrinsics::identity();
436 intr.accel_m = unmat3(&accel_m);
437 intr.accel_bias = unvec3(&accel_bias);
438 intr.gyro_a = unmat3(&gyro_a);
439 intr.c_gyro_i = unmat3(&c_gyro_i);
440
441 let a_ideal = na::Vector3::new(1.7, -3.1, 9.15);
443 let a_raw = accel_m * a_ideal + accel_bias;
444 let w_raw = gyro_a * (c_gyro_i * a_ideal) + true_bias;
445
446 intr.gyro_bias = [99.0, -99.0, 99.0];
448 let c = intr.corrector().unwrap();
449 let estimated = c.gyro_bias_from_static(a_raw.into(), w_raw.into());
450 assert_close(estimated, true_bias.into(), 1e-12);
451 }
452
453 #[test]
454 fn singular_matrix_is_rejected() {
455 let mut intr = ImuIntrinsics::identity();
456 intr.accel_m = ZERO3;
457 assert!(intr.corrector().is_err());
458 }
459
460 #[test]
461 fn noise_discretisation() {
462 let noise = ImuNoise {
463 accel_noise_density: 1.86e-3,
464 accel_random_walk: 4.33e-4,
465 gyro_noise_density: 1.87e-4,
466 gyro_random_walk: 2.66e-5,
467 update_rate: 200.0,
468 };
469 let dt: f64 = 1.0 / 200.0;
470 assert!((noise.accel_noise_discrete() - 1.86e-3 / dt.sqrt()).abs() < 1e-15);
471 assert!((noise.gyro_noise_discrete() - 1.87e-4 / dt.sqrt()).abs() < 1e-15);
472 assert!((noise.accel_bias_discrete() - 4.33e-4 * dt.sqrt()).abs() < 1e-15);
473 assert!((noise.gyro_bias_discrete() - 2.66e-5 * dt.sqrt()).abs() < 1e-15);
474 }
475
476 #[test]
477 fn msg_correction() {
478 let mut intr = ImuIntrinsics::identity();
479 intr.accel_bias = [1.0, 1.0, 1.0];
480 intr.gyro_bias = [0.5, 0.5, 0.5];
481 let c = intr.corrector().unwrap();
482
483 let corrected = c.correct_msg(ImuMsg::new(&[2.0, 3.0, 4.0], &[1.5, 2.5, 3.5]));
484 assert_close(corrected.linear_acceleration, [1.0, 2.0, 3.0], EPS);
485 assert_close(corrected.angular_velocity, [1.0, 2.0, 3.0], EPS);
486 }
487}