use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AntiWindupError {
InvalidParameter,
DimensionMismatch,
}
impl core::fmt::Display for AntiWindupError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
AntiWindupError::InvalidParameter => write!(f, "AntiWindup: invalid parameter"),
AntiWindupError::DimensionMismatch => write!(f, "AntiWindup: dimension mismatch"),
}
}
}
#[derive(Debug, Clone)]
pub struct LinearAntiWindup<S: ControlScalar, const N_C: usize> {
x_c: [S; N_C],
ac: [[S; N_C]; N_C],
bc: [S; N_C],
cc: [S; N_C],
dc: S,
e_aw: [S; N_C],
u_min: S,
u_max: S,
saturated: bool,
dv: S,
}
impl<S: ControlScalar, const N_C: usize> LinearAntiWindup<S, N_C> {
pub fn new(
ac: [[S; N_C]; N_C],
bc: [S; N_C],
cc: [S; N_C],
dc: S,
e_aw: [S; N_C],
u_min: S,
u_max: S,
) -> Result<Self, AntiWindupError> {
if u_min >= u_max {
return Err(AntiWindupError::InvalidParameter);
}
Ok(Self {
x_c: [S::ZERO; N_C],
ac,
bc,
cc,
dc,
e_aw,
u_min,
u_max,
saturated: false,
dv: S::ZERO,
})
}
pub fn update(&mut self, ref_minus_y: S) -> Result<S, AntiWindupError> {
let mut u_lin = self.dc * ref_minus_y;
for i in 0..N_C {
u_lin += self.cc[i] * self.x_c[i];
}
let v = u_lin.clamp_val(self.u_min, self.u_max);
let dv = v - u_lin;
self.dv = dv;
self.saturated = (dv * dv) > S::EPSILON * S::EPSILON;
let mut x_c_next = [S::ZERO; N_C];
#[allow(clippy::needless_range_loop)]
for i in 0..N_C {
let mut ax = S::ZERO;
for j in 0..N_C {
ax += self.ac[i][j] * self.x_c[j];
}
x_c_next[i] = ax + self.bc[i] * ref_minus_y + self.e_aw[i] * dv;
}
self.x_c = x_c_next;
Ok(v)
}
#[inline]
pub fn is_saturated(&self) -> bool {
self.saturated
}
#[inline]
pub fn windup_signal(&self) -> S {
self.dv
}
pub fn reset(&mut self) {
self.x_c = [S::ZERO; N_C];
self.saturated = false;
self.dv = S::ZERO;
}
#[inline]
pub fn controller_state(&self) -> &[S; N_C] {
&self.x_c
}
}
#[derive(Debug, Clone, Copy)]
pub struct SimpleAntiWindup<S: ControlScalar> {
integrator: S,
kp: S,
ki: S,
u_min: S,
u_max: S,
e_aw: S,
dt: S,
}
impl<S: ControlScalar> SimpleAntiWindup<S> {
pub fn new(kp: S, ki: S, e_aw: 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);
}
if kp <= S::ZERO {
return Err(AntiWindupError::InvalidParameter);
}
if ki < S::ZERO || e_aw < S::ZERO {
return Err(AntiWindupError::InvalidParameter);
}
Ok(Self {
integrator: S::ZERO,
kp,
ki,
u_min,
u_max,
e_aw,
dt,
})
}
pub fn update(&mut self, error: S) -> Result<S, AntiWindupError> {
let u_lin = self.kp * error + self.ki * self.integrator;
let v = u_lin.clamp_val(self.u_min, self.u_max);
let dv = v - u_lin;
self.integrator += self.dt * (error + self.e_aw * dv);
Ok(v)
}
pub fn reset(&mut self) {
self.integrator = S::ZERO;
}
#[inline]
pub fn integrator_state(&self) -> S {
self.integrator
}
#[inline]
pub fn integrator(&self) -> S {
self.integrator
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_law_1(a: f64, b: f64, c: f64, d: f64, e_aw: f64) -> LinearAntiWindup<f64, 1> {
LinearAntiWindup::new([[a; 1]; 1], [b; 1], [c; 1], d, [e_aw; 1], -10.0, 10.0).unwrap()
}
#[test]
fn law_unsaturated_no_correction() {
let mut law = make_law_1(1.0, 1.0, 1.0, 0.0, 5.0);
let v = law.update(0.5).unwrap();
assert!((v - 0.0).abs() < 1e-12, "v={v}");
assert!(!law.is_saturated());
assert!((law.windup_signal()).abs() < 1e-12);
}
#[test]
fn law_saturated_correction_applied() {
let mut law_aw =
LinearAntiWindup::<f64, 1>::new([[1.0]], [1.0], [1.0], 0.0, [1.5], -1.0, 1.0).unwrap();
let mut law_no_aw =
LinearAntiWindup::<f64, 1>::new([[1.0]], [1.0], [1.0], 0.0, [0.0], -1.0, 1.0).unwrap();
for _ in 0..30 {
let _ = law_aw.update(5.0).unwrap();
let _ = law_no_aw.update(5.0).unwrap();
}
let state_aw = law_aw.controller_state()[0].abs();
let state_no_aw = law_no_aw.controller_state()[0].abs();
assert!(
state_aw < 10.0,
"AW state should be bounded near fixed point: {state_aw}"
);
assert!(
state_no_aw > 100.0,
"no-AW state should grow large: {state_no_aw}"
);
assert!(
state_aw < state_no_aw,
"AW state ({state_aw}) should be much smaller than no-AW ({state_no_aw})"
);
}
#[test]
fn law_saturation_flag() {
let mut law =
LinearAntiWindup::<f64, 1>::new([[0.0]], [0.0], [1.0], 1.0, [0.0], -1.0, 1.0).unwrap();
let _ = law.update(5.0).unwrap();
assert!(law.is_saturated(), "should be saturated");
let mut law2 =
LinearAntiWindup::<f64, 1>::new([[0.0]], [0.0], [0.0], 1.0, [0.0], -10.0, 10.0)
.unwrap();
let _ = law2.update(0.1).unwrap();
assert!(!law2.is_saturated(), "should not be saturated");
}
#[test]
fn law_zero_eaw_no_correction() {
let mut law_aw =
LinearAntiWindup::<f64, 1>::new([[1.0]], [1.0], [1.0], 0.0, [0.0], -1.0, 1.0).unwrap();
let mut law_plain =
LinearAntiWindup::<f64, 1>::new([[1.0]], [1.0], [1.0], 0.0, [0.0], -1.0, 1.0).unwrap();
for _ in 0..5 {
let va = law_aw.update(2.0).unwrap();
let vb = law_plain.update(2.0).unwrap();
assert!((va - vb).abs() < 1e-12, "va={va} vb={vb}");
}
}
#[test]
fn law_reset_zeroes_state() {
let mut law = make_law_1(1.0, 1.0, 1.0, 0.0, 0.0);
for _ in 0..5 {
let _ = law.update(1.0).unwrap();
}
law.reset();
assert_eq!(law.controller_state()[0], 0.0);
assert!(!law.is_saturated());
assert_eq!(law.windup_signal(), 0.0);
}
#[test]
fn law_high_ref_triggers_saturation() {
let mut law =
LinearAntiWindup::<f64, 1>::new([[0.0]], [0.0], [0.0], 1.0, [0.0], -10.0, 10.0)
.unwrap();
let v = law.update(100.0).unwrap();
assert!((v - 10.0).abs() < 1e-12, "v={v}");
assert!(law.is_saturated());
assert!((law.windup_signal() - (-90.0)).abs() < 1e-10);
}
#[test]
fn simple_aw_unsaturated() {
let mut saw = SimpleAntiWindup::<f64>::new(1.0, 1.0, 0.0, -100.0, 100.0, 0.01).unwrap();
let v = saw.update(1.0).unwrap();
assert!((v - 1.0).abs() < 1e-12, "v={v}");
assert!((saw.integrator_state() - 0.01).abs() < 1e-12);
}
#[test]
fn simple_aw_saturation_limits_integrator() {
let mut saw = SimpleAntiWindup::<f64>::new(1.0, 1.0, 1.0, -1.0, 1.0, 0.01).unwrap();
for _ in 0..200 {
let _ = saw.update(10.0).unwrap();
}
assert!(
saw.integrator_state().abs() < 20.0,
"integrator={}",
saw.integrator_state()
);
}
#[test]
fn simple_aw_reset() {
let mut saw = SimpleAntiWindup::<f64>::new(1.0, 1.0, 0.5, -10.0, 10.0, 0.01).unwrap();
for _ in 0..10 {
let _ = saw.update(5.0).unwrap();
}
saw.reset();
assert_eq!(saw.integrator_state(), 0.0);
}
#[test]
fn law_invalid_limits() {
let res = LinearAntiWindup::<f64, 1>::new([[1.0]], [1.0], [1.0], 0.0, [0.0], 5.0, 5.0);
assert_eq!(res.unwrap_err(), AntiWindupError::InvalidParameter);
}
#[test]
fn simple_aw_invalid_limits() {
let res = SimpleAntiWindup::<f64>::new(1.0, 1.0, 0.0, 5.0, 3.0, 0.01);
assert_eq!(res.unwrap_err(), AntiWindupError::InvalidParameter);
}
}