#![allow(unused, clippy::needless_range_loop)]
use crate::core::matrix::{matmul, Matrix};
use crate::core::scalar::ControlScalar;
#[derive(Debug)]
pub enum MheError {
WindowTooSmall,
DegenerateJacobian,
DimensionMismatch,
}
impl core::fmt::Display for MheError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
MheError::WindowTooSmall => write!(f, "MHE window too small"),
MheError::DegenerateJacobian => write!(f, "Degenerate Jacobian in MHE linearisation"),
MheError::DimensionMismatch => write!(f, "Dimension mismatch in MHE"),
}
}
}
pub type DynamicsFn<S, const N: usize, const I: usize> =
fn(&Matrix<S, N, 1>, &Matrix<S, I, 1>) -> Matrix<S, N, 1>;
pub type MeasurementFn<S, const N: usize, const M: usize> = fn(&Matrix<S, N, 1>) -> Matrix<S, M, 1>;
pub struct MheWindow<
S: ControlScalar,
const N: usize,
const I: usize,
const M: usize,
const W: usize,
> {
pub measurements: [Matrix<S, M, 1>; W],
pub inputs: [Matrix<S, I, 1>; W],
pub count: usize,
}
impl<S: ControlScalar, const N: usize, const I: usize, const M: usize, const W: usize>
MheWindow<S, N, I, M, W>
{
pub fn new() -> Self {
Self {
measurements: [Matrix::zeros(); W],
inputs: [Matrix::zeros(); W],
count: 0,
}
}
pub fn push(&mut self, y: Matrix<S, M, 1>, u: Matrix<S, I, 1>) {
if self.count < W {
self.measurements[self.count] = y;
self.inputs[self.count] = u;
self.count += 1;
} else {
for k in 0..(W - 1) {
self.measurements[k] = self.measurements[k + 1];
self.inputs[k] = self.inputs[k + 1];
}
self.measurements[W - 1] = y;
self.inputs[W - 1] = u;
}
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
}
impl<S: ControlScalar, const N: usize, const I: usize, const M: usize, const W: usize> Default
for MheWindow<S, N, I, M, W>
{
fn default() -> Self {
Self::new()
}
}
pub struct MovingHorizonEstimator<
S: ControlScalar,
const N: usize,
const I: usize,
const M: usize,
const W: usize,
> {
pub dynamics: DynamicsFn<S, N, I>,
pub measurement: MeasurementFn<S, N, M>,
pub q_inv_diag: Matrix<S, N, 1>,
pub r_inv_diag: Matrix<S, M, 1>,
pub p0_inv_diag: Matrix<S, N, 1>,
pub x_prior: Matrix<S, N, 1>,
pub x_estimate: Matrix<S, N, 1>,
pub window: MheWindow<S, N, I, M, W>,
pub iterations: usize,
pub fd_eps: S,
pub step_size: S,
}
impl<S: ControlScalar, const N: usize, const I: usize, const M: usize, const W: usize>
MovingHorizonEstimator<S, N, I, M, W>
{
pub fn new(
dynamics: DynamicsFn<S, N, I>,
measurement: MeasurementFn<S, N, M>,
q_inv_diag: Matrix<S, N, 1>,
r_inv_diag: Matrix<S, M, 1>,
p0_inv_diag: Matrix<S, N, 1>,
iterations: usize,
) -> Self {
Self {
dynamics,
measurement,
q_inv_diag,
r_inv_diag,
p0_inv_diag,
x_prior: Matrix::zeros(),
x_estimate: Matrix::zeros(),
window: MheWindow::new(),
iterations,
fd_eps: S::from_f64(1e-5),
step_size: S::from_f64(0.1),
}
}
pub fn push_measurement(&mut self, y: Matrix<S, M, 1>, u: Matrix<S, I, 1>) {
self.window.push(y, u);
}
fn rollout(&self, x0: &Matrix<S, N, 1>, n: usize) -> [Matrix<S, N, 1>; W] {
let mut states = [Matrix::<S, N, 1>::zeros(); W];
let mut x = *x0;
for k in 0..n {
let u = self.window.inputs[k];
let x_next = (self.dynamics)(&x, &u);
states[k] = x_next;
x = x_next;
}
states
}
pub fn cost(&self, x0: &Matrix<S, N, 1>) -> S {
let n = self.window.len().min(W);
if n == 0 {
return S::ZERO;
}
let mut arrival = S::ZERO;
for i in 0..N {
let diff = x0.data[i][0] - self.x_prior.data[i][0];
arrival += self.p0_inv_diag.data[i][0] * diff * diff;
}
let states = self.rollout(x0, n);
let mut meas_cost = S::ZERO;
let mut proc_cost = S::ZERO;
{
let y0 = self.window.measurements[0];
let h0 = (self.measurement)(x0);
for j in 0..M {
let r = y0.data[j][0] - h0.data[j][0];
meas_cost += self.r_inv_diag.data[j][0] * r * r;
}
}
for k in 0..(n - 1) {
let y = self.window.measurements[k + 1];
let h = (self.measurement)(&states[k]);
for j in 0..M {
let r = y.data[j][0] - h.data[j][0];
meas_cost += self.r_inv_diag.data[j][0] * r * r;
}
}
{
let u0 = self.window.inputs[0];
let fx = (self.dynamics)(x0, &u0);
for i in 0..N {
let w = states[0].data[i][0] - fx.data[i][0];
proc_cost += self.q_inv_diag.data[i][0] * w * w;
}
}
for k in 1..(n - 1) {
let u = self.window.inputs[k];
let fx = (self.dynamics)(&states[k - 1], &u);
for i in 0..N {
let w = states[k].data[i][0] - fx.data[i][0];
proc_cost += self.q_inv_diag.data[i][0] * w * w;
}
}
arrival + meas_cost + proc_cost
}
fn gradient_x0(&self, x0: &Matrix<S, N, 1>) -> Matrix<S, N, 1> {
let eps = self.fd_eps;
let two_eps = S::TWO * eps;
let mut grad = Matrix::<S, N, 1>::zeros();
for i in 0..N {
let mut xp = *x0;
let mut xm = *x0;
xp.data[i][0] += eps;
xm.data[i][0] -= eps;
grad.data[i][0] = (self.cost(&xp) - self.cost(&xm)) / two_eps;
}
grad
}
pub fn solve(&mut self) -> Result<Matrix<S, N, 1>, MheError> {
let n = self.window.len();
if n == 0 {
return Err(MheError::WindowTooSmall);
}
let mut x0 = self.x_prior;
for _iter in 0..self.iterations {
let grad = self.gradient_x0(&x0);
for i in 0..N {
x0.data[i][0] -= self.step_size * grad.data[i][0];
}
}
let states = self.rollout(&x0, n.min(W));
let x_final = if n > 0 {
states[(n - 1).min(W - 1)]
} else {
x0
};
self.x_estimate = x_final;
self.x_prior = x0;
Ok(x_final)
}
pub fn estimate(&self) -> Matrix<S, N, 1> {
self.x_estimate
}
pub fn reset(&mut self, x0: Matrix<S, N, 1>) {
self.x_estimate = x0;
self.x_prior = x0;
self.window = MheWindow::new();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn linear_dynamics(x: &Matrix<f64, 2, 1>, _u: &Matrix<f64, 1, 1>) -> Matrix<f64, 2, 1> {
let mut xn = Matrix::<f64, 2, 1>::zeros();
xn.data[0][0] = x.data[0][0];
xn.data[1][0] = x.data[1][0];
xn
}
fn identity_measurement(x: &Matrix<f64, 2, 1>) -> Matrix<f64, 2, 1> {
*x
}
fn make_mhe() -> MovingHorizonEstimator<f64, 2, 1, 2, 4> {
let q_inv = {
let mut m = Matrix::<f64, 2, 1>::zeros();
m.data[0][0] = 1.0;
m.data[1][0] = 1.0;
m
};
let r_inv = {
let mut m = Matrix::<f64, 2, 1>::zeros();
m.data[0][0] = 10.0;
m.data[1][0] = 10.0;
m
};
let p0_inv = {
let mut m = Matrix::<f64, 2, 1>::zeros();
m.data[0][0] = 1.0;
m.data[1][0] = 1.0;
m
};
MovingHorizonEstimator::new(
linear_dynamics,
identity_measurement,
q_inv,
r_inv,
p0_inv,
50,
)
}
#[test]
fn window_push_fills_correctly() {
let mut w = MheWindow::<f64, 2, 1, 2, 4>::new();
assert_eq!(w.len(), 0);
let y = Matrix::<f64, 2, 1>::zeros();
let u = Matrix::<f64, 1, 1>::zeros();
w.push(y, u);
assert_eq!(w.len(), 1);
}
#[test]
fn window_push_slides_when_full() {
let mut w = MheWindow::<f64, 2, 1, 2, 3>::new();
for k in 0..5u32 {
let mut y = Matrix::<f64, 2, 1>::zeros();
y.data[0][0] = k as f64;
w.push(y, Matrix::zeros());
}
assert_eq!(w.len(), 3);
assert!((w.measurements[0].data[0][0] - 2.0).abs() < 1e-12);
assert!((w.measurements[2].data[0][0] - 4.0).abs() < 1e-12);
}
#[test]
fn mhe_empty_window_returns_error() {
let mut mhe = make_mhe();
let result = mhe.solve();
assert!(matches!(result, Err(MheError::WindowTooSmall)));
}
#[test]
fn mhe_cost_zero_at_prior_with_consistent_measurements() {
let mut mhe = make_mhe();
let mut x0 = Matrix::<f64, 2, 1>::zeros();
x0.data[0][0] = 1.0;
mhe.x_prior = x0;
let y = x0;
let u = Matrix::<f64, 1, 1>::zeros();
mhe.window.push(y, u);
let c = mhe.cost(&x0);
assert!(
c < 1e-10,
"Cost at consistent state should be near zero, got {}",
c
);
}
#[test]
fn mhe_solve_with_one_measurement() {
let mut mhe = make_mhe();
let mut y = Matrix::<f64, 2, 1>::zeros();
y.data[0][0] = 2.0;
mhe.push_measurement(y, Matrix::zeros());
let result = mhe.solve();
assert!(
result.is_ok(),
"MHE solve should succeed with one measurement"
);
}
#[test]
fn mhe_reset_clears_window() {
let mut mhe = make_mhe();
mhe.push_measurement(Matrix::zeros(), Matrix::zeros());
assert_eq!(mhe.window.len(), 1);
let x_new = Matrix::<f64, 2, 1>::zeros();
mhe.reset(x_new);
assert_eq!(mhe.window.len(), 0);
}
}