use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DobError {
NonPositiveTau,
NonPositiveDt,
ZeroBn,
InvalidOrder,
}
impl core::fmt::Display for DobError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
DobError::NonPositiveTau => f.write_str("Q-filter time constant tau must be positive"),
DobError::NonPositiveDt => f.write_str("sampling period dt must be positive"),
DobError::ZeroBn => f.write_str("nominal plant gain b_n must be non-zero"),
DobError::InvalidOrder => f.write_str("Q-filter order must be 1 or 2"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DisturbanceObserverConfig<S> {
pub a_n: S,
pub b_n: S,
pub tau: S,
pub order: u8,
pub dt: S,
}
#[derive(Debug, Clone, Copy)]
pub struct DisturbanceObserver<S: ControlScalar> {
q1: S,
q2: S,
y_prev: S,
a_n: S,
b_n: S,
tau: S,
order: u8,
dt: S,
d_hat: S,
initialized: bool,
}
impl<S: ControlScalar> DisturbanceObserver<S> {
pub fn new(config: DisturbanceObserverConfig<S>) -> Result<Self, DobError> {
if config.tau <= S::ZERO {
return Err(DobError::NonPositiveTau);
}
if config.dt <= S::ZERO {
return Err(DobError::NonPositiveDt);
}
if config.b_n == S::ZERO {
return Err(DobError::ZeroBn);
}
if config.order != 1 && config.order != 2 {
return Err(DobError::InvalidOrder);
}
Ok(Self {
q1: S::ZERO,
q2: S::ZERO,
y_prev: S::ZERO,
a_n: config.a_n,
b_n: config.b_n,
tau: config.tau,
order: config.order,
dt: config.dt,
d_hat: S::ZERO,
initialized: false,
})
}
pub fn update(&mut self, u: S, y: S) -> Result<S, DobError> {
if !self.initialized {
self.y_prev = y;
self.initialized = true;
return Ok(self.d_hat);
}
let y_dot = (y - self.y_prev) / self.dt;
let inv_plant_out = (y_dot + self.a_n * y) / self.b_n;
self.y_prev = y;
let e = inv_plant_out - u;
let alpha = self.dt / self.tau;
self.q1 = self.q1 + alpha * (e - self.q1);
self.d_hat = if self.order == 2 {
self.q2 = self.q2 + alpha * (self.q1 - self.q2);
self.q2
} else {
self.q1
};
Ok(self.d_hat)
}
#[inline]
pub fn disturbance_estimate(&self) -> S {
self.d_hat
}
pub fn reset(&mut self) {
self.q1 = S::ZERO;
self.q2 = S::ZERO;
self.y_prev = S::ZERO;
self.d_hat = S::ZERO;
self.initialized = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg_order1() -> DisturbanceObserverConfig<f64> {
DisturbanceObserverConfig {
a_n: 1.0,
b_n: 2.0,
tau: 0.05,
order: 1,
dt: 0.005,
}
}
fn cfg_order2() -> DisturbanceObserverConfig<f64> {
DisturbanceObserverConfig {
a_n: 1.0,
b_n: 2.0,
tau: 0.05,
order: 2,
dt: 0.005,
}
}
fn simulate_nominal(a_n: f64, b_n: f64, dt: f64, u: f64, y0: f64, steps: usize) -> Vec<f64> {
let mut y = y0;
let mut ys = Vec::with_capacity(steps + 1);
ys.push(y);
for _ in 0..steps {
y = (1.0 - a_n * dt) * y + b_n * dt * u;
ys.push(y);
}
ys
}
fn simulate_disturbed(
a_n: f64,
b_n: f64,
dt: f64,
u: f64,
d: f64,
y0: f64,
steps: usize,
) -> Vec<f64> {
let mut y = y0;
let mut ys = Vec::with_capacity(steps + 1);
ys.push(y);
for _ in 0..steps {
y = (1.0 - a_n * dt) * y + b_n * dt * (u + d);
ys.push(y);
}
ys
}
#[test]
fn test_zero_disturbance_order1() {
let cfg = cfg_order1();
let a_n = cfg.a_n;
let b_n = cfg.b_n;
let dt = cfg.dt;
let u = 0.5_f64;
let y0 = 1.0_f64;
let steps = 800usize;
let mut dob = DisturbanceObserver::new(cfg).expect("valid config");
let ys = simulate_nominal(a_n, b_n, dt, u, y0, steps);
let mut d_hat = 0.0_f64;
for &y_k in &ys[..steps] {
d_hat = dob.update(u, y_k).expect("update ok");
}
assert!(
d_hat.abs() < 0.05,
"Expected d_hat ≈ 0 for zero disturbance, got {d_hat}"
);
}
#[test]
fn test_step_disturbance_order1() {
let cfg = cfg_order1();
let a_n = cfg.a_n;
let b_n = cfg.b_n;
let dt = cfg.dt;
let u = 0.5_f64;
let d = 1.0_f64;
let y0 = 0.0_f64;
let steps = 800usize;
let mut dob = DisturbanceObserver::new(cfg).expect("valid config");
let ys = simulate_disturbed(a_n, b_n, dt, u, d, y0, steps);
let mut d_hat = 0.0_f64;
for &y_k in &ys[..steps] {
d_hat = dob.update(u, y_k).expect("update ok");
}
let err = (d_hat - d).abs();
assert!(
err < 0.25 * d,
"Expected d_hat ≈ {d}, got {d_hat} (err={err})"
);
}
#[test]
fn test_zero_disturbance_order2() {
let cfg = cfg_order2();
let a_n = cfg.a_n;
let b_n = cfg.b_n;
let dt = cfg.dt;
let u = 0.3_f64;
let y0 = 0.5_f64;
let steps = 1000usize;
let mut dob = DisturbanceObserver::new(cfg).expect("valid config");
let ys = simulate_nominal(a_n, b_n, dt, u, y0, steps);
let mut d_hat = 0.0_f64;
for &y_k in &ys[..steps] {
d_hat = dob.update(u, y_k).expect("update ok");
}
assert!(
d_hat.abs() < 0.05,
"Expected d_hat ≈ 0 for zero disturbance (order 2), got {d_hat}"
);
}
#[test]
fn test_step_disturbance_order2() {
let cfg = cfg_order2();
let a_n = cfg.a_n;
let b_n = cfg.b_n;
let dt = cfg.dt;
let u = 0.0_f64;
let d = 2.0_f64;
let y0 = 0.0_f64;
let steps = 1200usize;
let mut dob = DisturbanceObserver::new(cfg).expect("valid config");
let ys = simulate_disturbed(a_n, b_n, dt, u, d, y0, steps);
let mut d_hat = 0.0_f64;
for &y_k in &ys[..steps] {
d_hat = dob.update(u, y_k).expect("update ok");
}
let err = (d_hat - d).abs();
assert!(
err < 0.5 * d,
"Expected d_hat ≈ {d} (order 2), got {d_hat} (err={err})"
);
}
#[test]
fn test_bandwidth_effect() {
let a_n = 1.0_f64;
let b_n = 2.0_f64;
let dt = 0.005_f64;
let u = 0.0_f64;
let d = 1.0_f64;
let y0 = 0.0_f64;
let steps = 200usize;
let ys = simulate_disturbed(a_n, b_n, dt, u, d, y0, steps);
let mut dob_fast = DisturbanceObserver::new(DisturbanceObserverConfig {
a_n,
b_n,
tau: 0.01,
order: 1,
dt,
})
.expect("valid fast config");
let mut dob_slow = DisturbanceObserver::new(DisturbanceObserverConfig {
a_n,
b_n,
tau: 0.1,
order: 1,
dt,
})
.expect("valid slow config");
let mut d_fast = 0.0_f64;
let mut d_slow = 0.0_f64;
for &y_k in &ys[..steps] {
d_fast = dob_fast.update(u, y_k).expect("fast update ok");
d_slow = dob_slow.update(u, y_k).expect("slow update ok");
}
let err_fast = (d_fast - d).abs();
let err_slow = (d_slow - d).abs();
assert!(
err_fast < err_slow,
"Fast DOB (τ=0.01) err={err_fast} should be < slow DOB (τ=0.1) err={err_slow}"
);
}
#[test]
fn test_invalid_config_tau() {
let mut cfg = cfg_order1();
cfg.tau = -0.1;
assert_eq!(
DisturbanceObserver::new(cfg).unwrap_err(),
DobError::NonPositiveTau
);
cfg.tau = 0.0;
assert_eq!(
DisturbanceObserver::new(cfg).unwrap_err(),
DobError::NonPositiveTau
);
}
#[test]
fn test_invalid_config_dt() {
let mut cfg = cfg_order1();
cfg.dt = 0.0;
assert_eq!(
DisturbanceObserver::new(cfg).unwrap_err(),
DobError::NonPositiveDt
);
}
#[test]
fn test_invalid_config_bn_zero() {
let mut cfg = cfg_order1();
cfg.b_n = 0.0;
assert_eq!(DisturbanceObserver::new(cfg).unwrap_err(), DobError::ZeroBn);
}
#[test]
fn test_invalid_config_order() {
let mut cfg = cfg_order1();
cfg.order = 3;
assert_eq!(
DisturbanceObserver::new(cfg).unwrap_err(),
DobError::InvalidOrder
);
cfg.order = 0;
assert_eq!(
DisturbanceObserver::new(cfg).unwrap_err(),
DobError::InvalidOrder
);
}
#[test]
fn test_estimate_accessor() {
let cfg = cfg_order1();
let mut dob = DisturbanceObserver::new(cfg).expect("valid config");
let ret = dob.update(0.5, 0.3).expect("update ok");
assert_eq!(ret, dob.disturbance_estimate());
}
#[test]
fn test_reset() {
let cfg = cfg_order1();
let a_n = cfg.a_n;
let b_n = cfg.b_n;
let dt = cfg.dt;
let mut dob = DisturbanceObserver::new(cfg).expect("valid config");
let ys = simulate_disturbed(a_n, b_n, dt, 0.0, 3.0, 0.0, 300);
for &y_k in &ys[..300] {
let _ = dob.update(0.0, y_k);
}
dob.reset();
assert_eq!(dob.disturbance_estimate(), 0.0);
let d = dob.update(0.0, 0.0).expect("post-reset update ok");
assert_eq!(d, 0.0);
}
}