use nalgebra as na;
mod conv;
use conv::{mat3, unvec3, vec3};
pub use conv::{Mat3, Mat4, Vec3, IDENTITY3, ZERO3, ZERO_VEC3};
pub mod allan;
pub mod estimate;
#[cfg(feature = "kalibr")]
pub mod kalibr;
#[derive(Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImuMsg {
pub orientation: [f64; 4],
pub orientation_covariance: [f64; 9],
pub angular_velocity: [f64; 3],
pub angular_velocity_covariance: [f64; 9],
pub linear_acceleration: [f64; 3],
pub linear_acceleration_covariance: [f64; 9],
}
impl ImuMsg {
pub fn new(linear_accel: &[f64; 3], angular_vel: &[f64; 3]) -> Self {
Self {
linear_acceleration: *linear_accel,
angular_velocity: *angular_vel,
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ImuModel {
#[default]
Calibrated,
ScaleMisalignment,
ScaleMisalignmentSizeEffect,
}
impl ImuModel {
pub fn as_str(&self) -> &'static str {
match self {
ImuModel::Calibrated => "calibrated",
ImuModel::ScaleMisalignment => "scale-misalignment",
ImuModel::ScaleMisalignmentSizeEffect => "scale-misalignment-size-effect",
}
}
pub fn from_str_kalibr(s: &str) -> anyhow::Result<Self> {
match s {
"calibrated" => Ok(ImuModel::Calibrated),
"scale-misalignment" => Ok(ImuModel::ScaleMisalignment),
"scale-misalignment-size-effect" => Ok(ImuModel::ScaleMisalignmentSizeEffect),
other => Err(anyhow::anyhow!("unknown Kalibr IMU model `{other}`")),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImuNoise {
pub accel_noise_density: f64,
pub accel_random_walk: f64,
pub gyro_noise_density: f64,
pub gyro_random_walk: f64,
pub update_rate: f64,
}
impl Default for ImuNoise {
fn default() -> Self {
Self {
accel_noise_density: 0.0,
accel_random_walk: 0.0,
gyro_noise_density: 0.0,
gyro_random_walk: 0.0,
update_rate: 200.0,
}
}
}
impl ImuNoise {
pub fn accel_noise_discrete(&self) -> f64 {
self.accel_noise_density * self.update_rate.sqrt()
}
pub fn gyro_noise_discrete(&self) -> f64 {
self.gyro_noise_density * self.update_rate.sqrt()
}
pub fn accel_bias_discrete(&self) -> f64 {
self.accel_random_walk / self.update_rate.sqrt()
}
pub fn gyro_bias_discrete(&self) -> f64 {
self.gyro_random_walk / self.update_rate.sqrt()
}
}
#[derive(Debug, Clone)]
pub struct ImuIntrinsics {
pub model: ImuModel,
pub accel_m: Mat3,
pub accel_bias: Vec3,
pub gyro_m: Mat3,
pub c_gyro_i: Mat3,
pub gyro_a: Mat3,
pub gyro_bias: Vec3,
pub noise: ImuNoise,
pub rostopic: Option<String>,
pub t_i_b: Option<Mat4>,
pub time_offset: f64,
pub accel_lever_arms: Option<[Vec3; 3]>,
}
impl Default for ImuIntrinsics {
fn default() -> Self {
Self::identity()
}
}
impl ImuIntrinsics {
pub fn identity() -> Self {
Self {
model: ImuModel::Calibrated,
accel_m: IDENTITY3,
accel_bias: ZERO_VEC3,
gyro_m: IDENTITY3,
c_gyro_i: IDENTITY3,
gyro_a: ZERO3,
gyro_bias: ZERO_VEC3,
noise: ImuNoise::default(),
rostopic: None,
t_i_b: None,
time_offset: 0.0,
accel_lever_arms: None,
}
}
pub fn corrector(&self) -> anyhow::Result<ImuCorrector> {
ImuCorrector::new(self)
}
pub fn accel_scale(&self) -> [f64; 3] {
[self.accel_m[0][0], self.accel_m[1][1], self.accel_m[2][2]]
}
pub fn gyro_scale(&self) -> [f64; 3] {
[self.gyro_m[0][0], self.gyro_m[1][1], self.gyro_m[2][2]]
}
}
#[derive(Debug, Clone)]
pub struct ImuCorrector {
accel_inv: na::Matrix3<f64>,
gyro_inv: na::Matrix3<f64>,
gyro_accel_coupling: na::Matrix3<f64>,
accel_bias: na::Vector3<f64>,
gyro_bias: na::Vector3<f64>,
}
impl ImuCorrector {
pub fn new(intrinsics: &ImuIntrinsics) -> anyhow::Result<Self> {
let accel_inv = mat3(&intrinsics.accel_m)
.try_inverse()
.ok_or_else(|| anyhow::anyhow!("accelerometer matrix M_a is singular"))?;
let gyro_m_inv = mat3(&intrinsics.gyro_m)
.try_inverse()
.ok_or_else(|| anyhow::anyhow!("gyroscope matrix M_g is singular"))?;
let c_gyro_i = mat3(&intrinsics.c_gyro_i);
Ok(Self {
accel_inv,
gyro_inv: c_gyro_i.transpose() * gyro_m_inv,
gyro_accel_coupling: mat3(&intrinsics.gyro_a) * c_gyro_i,
accel_bias: vec3(&intrinsics.accel_bias),
gyro_bias: vec3(&intrinsics.gyro_bias),
})
}
pub fn identity() -> Self {
Self {
accel_inv: na::Matrix3::identity(),
gyro_inv: na::Matrix3::identity(),
gyro_accel_coupling: na::Matrix3::zeros(),
accel_bias: na::Vector3::zeros(),
gyro_bias: na::Vector3::zeros(),
}
}
pub fn correct_accel(&self, raw_accel: [f64; 3]) -> [f64; 3] {
let a = self.accel_inv * (na::Vector3::from(raw_accel) - self.accel_bias);
unvec3(&a)
}
pub fn correct_gyro(&self, raw_gyro: [f64; 3], corrected_accel: [f64; 3]) -> [f64; 3] {
let w_raw = na::Vector3::from(raw_gyro);
let a = na::Vector3::from(corrected_accel);
let w = self.gyro_inv * (w_raw - self.gyro_bias - self.gyro_accel_coupling * a);
unvec3(&w)
}
pub fn correct(&self, raw_accel: [f64; 3], raw_gyro: [f64; 3]) -> ([f64; 3], [f64; 3]) {
let a = self.correct_accel(raw_accel);
let w = self.correct_gyro(raw_gyro, a);
(a, w)
}
pub fn gyro_bias_from_static(
&self,
mean_raw_accel: [f64; 3],
mean_raw_gyro: [f64; 3],
) -> [f64; 3] {
let a = na::Vector3::from(self.correct_accel(mean_raw_accel));
let b = na::Vector3::from(mean_raw_gyro) - self.gyro_accel_coupling * a;
unvec3(&b)
}
pub fn correct_msg_in_place(&self, imu: &mut ImuMsg) {
let (a, w) = self.correct(imu.linear_acceleration, imu.angular_velocity);
imu.linear_acceleration = a;
imu.angular_velocity = w;
}
pub fn correct_msg(&self, mut imu: ImuMsg) -> ImuMsg {
self.correct_msg_in_place(&mut imu);
imu
}
}
#[cfg(test)]
mod tests {
use super::*;
use conv::{unmat3, unvec3};
const EPS: f64 = 1e-12;
fn small_rotation() -> na::Matrix3<f64> {
na::Rotation3::from_euler_angles(0.001, -0.002, 0.0015)
.matrix()
.to_owned()
}
fn assert_close(a: [f64; 3], b: [f64; 3], eps: f64) {
for i in 0..3 {
assert!((a[i] - b[i]).abs() < eps, "{a:?} != {b:?}");
}
}
#[test]
fn identity_passes_through() {
let c = ImuIntrinsics::identity().corrector().unwrap();
let (a, g) = c.correct([1.0, 2.0, 3.0], [0.1, 0.2, 0.3]);
assert_close(a, [1.0, 2.0, 3.0], EPS);
assert_close(g, [0.1, 0.2, 0.3], EPS);
}
#[test]
fn correction_inverts_the_kalibr_forward_model() {
let accel_m = na::Matrix3::new(1.01, 0.0, 0.0, 0.004, 0.99, 0.0, -0.002, 0.003, 1.02);
let accel_bias = na::Vector3::new(0.05, -0.1, 0.2);
let gyro_m = na::Matrix3::new(0.98, 0.0, 0.0, 0.003, 1.01, 0.0, -0.001, 0.002, 0.99);
let gyro_bias = na::Vector3::new(0.01, -0.02, 0.005);
let gyro_a = na::Matrix3::new(
1e-3, 2e-4, -3e-4, -5e-4, 7e-4, 1e-4, 2e-4, -1e-4, 6e-4,
);
let c_gyro_i = small_rotation();
let mut intr = ImuIntrinsics::identity();
intr.accel_m = unmat3(&accel_m);
intr.accel_bias = unvec3(&accel_bias);
intr.gyro_m = unmat3(&gyro_m);
intr.gyro_bias = unvec3(&gyro_bias);
intr.gyro_a = unmat3(&gyro_a);
intr.c_gyro_i = unmat3(&c_gyro_i);
let a_ideal = na::Vector3::new(0.3, -1.2, 9.7);
let w_ideal = na::Vector3::new(0.4, -0.15, 0.9);
let a_raw = accel_m * a_ideal + accel_bias;
let w_raw = gyro_m * (c_gyro_i * w_ideal) + gyro_a * (c_gyro_i * a_ideal) + gyro_bias;
let c = intr.corrector().unwrap();
let (a, w) = c.correct(a_raw.into(), w_raw.into());
assert_close(a, a_ideal.into(), 1e-12);
assert_close(w, w_ideal.into(), 1e-12);
}
#[test]
fn static_gyro_bias_is_recovered() {
let accel_m = na::Matrix3::new(1.01, 0.0, 0.0, 0.004, 0.99, 0.0, -0.002, 0.003, 1.02);
let accel_bias = na::Vector3::new(0.05, -0.1, 0.2);
let gyro_a = na::Matrix3::new(
1e-3, 2e-4, -3e-4, -5e-4, 7e-4, 1e-4, 2e-4, -1e-4, 6e-4,
);
let c_gyro_i = small_rotation();
let true_bias = na::Vector3::new(0.011, -0.023, 0.006);
let mut intr = ImuIntrinsics::identity();
intr.accel_m = unmat3(&accel_m);
intr.accel_bias = unvec3(&accel_bias);
intr.gyro_a = unmat3(&gyro_a);
intr.c_gyro_i = unmat3(&c_gyro_i);
let a_ideal = na::Vector3::new(1.7, -3.1, 9.15);
let a_raw = accel_m * a_ideal + accel_bias;
let w_raw = gyro_a * (c_gyro_i * a_ideal) + true_bias;
intr.gyro_bias = [99.0, -99.0, 99.0];
let c = intr.corrector().unwrap();
let estimated = c.gyro_bias_from_static(a_raw.into(), w_raw.into());
assert_close(estimated, true_bias.into(), 1e-12);
}
#[test]
fn singular_matrix_is_rejected() {
let mut intr = ImuIntrinsics::identity();
intr.accel_m = ZERO3;
assert!(intr.corrector().is_err());
}
#[test]
fn noise_discretisation() {
let noise = ImuNoise {
accel_noise_density: 1.86e-3,
accel_random_walk: 4.33e-4,
gyro_noise_density: 1.87e-4,
gyro_random_walk: 2.66e-5,
update_rate: 200.0,
};
let dt: f64 = 1.0 / 200.0;
assert!((noise.accel_noise_discrete() - 1.86e-3 / dt.sqrt()).abs() < 1e-15);
assert!((noise.gyro_noise_discrete() - 1.87e-4 / dt.sqrt()).abs() < 1e-15);
assert!((noise.accel_bias_discrete() - 4.33e-4 * dt.sqrt()).abs() < 1e-15);
assert!((noise.gyro_bias_discrete() - 2.66e-5 * dt.sqrt()).abs() < 1e-15);
}
#[test]
fn msg_correction() {
let mut intr = ImuIntrinsics::identity();
intr.accel_bias = [1.0, 1.0, 1.0];
intr.gyro_bias = [0.5, 0.5, 0.5];
let c = intr.corrector().unwrap();
let corrected = c.correct_msg(ImuMsg::new(&[2.0, 3.0, 4.0], &[1.5, 2.5, 3.5]));
assert_close(corrected.linear_acceleration, [1.0, 2.0, 3.0], EPS);
assert_close(corrected.angular_velocity, [1.0, 2.0, 3.0], EPS);
}
}