type Mat = Vec<Vec<f64>>;
type SigmaSet = (Vec<Vec<f64>>, Vec<f64>, Vec<f64>);
pub fn cholesky(p: &[Vec<f64>]) -> Option<Mat> {
let n = p.len();
let mut l = vec![vec![0.0; n]; n];
for i in 0..n {
for j in 0..=i {
let dot: f64 = l[i][..j].iter().zip(&l[j][..j]).map(|(&a, &b)| a * b).sum();
let sum = p[i][j] - dot;
if i == j {
if sum <= 0.0 {
return None;
}
l[i][j] = sum.sqrt();
} else {
l[i][j] = sum / l[j][j];
}
}
}
Some(l)
}
pub fn inverse(a: &[Vec<f64>]) -> Option<Mat> {
let n = a.len();
let mut m: Mat = a
.iter()
.enumerate()
.map(|(i, row)| {
let mut r = row.clone();
r.extend((0..n).map(|j| if i == j { 1.0 } else { 0.0 }));
r
})
.collect();
for col in 0..n {
let mut piv = col;
for r in (col + 1)..n {
if m[r][col].abs() > m[piv][col].abs() {
piv = r;
}
}
if m[piv][col].abs() < 1e-300 {
return None;
}
m.swap(col, piv);
let d = m[col][col];
for x in m[col].iter_mut() {
*x /= d;
}
let pivot_row = m[col].clone();
for (r, row) in m.iter_mut().enumerate() {
if r != col {
let f = row[col];
if f != 0.0 {
for (x, &pv) in row.iter_mut().zip(&pivot_row) {
*x -= f * pv;
}
}
}
}
}
Some(m.iter().map(|row| row[n..2 * n].to_vec()).collect())
}
fn mat_vec(a: &[Vec<f64>], v: &[f64]) -> Vec<f64> {
a.iter()
.map(|row| row.iter().zip(v).map(|(&x, &y)| x * y).sum())
.collect()
}
fn matmul(a: &[Vec<f64>], b: &[Vec<f64>]) -> Mat {
let (n, m, p) = (a.len(), b[0].len(), b.len());
let mut r = vec![vec![0.0; m]; n];
for (i, ri) in r.iter_mut().enumerate() {
for (j, rij) in ri.iter_mut().enumerate() {
*rij = (0..p).map(|k| a[i][k] * b[k][j]).sum();
}
}
r
}
fn transpose(a: &[Vec<f64>]) -> Mat {
let (n, m) = (a.len(), a[0].len());
let mut t = vec![vec![0.0; n]; m];
for (i, row) in a.iter().enumerate() {
for (j, &x) in row.iter().enumerate() {
t[j][i] = x;
}
}
t
}
#[derive(Clone, Debug)]
pub struct Ukf {
pub x: Vec<f64>,
pub p: Mat,
pub alpha: f64,
pub beta: f64,
pub kappa: f64,
}
impl Ukf {
pub fn new(x: Vec<f64>, p: Mat) -> Self {
Self {
x,
p,
alpha: 1e-3,
beta: 2.0,
kappa: 0.0,
}
}
fn n(&self) -> usize {
self.x.len()
}
fn lambda(&self) -> f64 {
self.alpha * self.alpha * (self.n() as f64 + self.kappa) - self.n() as f64
}
fn sigma_points(&self) -> Option<SigmaSet> {
let n = self.n();
let lambda = self.lambda();
let l = cholesky(&self.p)?;
let gamma = (n as f64 + lambda).sqrt();
let lt = transpose(&l);
let mut pts = Vec::with_capacity(2 * n + 1);
pts.push(self.x.clone());
for lcol in < {
let plus: Vec<f64> = self
.x
.iter()
.zip(lcol)
.map(|(&xr, &v)| xr + gamma * v)
.collect();
let minus: Vec<f64> = self
.x
.iter()
.zip(lcol)
.map(|(&xr, &v)| xr - gamma * v)
.collect();
pts.push(plus);
pts.push(minus);
}
let wm0 = lambda / (n as f64 + lambda);
let wc0 = wm0 + (1.0 - self.alpha * self.alpha + self.beta);
let wi = 1.0 / (2.0 * (n as f64 + lambda));
let mut wm = vec![wi; 2 * n + 1];
let mut wc = vec![wi; 2 * n + 1];
wm[0] = wm0;
wc[0] = wc0;
Some((pts, wm, wc))
}
pub fn predict<F>(&mut self, f: F, q: &[Vec<f64>]) -> bool
where
F: Fn(&[f64]) -> Vec<f64>,
{
let n = self.n();
let Some((pts, wm, wc)) = self.sigma_points() else {
return false;
};
let prop: Vec<Vec<f64>> = pts.iter().map(|p| f(p)).collect();
let mut x = vec![0.0; n];
for (w, y) in wm.iter().zip(&prop) {
for (xi, &yi) in x.iter_mut().zip(y) {
*xi += w * yi;
}
}
let mut p = vec![vec![0.0; n]; n];
for (w, y) in wc.iter().zip(&prop) {
let d: Vec<f64> = y.iter().zip(&x).map(|(&yi, &xi)| yi - xi).collect();
for i in 0..n {
for j in 0..n {
p[i][j] += w * d[i] * d[j];
}
}
}
for i in 0..n {
for j in 0..n {
p[i][j] += q[i][j];
}
}
self.x = x;
self.p = p;
true
}
pub fn update<H>(&mut self, h: H, z: &[f64], r: &[Vec<f64>]) -> bool
where
H: Fn(&[f64]) -> Vec<f64>,
{
let n = self.n();
let Some((pts, wm, wc)) = self.sigma_points() else {
return false;
};
let zsig: Vec<Vec<f64>> = pts.iter().map(|p| h(p)).collect();
let m = zsig[0].len();
let mut zbar = vec![0.0; m];
for (w, zs) in wm.iter().zip(&zsig) {
for (zi, &zv) in zbar.iter_mut().zip(zs) {
*zi += w * zv;
}
}
let mut s = vec![vec![0.0; m]; m];
let mut c = vec![vec![0.0; m]; n];
for ((w, zs), xs) in wc.iter().zip(&zsig).zip(&pts) {
let dz: Vec<f64> = zs.iter().zip(&zbar).map(|(&a, &b)| a - b).collect();
let dx: Vec<f64> = xs.iter().zip(&self.x).map(|(&a, &b)| a - b).collect();
for i in 0..m {
for j in 0..m {
s[i][j] += w * dz[i] * dz[j];
}
}
for i in 0..n {
for j in 0..m {
c[i][j] += w * dx[i] * dz[j];
}
}
}
for i in 0..m {
for j in 0..m {
s[i][j] += r[i][j];
}
}
let Some(s_inv) = inverse(&s) else {
return false;
};
let k = matmul(&c, &s_inv); let innov: Vec<f64> = z.iter().zip(&zbar).map(|(&a, &b)| a - b).collect();
let dx = mat_vec(&k, &innov);
for (xi, &d) in self.x.iter_mut().zip(&dx) {
*xi += d;
}
let ks = matmul(&k, &s); let ksk = matmul(&ks, &transpose(&k)); for (prow, krow) in self.p.iter_mut().zip(&ksk) {
for (pij, &kij) in prow.iter_mut().zip(krow) {
*pij -= kij;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq_mat(a: &[Vec<f64>], b: &[Vec<f64>], tol: f64) -> bool {
a.iter()
.zip(b)
.all(|(ra, rb)| ra.iter().zip(rb).all(|(&x, &y)| (x - y).abs() < tol))
}
#[test]
fn cholesky_reconstructs_spd() {
let p = vec![
vec![4.0, 2.0, 0.4],
vec![2.0, 5.0, 1.0],
vec![0.4, 1.0, 3.0],
];
let l = cholesky(&p).expect("spd");
let recon = matmul(&l, &transpose(&l));
assert!(approx_eq_mat(&p, &recon, 1e-12));
assert!(cholesky(&[vec![1.0, 2.0], vec![2.0, 1.0]]).is_none());
}
#[test]
fn inverse_is_correct() {
let a = vec![vec![4.0, 7.0], vec![2.0, 6.0]];
let ai = inverse(&a).expect("nonsingular");
let id = matmul(&a, &ai);
assert!(approx_eq_mat(&id, &[vec![1.0, 0.0], vec![0.0, 1.0]], 1e-12));
assert!(inverse(&[vec![1.0, 2.0], vec![2.0, 4.0]]).is_none());
}
fn kf_predict(x: &[f64], p: &[Vec<f64>], f: &[Vec<f64>], q: &[Vec<f64>]) -> (Vec<f64>, Mat) {
let xp = mat_vec(f, x);
let pp = matmul(&matmul(f, p), &transpose(f));
let pp = (0..pp.len())
.map(|i| (0..pp.len()).map(|j| pp[i][j] + q[i][j]).collect())
.collect();
(xp, pp)
}
fn kf_update(
x: &[f64],
p: &[Vec<f64>],
h: &[Vec<f64>],
z: &[f64],
r: &[Vec<f64>],
) -> (Vec<f64>, Mat) {
let ht = transpose(h);
let s: Mat = {
let hp = matmul(h, p);
let hph = matmul(&hp, &ht);
(0..hph.len())
.map(|i| (0..hph.len()).map(|j| hph[i][j] + r[i][j]).collect())
.collect()
};
let si = inverse(&s).unwrap();
let k = matmul(&matmul(p, &ht), &si); let hx = mat_vec(h, x);
let innov: Vec<f64> = z.iter().zip(&hx).map(|(&a, &b)| a - b).collect();
let dx = mat_vec(&k, &innov);
let xn: Vec<f64> = x.iter().zip(&dx).map(|(&a, &b)| a + b).collect();
let kh = matmul(&k, h);
let n = x.len();
let mut imkh = vec![vec![0.0; n]; n];
for i in 0..n {
for j in 0..n {
imkh[i][j] = (if i == j { 1.0 } else { 0.0 }) - kh[i][j];
}
}
(xn, matmul(&imkh, p))
}
#[test]
fn ukf_predict_equals_linear_kf() {
let f_mat = vec![vec![1.0, 0.5], vec![0.0, 1.0]];
let q = vec![vec![0.01, 0.0], vec![0.0, 0.04]];
let x0 = vec![3.0, -1.0];
let p0 = vec![vec![1.0, 0.2], vec![0.2, 0.5]];
let mut ukf = Ukf::new(x0.clone(), p0.clone());
let fm = f_mat.clone();
assert!(ukf.predict(|s| mat_vec(&fm, s), &q));
let (xk, pk) = kf_predict(&x0, &p0, &f_mat, &q);
assert!(ukf.x.iter().zip(&xk).all(|(&a, &b)| (a - b).abs() < 1e-9));
assert!(
approx_eq_mat(&ukf.p, &pk, 1e-9),
"P {:?} vs {:?}",
ukf.p,
pk
);
}
#[test]
fn ukf_update_equals_linear_kf() {
let h_mat = vec![vec![1.0, 0.0]];
let r = vec![vec![0.25]];
let x0 = vec![3.0, -1.0];
let p0 = vec![vec![1.0, 0.2], vec![0.2, 0.5]];
let z = vec![3.6];
let mut ukf = Ukf::new(x0.clone(), p0.clone());
let hm = h_mat.clone();
assert!(ukf.update(|s| mat_vec(&hm, s), &z, &r));
let (xk, pk) = kf_update(&x0, &p0, &h_mat, &z, &r);
assert!(
ukf.x.iter().zip(&xk).all(|(&a, &b)| (a - b).abs() < 1e-9),
"x {:?} vs {:?}",
ukf.x,
xk
);
assert!(
approx_eq_mat(&ukf.p, &pk, 1e-9),
"P {:?} vs {:?}",
ukf.p,
pk
);
}
#[test]
fn ukf_full_cycle_equals_linear_kf() {
let f_mat = vec![vec![1.0, 1.0], vec![0.0, 1.0]];
let q = vec![vec![0.001, 0.0], vec![0.0, 0.001]];
let h_mat = vec![vec![1.0, 0.0]];
let r = vec![vec![0.1]];
let x0 = vec![0.0, 1.0];
let p0 = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let z = vec![1.2];
let mut ukf = Ukf::new(x0.clone(), p0.clone());
ukf.alpha = 0.5; let fm = f_mat.clone();
let hm = h_mat.clone();
assert!(ukf.predict(|s| mat_vec(&fm, s), &q));
assert!(ukf.update(|s| mat_vec(&hm, s), &z, &r));
let (xk, pk) = kf_predict(&x0, &p0, &f_mat, &q);
let (xk, pk) = kf_update(&xk, &pk, &h_mat, &z, &r);
assert!(ukf.x.iter().zip(&xk).all(|(&a, &b)| (a - b).abs() < 1e-9));
assert!(approx_eq_mat(&ukf.p, &pk, 1e-9));
}
#[test]
fn ukf_1d_constant_recovers_bayesian_posterior() {
let mu0 = 2.0;
let s0 = 4.0;
let sz = 1.0;
let z = 5.0;
let mut ukf = Ukf::new(vec![mu0], vec![vec![s0]]);
assert!(ukf.update(|s| vec![s[0]], &[z], &[vec![sz]]));
let post_var = 1.0 / (1.0 / s0 + 1.0 / sz);
let post_mean = post_var * (mu0 / s0 + z / sz);
assert!((ukf.x[0] - post_mean).abs() < 1e-9, "mean {}", ukf.x[0]);
assert!((ukf.p[0][0] - post_var).abs() < 1e-9, "var {}", ukf.p[0][0]);
}
}