#![allow(clippy::needless_range_loop)]
type Mat6 = [[f64; 6]; 6];
type Mat3 = [[f64; 3]; 3];
type Vec6 = [f64; 6];
type Vec3 = [f64; 3];
#[derive(Debug, Clone)]
pub struct KalmanState {
pub x: Vec6,
pub p: Mat6,
process_noise_var: f64,
obs_noise_var: f64,
}
impl KalmanState {
pub fn new(initial_position: Vec3, process_noise_var: f64, obs_noise_var: f64) -> Self {
let x: Vec6 = [
initial_position[0],
initial_position[1],
initial_position[2],
0.0,
0.0,
0.0,
];
let mut p = [[0.0f64; 6]; 6];
for i in 0..6 {
p[i][i] = 10.0;
}
Self {
x,
p,
process_noise_var,
obs_noise_var,
}
}
pub fn predict(&mut self, dt_secs: f64) {
for i in 0..3 {
self.x[i] += dt_secs * self.x[i + 3];
}
let mut f = mat6_identity();
for i in 0..3 {
f[i][i + 3] = dt_secs;
}
let ft = mat6_transpose(&f);
let fp = mat6_mul(&f, &self.p);
let fpft = mat6_mul(&fp, &ft);
let q = build_process_noise(dt_secs, self.process_noise_var);
self.p = mat6_add(&fpft, &q);
}
pub fn update(&mut self, observation: Vec3) {
let hx: Vec3 = [self.x[0], self.x[1], self.x[2]];
let y = vec3_sub(observation, hx);
let ph_t = mat6x3_from_cols(&self.p);
let hpht = mat3_from_top_left(&self.p);
let mut s = hpht;
for i in 0..3 {
s[i][i] += self.obs_noise_var;
}
let s_inv = match mat3_inv(&s) {
Some(m) => m,
None => return,
};
let k = mat6x3_mul_mat3(&ph_t, &s_inv);
let kv = mat6x3_mul_vec3(&k, y);
self.x = vec6_add(self.x, kv);
let mut kh = [[0.0f64; 6]; 6];
for i in 0..6 {
for j in 0..3 {
kh[i][j] = k[i][j];
}
}
let i_minus_kh = mat6_sub(&mat6_identity(), &kh);
self.p = mat6_mul(&i_minus_kh, &self.p);
}
pub fn mahalanobis_distance_sq(&self, observation: Vec3) -> f64 {
let hx: Vec3 = [self.x[0], self.x[1], self.x[2]];
let y = vec3_sub(observation, hx);
let hpht = mat3_from_top_left(&self.p);
let mut s = hpht;
for i in 0..3 {
s[i][i] += self.obs_noise_var;
}
let s_inv = match mat3_inv(&s) {
Some(m) => m,
None => return f64::INFINITY,
};
let s_inv_y = mat3_mul_vec3(&s_inv, y);
s_inv_y[0] * y[0] + s_inv_y[1] * y[1] + s_inv_y[2] * y[2]
}
pub fn position(&self) -> Vec3 {
[self.x[0], self.x[1], self.x[2]]
}
pub fn velocity(&self) -> Vec3 {
[self.x[3], self.x[4], self.x[5]]
}
pub fn position_uncertainty(&self) -> f64 {
self.p[0][0] + self.p[1][1] + self.p[2][2]
}
}
fn mat6_mul(a: &Mat6, b: &Mat6) -> Mat6 {
let mut c = [[0.0f64; 6]; 6];
for i in 0..6 {
for j in 0..6 {
for k in 0..6 {
c[i][j] += a[i][k] * b[k][j];
}
}
}
c
}
fn mat6_add(a: &Mat6, b: &Mat6) -> Mat6 {
let mut c = [[0.0f64; 6]; 6];
for i in 0..6 {
for j in 0..6 {
c[i][j] = a[i][j] + b[i][j];
}
}
c
}
fn mat6_sub(a: &Mat6, b: &Mat6) -> Mat6 {
let mut c = [[0.0f64; 6]; 6];
for i in 0..6 {
for j in 0..6 {
c[i][j] = a[i][j] - b[i][j];
}
}
c
}
fn mat6_identity() -> Mat6 {
let mut m = [[0.0f64; 6]; 6];
for i in 0..6 {
m[i][i] = 1.0;
}
m
}
fn mat6_transpose(a: &Mat6) -> Mat6 {
let mut t = [[0.0f64; 6]; 6];
for i in 0..6 {
for j in 0..6 {
t[j][i] = a[i][j];
}
}
t
}
fn mat3_inv(m: &Mat3) -> Option<Mat3> {
let c00 = m[1][1] * m[2][2] - m[1][2] * m[2][1];
let c01 = -(m[1][0] * m[2][2] - m[1][2] * m[2][0]);
let c02 = m[1][0] * m[2][1] - m[1][1] * m[2][0];
let c10 = -(m[0][1] * m[2][2] - m[0][2] * m[2][1]);
let c11 = m[0][0] * m[2][2] - m[0][2] * m[2][0];
let c12 = -(m[0][0] * m[2][1] - m[0][1] * m[2][0]);
let c20 = m[0][1] * m[1][2] - m[0][2] * m[1][1];
let c21 = -(m[0][0] * m[1][2] - m[0][2] * m[1][0]);
let c22 = m[0][0] * m[1][1] - m[0][1] * m[1][0];
let det = m[0][0] * c00 + m[0][1] * c01 + m[0][2] * c02;
if det.abs() < 1e-12 {
return None;
}
let inv_det = 1.0 / det;
Some([
[c00 * inv_det, c10 * inv_det, c20 * inv_det],
[c01 * inv_det, c11 * inv_det, c21 * inv_det],
[c02 * inv_det, c12 * inv_det, c22 * inv_det],
])
}
fn mat6x3_from_cols(p: &Mat6) -> [[f64; 3]; 6] {
let mut out = [[0.0f64; 3]; 6];
for i in 0..6 {
for j in 0..3 {
out[i][j] = p[i][j];
}
}
out
}
fn mat3_from_top_left(p: &Mat6) -> Mat3 {
let mut out = [[0.0f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
out[i][j] = p[i][j];
}
}
out
}
fn vec6_add(a: Vec6, b: Vec6) -> Vec6 {
[
a[0] + b[0],
a[1] + b[1],
a[2] + b[2],
a[3] + b[3],
a[4] + b[4],
a[5] + b[5],
]
}
fn mat6x3_mul_vec3(m: &[[f64; 3]; 6], v: Vec3) -> Vec6 {
let mut out = [0.0f64; 6];
for i in 0..6 {
for j in 0..3 {
out[i] += m[i][j] * v[j];
}
}
out
}
fn mat3_mul_vec3(m: &Mat3, v: Vec3) -> Vec3 {
[
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
]
}
fn vec3_sub(a: Vec3, b: Vec3) -> Vec3 {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn mat6x3_mul_mat3(a: &[[f64; 3]; 6], b: &Mat3) -> [[f64; 3]; 6] {
let mut out = [[0.0f64; 3]; 6];
for i in 0..6 {
for j in 0..3 {
for k in 0..3 {
out[i][j] += a[i][k] * b[k][j];
}
}
}
out
}
fn build_process_noise(dt: f64, q_a: f64) -> Mat6 {
let dt2 = dt * dt;
let dt3 = dt2 * dt;
let dt4 = dt3 * dt;
let qpp = dt4 / 4.0 * q_a; let qpv = dt3 / 2.0 * q_a; let qvv = dt2 * q_a;
let mut q = [[0.0f64; 6]; 6];
for i in 0..3 {
q[i][i] = qpp;
q[i + 3][i + 3] = qvv;
q[i][i + 3] = qpv;
q[i + 3][i] = qpv;
}
q
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kalman_stationary() {
let initial = [1.0, 2.0, 3.0];
let mut state = KalmanState::new(initial, 0.01, 1.0);
state.predict(0.5);
let pos = state.position();
assert!(
(pos[0] - 1.0).abs() < 0.01,
"px should remain near 1.0, got {}",
pos[0]
);
assert!(
(pos[1] - 2.0).abs() < 0.01,
"py should remain near 2.0, got {}",
pos[1]
);
assert!(
(pos[2] - 3.0).abs() < 0.01,
"pz should remain near 3.0, got {}",
pos[2]
);
}
#[test]
fn test_kalman_update_converges() {
let mut state = KalmanState::new([0.0, 0.0, 0.0], 1.0, 1.0);
let target = [5.0, 0.0, 0.0];
for _ in 0..10 {
state.predict(0.5);
state.update(target);
}
let pos = state.position();
assert!(
(pos[0] - 5.0).abs() < 2.0,
"px should converge toward 5.0, got {}",
pos[0]
);
}
#[test]
fn test_mahalanobis_close_observation() {
let state = KalmanState::new([3.0, 4.0, 5.0], 0.1, 0.5);
let obs = state.position();
let d2 = state.mahalanobis_distance_sq(obs);
assert!(
d2 < 1.0,
"Mahalanobis distance² for the current position should be < 1.0, got {}",
d2
);
}
#[test]
fn test_mahalanobis_far_observation() {
let state = KalmanState::new([0.0, 0.0, 0.0], 0.01, 0.01);
let far_obs = [100.0, 0.0, 0.0];
let d2 = state.mahalanobis_distance_sq(far_obs);
assert!(
d2 > 9.0,
"Mahalanobis distance² for a 100 m observation should be >> 9, got {}",
d2
);
}
}