use crate::antiwindup::aw_compensator::AntiWindupError;
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone)]
pub struct ObserverAntiWindup<S: ControlScalar, const N: usize> {
x_hat: [S; N],
a: [[S; N]; N],
b: [S; N],
c: [S; N],
l: [S; N],
k: [S; N],
n_gain: S,
u_min: S,
u_max: S,
saturated: bool,
dt: S,
}
impl<S: ControlScalar, const N: usize> ObserverAntiWindup<S, N> {
#[allow(clippy::too_many_arguments)]
pub fn new(
a: [[S; N]; N],
b: [S; N],
c: [S; N],
l: [S; N],
k: [S; N],
n_gain: S,
u_min: S,
u_max: S,
dt: S,
) -> Result<Self, AntiWindupError> {
if u_min >= u_max {
return Err(AntiWindupError::InvalidParameter);
}
if dt <= S::ZERO {
return Err(AntiWindupError::InvalidParameter);
}
Ok(Self {
x_hat: [S::ZERO; N],
a,
b,
c,
l,
k,
n_gain,
u_min,
u_max,
saturated: false,
dt,
})
}
pub fn update(&mut self, r: S, y: S) -> Result<S, AntiWindupError> {
let mut u_lin = self.n_gain * r;
for i in 0..N {
u_lin -= self.k[i] * self.x_hat[i];
}
let v = u_lin.clamp_val(self.u_min, self.u_max);
self.saturated = (v - u_lin) * (v - u_lin) > S::EPSILON * S::EPSILON;
let mut c_xhat = S::ZERO;
for i in 0..N {
c_xhat += self.c[i] * self.x_hat[i];
}
let innov = y - c_xhat;
let mut ax = [S::ZERO; N];
#[allow(clippy::needless_range_loop)]
for i in 0..N {
for j in 0..N {
ax[i] += self.a[i][j] * self.x_hat[j];
}
}
#[allow(clippy::needless_range_loop)]
for i in 0..N {
let dx = ax[i] + self.b[i] * v + self.l[i] * innov;
self.x_hat[i] += self.dt * dx;
}
Ok(v)
}
#[inline]
pub fn state_estimate(&self) -> &[S; N] {
&self.x_hat
}
#[inline]
pub fn is_saturated(&self) -> bool {
self.saturated
}
pub fn reset(&mut self, x0: [S; N]) {
self.x_hat = x0;
self.saturated = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::too_many_arguments)]
fn make_obs_aw(
a: f64,
b: f64,
c: f64,
l: f64,
k: f64,
n_gain: f64,
u_min: f64,
u_max: f64,
dt: f64,
) -> ObserverAntiWindup<f64, 1> {
ObserverAntiWindup::new([[a]], [b], [c], [l], [k], n_gain, u_min, u_max, dt).unwrap()
}
#[test]
fn observer_uses_saturated_input_v() {
let mut obs = make_obs_aw(0.0, 1.0, 1.0, 0.5, 1.0, 1.0, -1.0, 1.0, 0.01);
let v = obs.update(10.0, 0.0).unwrap();
assert!((v - 1.0).abs() < 1e-12, "expected v=1.0, got {v}");
assert!(obs.is_saturated());
let expected = 0.01_f64;
assert!(
(obs.state_estimate()[0] - expected).abs() < 1e-12,
"x_hat={} expected={expected}",
obs.state_estimate()[0]
);
}
#[test]
fn observer_saturation_flag() {
let mut obs = make_obs_aw(0.0, 1.0, 1.0, 0.5, 1.0, 1.0, -10.0, 10.0, 0.01);
obs.update(0.5, 0.0).unwrap();
assert!(!obs.is_saturated(), "should not be saturated");
obs.reset([0.0]);
obs.update(100.0, 0.0).unwrap();
assert!(obs.is_saturated(), "should be saturated");
}
#[test]
fn observer_invalid_params() {
let res = ObserverAntiWindup::<f64, 1>::new(
[[0.0]],
[1.0],
[1.0],
[0.5],
[1.0],
1.0,
5.0,
3.0,
0.01,
);
assert!(
matches!(res, Err(AntiWindupError::InvalidParameter)),
"expected InvalidParameter for u_min >= u_max"
);
let res2 = ObserverAntiWindup::<f64, 1>::new(
[[0.0]],
[1.0],
[1.0],
[0.5],
[1.0],
1.0,
-10.0,
10.0,
0.0,
);
assert!(
matches!(res2, Err(AntiWindupError::InvalidParameter)),
"expected InvalidParameter for dt <= 0"
);
}
#[test]
fn observer_reset() {
let mut obs = make_obs_aw(0.0, 1.0, 1.0, 0.5, 1.0, 1.0, -10.0, 10.0, 0.01);
for _ in 0..20 {
obs.update(5.0, 0.0).unwrap();
}
obs.reset([core::f64::consts::PI]);
assert!(
(obs.state_estimate()[0] - core::f64::consts::PI).abs() < 1e-12,
"state after reset: {}",
obs.state_estimate()[0]
);
assert!(!obs.is_saturated());
}
#[test]
fn observer_unsaturated_tracks_output() {
let mut obs = make_obs_aw(-1.0, 1.0, 1.0, 2.0, 2.0, 3.0, -100.0, 100.0, 0.001);
for _ in 0..200 {
let v = obs.update(1.0, 0.0).unwrap();
assert!(v.abs() < 100.0, "output grew unbounded: {v}");
}
assert!(
obs.state_estimate()[0].is_finite(),
"x_hat diverged: {}",
obs.state_estimate()[0]
);
}
}