use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NdobError {
NonPositiveGain,
NonPositiveDt,
}
impl core::fmt::Display for NdobError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
NdobError::NonPositiveGain => f.write_str("observer gain l must be strictly positive"),
NdobError::NonPositiveDt => f.write_str("sampling period dt must be strictly positive"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct NonlinearDob<S: ControlScalar> {
z: S,
gain: S,
dt: S,
d_hat: S,
}
impl<S: ControlScalar> NonlinearDob<S> {
pub fn new(gain: S, dt: S, x0: S) -> Result<Self, NdobError> {
if gain <= S::ZERO {
return Err(NdobError::NonPositiveGain);
}
if dt <= S::ZERO {
return Err(NdobError::NonPositiveDt);
}
let z = -(gain * x0);
Ok(Self {
z,
gain,
dt,
d_hat: S::ZERO,
})
}
pub fn update(&mut self, x: S, u: S, f_x: S, g_x: S) -> Result<S, NdobError> {
let l = self.gain;
let z_dot = -(l * g_x * self.z) - l * (g_x * l * x + f_x + g_x * u);
self.z += z_dot * self.dt;
self.d_hat = self.z + l * x;
Ok(self.d_hat)
}
#[inline]
pub fn estimate(&self) -> S {
self.d_hat
}
pub fn reset(&mut self, x: S) {
self.z = -(self.gain * x);
self.d_hat = S::ZERO;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zero_disturbance() {
let mut obs = NonlinearDob::new(2.0_f64, 0.005, 0.0).expect("valid");
let x = 0.0_f64;
let u = 0.0_f64;
let f_x = 0.0_f64;
let g_x = 1.0_f64;
let mut d_hat = 0.0_f64;
for _ in 0..400 {
d_hat = obs.update(x, u, f_x, g_x).expect("update ok");
}
assert!(
d_hat.abs() < 0.05,
"Expected d_hat ≈ 0 for zero disturbance, got {d_hat}"
);
}
#[test]
fn test_constant_disturbance() {
let gain = 3.0_f64;
let dt = 0.005_f64;
let d_true = 2.0_f64;
let f_x = 0.0_f64;
let g_x = 1.0_f64;
let u = 0.0_f64;
let mut obs = NonlinearDob::new(gain, dt, 0.0_f64).expect("valid");
let mut x = 0.0_f64;
let mut d_hat = 0.0_f64;
for _ in 0..600 {
x += dt * (f_x + g_x * (u + d_true));
d_hat = obs.update(x, u, f_x, g_x).expect("update ok");
}
let err = (d_hat - d_true).abs();
assert!(
err < 0.5 * d_true,
"Expected d_hat ≈ {d_true}, got {d_hat} (err={err})"
);
}
#[test]
fn test_negative_gain_error() {
let result = NonlinearDob::<f64>::new(-1.0, 0.01, 0.0);
assert_eq!(result.unwrap_err(), NdobError::NonPositiveGain);
}
#[test]
fn test_nonpositive_dt_error() {
let result = NonlinearDob::<f64>::new(1.0, 0.0, 0.0);
assert_eq!(result.unwrap_err(), NdobError::NonPositiveDt);
}
#[test]
fn test_convergence_speed() {
let dt = 0.005_f64;
let d_true = 1.0_f64;
let f_x = 0.0_f64;
let g_x = 1.0_f64;
let u = 0.0_f64;
let steps = 150usize;
let mut obs_slow = NonlinearDob::new(1.0_f64, dt, 0.0).expect("slow valid");
let mut obs_fast = NonlinearDob::new(5.0_f64, dt, 0.0).expect("fast valid");
let mut x_slow = 0.0_f64;
let mut x_fast = 0.0_f64;
let mut d_slow = 0.0_f64;
let mut d_fast = 0.0_f64;
for _ in 0..steps {
x_slow += dt * (f_x + g_x * (u + d_true));
x_fast += dt * (f_x + g_x * (u + d_true));
d_slow = obs_slow.update(x_slow, u, f_x, g_x).expect("slow update");
d_fast = obs_fast.update(x_fast, u, f_x, g_x).expect("fast update");
}
let err_slow = (d_slow - d_true).abs();
let err_fast = (d_fast - d_true).abs();
assert!(
err_fast < err_slow,
"Higher-gain observer should converge faster: err_fast={err_fast} err_slow={err_slow}"
);
}
#[test]
fn test_estimate_accessor() {
let mut obs = NonlinearDob::new(2.0_f64, 0.01, 0.0).expect("valid");
let ret = obs.update(0.5, 0.0, 0.0, 1.0).expect("update ok");
assert_eq!(ret, obs.estimate());
}
#[test]
fn test_reset() {
let dt = 0.005_f64;
let mut obs = NonlinearDob::new(2.0, dt, 0.0).expect("valid");
let mut x = 0.0_f64;
for _ in 0..200 {
x += dt * 1.0;
let _ = obs.update(x, 0.0, 0.0, 1.0);
}
obs.reset(0.0);
assert_eq!(obs.estimate(), 0.0);
}
}