use crate::core::matrix::{matmul, matvec, vec_add, Matrix};
use crate::core::scalar::ControlScalar;
pub struct Ekf<S: ControlScalar, const N: usize, const M: usize, const I: usize> {
pub q: Matrix<S, N, N>,
pub r: Matrix<S, M, M>,
f: fn(&[S; N], &[S; I]) -> [S; N],
f_jac: fn(&[S; N], &[S; I]) -> Matrix<S, N, N>,
h: fn(&[S; N]) -> [S; M],
h_jac: fn(&[S; N]) -> Matrix<S, M, N>,
x: [S; N],
p: Matrix<S, N, N>,
}
impl<S: ControlScalar, const N: usize, const M: usize, const I: usize> Ekf<S, N, M, I> {
#[allow(clippy::too_many_arguments)]
pub fn new(
q: Matrix<S, N, N>,
r: Matrix<S, M, M>,
f: fn(&[S; N], &[S; I]) -> [S; N],
f_jac: fn(&[S; N], &[S; I]) -> Matrix<S, N, N>,
h: fn(&[S; N]) -> [S; M],
h_jac: fn(&[S; N]) -> Matrix<S, M, N>,
x0: [S; N],
p0: Matrix<S, N, N>,
) -> Self {
Self {
q,
r,
f,
f_jac,
h,
h_jac,
x: x0,
p: p0,
}
}
pub fn predict(&mut self, u: &[S; I]) {
let f_k = (self.f_jac)(&self.x, u);
self.x = (self.f)(&self.x, u);
let fp = matmul(&f_k, &self.p);
let ft = f_k.transpose();
let fpft = matmul(&fp, &ft);
self.p = fpft.add_mat(&self.q);
}
pub fn update(&mut self, z: &[S; M]) -> Option<[S; M]> {
let h_k = (self.h_jac)(&self.x);
let hx = (self.h)(&self.x);
let innovation: [S; M] = core::array::from_fn(|i| z[i] - hx[i]);
let hp = matmul(&h_k, &self.p);
let ht = h_k.transpose();
let hpht = matmul(&hp, &ht);
let s_mat = hpht.add_mat(&self.r);
let s_inv = s_mat.inv()?;
let pht = matmul(&self.p, &ht);
let k = matmul(&pht, &s_inv);
let ky = matvec(&k, &innovation);
self.x = vec_add(&self.x, &ky);
let kh = matmul(&k, &h_k);
let eye = Matrix::<S, N, N>::identity();
let i_minus_kh = eye.sub_mat(&kh);
self.p = matmul(&i_minus_kh, &self.p);
Some(innovation)
}
pub fn state(&self) -> &[S; N] {
&self.x
}
pub fn covariance(&self) -> &Matrix<S, N, N> {
&self.p
}
pub fn reset(&mut self, x0: [S; N], p0: Matrix<S, N, N>) {
self.x = x0;
self.p = p0;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn f_linear(x: &[f64; 2], u: &[f64; 1]) -> [f64; 2] {
[x[0] + 0.01 * x[1] + 0.0 * u[0], x[1] + u[0] * 0.01]
}
fn f_jac_linear(_x: &[f64; 2], _u: &[f64; 1]) -> Matrix<f64, 2, 2> {
let mut m = Matrix::identity();
m.data[0][1] = 0.01;
m
}
fn h_linear(x: &[f64; 2]) -> [f64; 1] {
[x[0]]
}
fn h_jac_linear(_x: &[f64; 2]) -> Matrix<f64, 1, 2> {
let mut m = Matrix::zeros();
m.data[0][0] = 1.0;
m
}
#[test]
fn ekf_tracks_linear_system() {
let q = Matrix::<f64, 2, 2>::identity().scale(0.01);
let mut r = Matrix::<f64, 1, 1>::zeros();
r.data[0][0] = 0.5;
let p0 = Matrix::<f64, 2, 2>::identity().scale(10.0);
let mut ekf = Ekf::new(
q,
r,
f_linear,
f_jac_linear,
h_linear,
h_jac_linear,
[0.0, 2.0],
p0,
);
let dt = 0.01;
let v = 2.0;
for i in 0..1000 {
let true_pos = v * (i as f64 * dt);
ekf.predict(&[0.0]);
ekf.update(&[true_pos + 0.0]); }
let state = ekf.state();
assert!(
(state[0] - v * 10.0).abs() < 0.5,
"Position should be ~{}, got {}",
v * 10.0,
state[0]
);
assert!(
(state[1] - v).abs() < 0.5,
"Velocity should be ~{}, got {}",
v,
state[1]
);
}
fn h_bearing(x: &[f64; 2]) -> [f64; 1] {
[x[1].atan2(x[0])]
}
fn h_jac_bearing(x: &[f64; 2]) -> Matrix<f64, 1, 2> {
let r2 = x[0] * x[0] + x[1] * x[1];
let mut m = Matrix::zeros();
if r2 > 1e-10 {
m.data[0][0] = -x[1] / r2;
m.data[0][1] = x[0] / r2;
}
m
}
fn f_id(x: &[f64; 2], _u: &[f64; 0]) -> [f64; 2] {
*x
}
fn f_jac_id(_x: &[f64; 2], _u: &[f64; 0]) -> Matrix<f64, 2, 2> {
Matrix::identity()
}
#[test]
fn ekf_nonlinear_measurement() {
let q = Matrix::<f64, 2, 2>::identity().scale(0.001);
let mut r = Matrix::<f64, 1, 1>::zeros();
r.data[0][0] = 0.01;
let p0 = Matrix::<f64, 2, 2>::identity().scale(100.0);
let true_pos = [3.0_f64, 4.0_f64];
let mut ekf: Ekf<f64, 2, 1, 0> = Ekf::new(
q,
r,
f_id,
f_jac_id,
h_bearing,
h_jac_bearing,
[5.0, 5.0], p0,
);
let bearing = true_pos[1].atan2(true_pos[0]);
for _ in 0..500 {
ekf.predict(&[]);
ekf.update(&[bearing]);
}
let state = ekf.state();
let est_bearing = state[1].atan2(state[0]);
assert!(
(est_bearing - bearing).abs() < 0.1,
"Bearing should converge: est={:.3}, true={:.3}",
est_bearing,
bearing
);
}
#[test]
fn covariance_decreases() {
let q = Matrix::<f64, 2, 2>::identity().scale(0.01);
let mut r = Matrix::<f64, 1, 1>::zeros();
r.data[0][0] = 0.5;
let p0 = Matrix::<f64, 2, 2>::identity().scale(100.0);
let initial_trace = p0.trace();
let mut ekf = Ekf::new(
q,
r,
f_linear,
f_jac_linear,
h_linear,
h_jac_linear,
[0.0, 0.0],
p0,
);
for i in 0..100 {
ekf.predict(&[0.0]);
ekf.update(&[i as f64 * 0.01]);
}
assert!(ekf.covariance().trace() < initial_trace);
}
}