const STATE_COUNT: usize = 5;
const POSITION: usize = 0;
const VELOCITY: usize = 1;
const ACCELERATION: usize = 2;
const VELOCITY_BIAS: usize = 3;
const ACCELERATION_BIAS: usize = 4;
const PROCESS_JERK_VARIANCE: f32 = 800.0; const VELOCITY_BIAS_PROCESS_VARIANCE: f32 = 0.0001; const ACCELERATION_BIAS_PROCESS_VARIANCE: f32 = 0.0001;
const ACCELERATION_MEASUREMENT_VARIANCE: f32 = 0.40 * 0.40;
const ZERO_POSITION_MEASUREMENT_VARIANCE: f32 = 0.0005 * 0.0005;
const ZERO_VELOCITY_MEASUREMENT_VARIANCE: f32 = 0.01 * 0.01;
const ZERO_ACCELERATION_MEASUREMENT_VARIANCE: f32 = 0.10 * 0.10;
const INITIAL_POSITION_VARIANCE: f32 = 1.0e-4;
const INITIAL_VELOCITY_VARIANCE: f32 = 1.0;
const INITIAL_ACCELERATION_VARIANCE: f32 = 25.0;
const INITIAL_BIAS_VARIANCE: f32 = 0.01;
const BIAS_MEAN_REVERSION_TIME_S: f32 = 5.0;
const MIN_DT: f32 = 1.0e-6;
type Vec5 = [f32; STATE_COUNT];
type Mat5 = [[f32; STATE_COUNT]; STATE_COUNT];
fn identity() -> Mat5 {
let mut m = [[0.0; STATE_COUNT]; STATE_COUNT];
for (i, row) in m.iter_mut().enumerate() {
row[i] = 1.0;
}
m
}
fn mul(a: &Mat5, b: &Mat5) -> Mat5 {
let mut out = [[0.0; STATE_COUNT]; STATE_COUNT];
for i in 0..STATE_COUNT {
for j in 0..STATE_COUNT {
let mut sum = 0.0;
for (k, brow) in b.iter().enumerate() {
sum += a[i][k] * brow[j];
}
out[i][j] = sum;
}
}
out
}
fn transposed(a: &Mat5) -> Mat5 {
let mut out = [[0.0; STATE_COUNT]; STATE_COUNT];
for i in 0..STATE_COUNT {
for j in 0..STATE_COUNT {
out[j][i] = a[i][j];
}
}
out
}
fn add(a: &Mat5, b: &Mat5) -> Mat5 {
let mut out = [[0.0; STATE_COUNT]; STATE_COUNT];
for i in 0..STATE_COUNT {
for j in 0..STATE_COUNT {
out[i][j] = a[i][j] + b[i][j];
}
}
out
}
fn symmetrize(m: &mut Mat5) {
#[allow(clippy::needless_range_loop)] for i in 0..STATE_COUNT {
for j in (i + 1)..STATE_COUNT {
let avg = 0.5 * (m[i][j] + m[j][i]);
m[i][j] = avg;
m[j][i] = avg;
}
}
}
fn dot(a: &Vec5, b: &Vec5) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
#[derive(Debug, Clone, Copy)]
pub struct MotionKalmanAxis {
state: Vec5,
covariance: Mat5,
initialized: bool,
}
impl Default for MotionKalmanAxis {
fn default() -> Self {
Self {
state: [0.0; STATE_COUNT],
covariance: [[0.0; STATE_COUNT]; STATE_COUNT],
initialized: false,
}
}
}
impl MotionKalmanAxis {
pub fn acceleration(&self) -> f32 {
self.state[ACCELERATION]
}
fn reset(&mut self, acceleration_bias: f32) {
self.initialized = true;
self.state = [0.0, 0.0, 0.0, 0.0, acceleration_bias];
self.covariance = [[0.0; STATE_COUNT]; STATE_COUNT];
self.covariance[POSITION][POSITION] = INITIAL_POSITION_VARIANCE;
self.covariance[VELOCITY][VELOCITY] = INITIAL_VELOCITY_VARIANCE;
self.covariance[ACCELERATION][ACCELERATION] = INITIAL_ACCELERATION_VARIANCE;
self.covariance[VELOCITY_BIAS][VELOCITY_BIAS] = INITIAL_BIAS_VARIANCE;
self.covariance[ACCELERATION_BIAS][ACCELERATION_BIAS] = ACCELERATION_MEASUREMENT_VARIANCE;
}
fn transition_matrix(dt: f32) -> Mat5 {
let mut f = identity();
f[POSITION][VELOCITY] = dt;
f[POSITION][ACCELERATION] = 0.5 * dt * dt;
f[VELOCITY][ACCELERATION] = dt;
let alpha = (-dt / BIAS_MEAN_REVERSION_TIME_S).exp();
f[VELOCITY_BIAS][VELOCITY_BIAS] = alpha;
f[ACCELERATION_BIAS][ACCELERATION_BIAS] = alpha;
f
}
fn process_noise(dt: f32) -> Mat5 {
let (dt2, dt3) = (dt * dt, dt * dt * dt);
let (dt4, dt5) = (dt3 * dt, dt3 * dt * dt);
let qj = PROCESS_JERK_VARIANCE;
let mut q = [[0.0; STATE_COUNT]; STATE_COUNT];
q[POSITION][POSITION] = qj * (dt5 / 20.0);
q[POSITION][VELOCITY] = qj * (dt4 / 8.0);
q[POSITION][ACCELERATION] = qj * (dt3 / 6.0);
q[VELOCITY][POSITION] = qj * (dt4 / 8.0);
q[VELOCITY][VELOCITY] = qj * (dt3 / 3.0);
q[VELOCITY][ACCELERATION] = qj * (dt2 / 2.0);
q[ACCELERATION][POSITION] = qj * (dt3 / 6.0);
q[ACCELERATION][VELOCITY] = qj * (dt2 / 2.0);
q[ACCELERATION][ACCELERATION] = qj * dt;
q[VELOCITY_BIAS][VELOCITY_BIAS] = VELOCITY_BIAS_PROCESS_VARIANCE * dt;
q[ACCELERATION_BIAS][ACCELERATION_BIAS] = ACCELERATION_BIAS_PROCESS_VARIANCE * dt;
q
}
pub fn predict(&mut self, dt: f32) {
if !self.initialized {
return;
}
let dt = dt.max(MIN_DT);
let f = Self::transition_matrix(dt);
let ft = transposed(&f);
let q = Self::process_noise(dt);
let mut new_state = [0.0; STATE_COUNT];
for (i, s) in new_state.iter_mut().enumerate() {
*s = dot(&f[i], &self.state);
}
self.state = new_state;
self.covariance = add(&mul(&mul(&f, &self.covariance), &ft), &q);
symmetrize(&mut self.covariance);
}
fn update_scalar(&mut self, h: &Vec5, measurement: f32, variance: f32) {
let innovation = measurement - dot(h, &self.state);
let mut pht = [0.0; STATE_COUNT];
for (i, v) in pht.iter_mut().enumerate() {
*v = dot(&self.covariance[i], h);
}
let s = dot(h, &pht) + variance;
if s <= 0.0 {
return;
}
let inv_s = 1.0 / s;
let mut k = [0.0; STATE_COUNT];
for (i, v) in k.iter_mut().enumerate() {
*v = pht[i] * inv_s;
}
for (i, s) in self.state.iter_mut().enumerate() {
*s += k[i] * innovation;
}
let mut a = identity();
for i in 0..STATE_COUNT {
for j in 0..STATE_COUNT {
a[i][j] -= k[i] * h[j];
}
}
let at = transposed(&a);
let apat = mul(&mul(&a, &self.covariance), &at);
let mut krkt = [[0.0; STATE_COUNT]; STATE_COUNT];
for i in 0..STATE_COUNT {
for j in 0..STATE_COUNT {
krkt[i][j] = k[i] * k[j] * variance;
}
}
self.covariance = add(&apat, &krkt);
symmetrize(&mut self.covariance);
}
pub fn update_acceleration(&mut self, acceleration: f32) {
if !self.initialized {
self.reset(acceleration);
return;
}
let mut h = [0.0; STATE_COUNT];
h[ACCELERATION] = 1.0;
h[ACCELERATION_BIAS] = 1.0;
self.update_scalar(&h, acceleration, ACCELERATION_MEASUREMENT_VARIANCE);
}
pub fn update_rest(&mut self) {
if !self.initialized {
return;
}
let mut h = [0.0; STATE_COUNT];
h[POSITION] = 1.0;
self.update_scalar(&h, 0.0, ZERO_POSITION_MEASUREMENT_VARIANCE);
let mut h = [0.0; STATE_COUNT];
h[VELOCITY] = 1.0;
self.update_scalar(&h, 0.0, ZERO_VELOCITY_MEASUREMENT_VARIANCE);
let mut h = [0.0; STATE_COUNT];
h[ACCELERATION] = 1.0;
self.update_scalar(&h, 0.0, ZERO_ACCELERATION_MEASUREMENT_VARIANCE);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_steady_reading_at_startup_is_taken_as_bias() {
let mut k = MotionKalmanAxis::default();
for _ in 0..200 {
k.predict(0.008);
k.update_acceleration(0.3);
}
assert!(
k.acceleration().abs() < 0.15,
"reported {} m/s² of motion from a constant offset",
k.acceleration()
);
}
#[test]
fn it_follows_a_real_acceleration() {
let mut k = MotionKalmanAxis::default();
for _ in 0..50 {
k.predict(0.008);
k.update_acceleration(0.0);
}
for _ in 0..30 {
k.predict(0.008);
k.update_acceleration(5.0);
}
assert!(k.acceleration() > 2.0, "only reached {}", k.acceleration());
}
#[test]
fn rest_constraints_pull_it_back_to_zero() {
let mut k = MotionKalmanAxis::default();
for _ in 0..50 {
k.predict(0.008);
k.update_acceleration(4.0);
}
for _ in 0..100 {
k.predict(0.008);
k.update_rest();
}
assert!(
k.acceleration().abs() < 0.2,
"still at {}",
k.acceleration()
);
}
#[test]
fn the_filter_debiases_rather_than_smooths() {
let mut k = MotionKalmanAxis::default();
let mut seed: u32 = 12345;
let mut next = || {
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
((seed >> 16) as f32 / 32768.0) - 1.0 };
let (mut in_sq, mut out_sq, mut n) = (0.0f32, 0.0f32, 0.0f32);
for i in 0..2000 {
let sample = next();
k.predict(0.008);
k.update_acceleration(sample);
if i > 400 {
in_sq += sample * sample;
out_sq += k.acceleration() * k.acceleration();
n += 1.0;
}
}
let (rms_in, rms_out) = ((in_sq / n).sqrt(), (out_sq / n).sqrt());
assert!(
rms_out < rms_in * 1.5,
"noise went in at {rms_in} and came out at {rms_out}"
);
}
#[test]
fn a_sample_at_the_nyquist_rate_is_not_smoothed_away() {
let mut k = MotionKalmanAxis::default();
let mut worst: f32 = 0.0;
for i in 0..400 {
k.predict(0.008);
k.update_acceleration(if i % 2 == 0 { 1.0 } else { -1.0 });
if i > 100 {
worst = worst.max(k.acceleration().abs());
}
}
assert!(worst > 1.0, "expected overshoot, got {worst}");
}
}