#![allow(clippy::needless_range_loop)]
use crate::core::matrix::{matmul, Matrix};
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct StateConstraint<S: ControlScalar, const N: usize> {
pub d_row: Matrix<S, 1, N>,
pub lo: S,
pub hi: S,
}
impl<S: ControlScalar, const N: usize> StateConstraint<S, N> {
pub fn new(d_row: Matrix<S, 1, N>, lo: S, hi: S) -> Self {
Self { d_row, lo, hi }
}
pub fn is_satisfied(&self, x: &Matrix<S, N, 1>) -> bool {
let val = matmul(&self.d_row, x).data[0][0];
val >= self.lo && val <= self.hi
}
pub fn project(&self, x: &mut Matrix<S, N, 1>) {
let val = matmul(&self.d_row, x).data[0][0];
let clamped = val.clamp_val(self.lo, self.hi);
let diff = clamped - val;
if diff.abs() < S::EPSILON {
return;
}
let dt = self.d_row.transpose(); let ddt = matmul(&self.d_row, &dt).data[0][0]; if ddt < S::EPSILON {
return;
}
let scale = diff / ddt;
for i in 0..N {
x.data[i][0] += dt.data[i][0] * scale;
}
}
}
pub struct ConstrainedKf<
S: ControlScalar,
const N: usize,
const I: usize,
const M: usize,
const C: usize,
> {
pub a: Matrix<S, N, N>,
pub b: Matrix<S, N, I>,
pub c_mat: Matrix<S, M, N>,
pub q: Matrix<S, N, N>,
pub r: Matrix<S, M, M>,
pub p: Matrix<S, N, N>,
pub x_hat: Matrix<S, N, 1>,
pub constraints: [Option<StateConstraint<S, N>>; C],
pub proj_iterations: usize,
}
impl<S: ControlScalar, const N: usize, const I: usize, const M: usize, const C: usize>
ConstrainedKf<S, N, I, M, C>
{
pub fn new(
a: Matrix<S, N, N>,
b: Matrix<S, N, I>,
c_mat: Matrix<S, M, N>,
q: Matrix<S, N, N>,
r: Matrix<S, M, M>,
) -> Self {
Self {
a,
b,
c_mat,
q,
r,
p: Matrix::<S, N, N>::identity(),
x_hat: Matrix::<S, N, 1>::zeros(),
constraints: core::array::from_fn(|_| None),
proj_iterations: 3,
}
}
pub fn with_constraint(mut self, slot: usize, d_row: Matrix<S, 1, N>, lo: S, hi: S) -> Self {
if slot < C {
self.constraints[slot] = Some(StateConstraint::new(d_row, lo, hi));
}
self
}
pub fn predict(&mut self, u: &Matrix<S, I, 1>) {
let ax = matmul(&self.a, &self.x_hat);
let bu = matmul(&self.b, u);
self.x_hat = ax.add_mat(&bu);
let ap = matmul(&self.a, &self.p);
let at = self.a.transpose();
let apat = matmul(&ap, &at);
self.p = apat.add_mat(&self.q);
}
pub fn update(&mut self, y: &Matrix<S, M, 1>) {
let ct = self.c_mat.transpose();
let cp = matmul(&self.c_mat, &self.p);
let cpct = matmul(&cp, &ct);
let s_mat = cpct.add_mat(&self.r);
let s_inv = match s_mat.inv() {
Some(inv) => inv,
None => return,
};
let pct = matmul(&self.p, &ct);
let k = matmul(&pct, &s_inv);
let cx = matmul(&self.c_mat, &self.x_hat);
let innov = y.sub_mat(&cx);
let k_innov = matmul(&k, &innov);
self.x_hat = self.x_hat.add_mat(&k_innov);
let kc = matmul(&k, &self.c_mat);
let eye = Matrix::<S, N, N>::identity();
let i_minus_kc = eye.sub_mat(&kc);
self.p = matmul(&i_minus_kc, &self.p);
self.project_constraints();
}
fn project_constraints(&mut self) {
for _ in 0..self.proj_iterations {
for slot in self.constraints.iter().flatten() {
slot.project(&mut self.x_hat);
}
}
}
pub fn state(&self) -> &Matrix<S, N, 1> {
&self.x_hat
}
}
#[cfg(test)]
mod tests {
use super::*;
fn identity_kf() -> ConstrainedKf<f64, 2, 1, 1, 2> {
let a = Matrix::<f64, 2, 2>::identity();
let b = Matrix::<f64, 2, 1>::zeros();
let c = Matrix::<f64, 1, 2> { data: [[1.0, 0.0]] };
let q = Matrix::<f64, 2, 2>::identity().scale(0.01);
let r = Matrix::<f64, 1, 1> { data: [[0.1]] };
ConstrainedKf::new(a, b, c, q, r)
}
#[test]
fn unconstrained_tracks_measurement() {
let mut kf = identity_kf();
let u = Matrix::<f64, 1, 1>::zeros();
let y = Matrix::<f64, 1, 1> { data: [[3.0]] };
for _ in 0..50 {
kf.predict(&u);
kf.update(&y);
}
let x = kf.state().data[0][0];
assert!((x - 3.0).abs() < 0.1, "x={x}");
}
#[test]
fn constraint_clamps_state() {
let mut d_row = Matrix::<f64, 1, 2>::zeros();
d_row.data[0][0] = 1.0;
let kf = identity_kf().with_constraint(0, d_row, f64::NEG_INFINITY, 2.0);
let mut kf = kf;
let u = Matrix::<f64, 1, 1>::zeros();
let y = Matrix::<f64, 1, 1> { data: [[5.0]] }; for _ in 0..30 {
kf.predict(&u);
kf.update(&y);
}
let x = kf.state().data[0][0];
assert!(x <= 2.0 + 1e-9, "x={x} should be ≤ 2.0");
}
#[test]
fn constraint_is_satisfied_check() {
let mut d_row = Matrix::<f64, 1, 2>::zeros();
d_row.data[0][0] = 1.0;
let c = StateConstraint::new(d_row, -1.0, 1.0);
let mut x_in = Matrix::<f64, 2, 1>::zeros();
x_in.data[0][0] = 0.5;
assert!(c.is_satisfied(&x_in));
let mut x_out = Matrix::<f64, 2, 1>::zeros();
x_out.data[0][0] = 2.0;
assert!(!c.is_satisfied(&x_out));
}
#[test]
fn projection_brings_inside_bounds() {
let mut d_row = Matrix::<f64, 1, 2>::zeros();
d_row.data[0][0] = 1.0;
let c = StateConstraint::new(d_row, 0.0, 1.0);
let mut x = Matrix::<f64, 2, 1>::zeros();
x.data[0][0] = 3.0; c.project(&mut x);
assert!(x.data[0][0] <= 1.0 + 1e-9, "x={}", x.data[0][0]);
}
}