use crate::antiwindup::aw_compensator::AntiWindupError;
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct ConditioningController<S: ControlScalar> {
kp: S,
ki: S,
ka: S,
integrator: S,
u_min: S,
u_max: S,
dt: S,
}
impl<S: ControlScalar> ConditioningController<S> {
pub fn new(kp: S, ki: S, ka: 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 {
return Err(AntiWindupError::InvalidParameter);
}
if ka <= S::ZERO {
return Err(AntiWindupError::InvalidParameter);
}
Ok(Self {
kp,
ki,
ka,
integrator: S::ZERO,
u_min,
u_max,
dt,
})
}
pub fn update(&mut self, r: S, y: S) -> Result<S, AntiWindupError> {
let e = r - y;
let u_unsat = self.kp * e + self.integrator;
let v = u_unsat.clamp_val(self.u_min, self.u_max);
let sat_error = v - u_unsat;
self.integrator += self.dt * (self.ki * e + self.ka * sat_error);
Ok(v)
}
pub fn reset(&mut self) {
self.integrator = S::ZERO;
}
#[inline]
pub fn integrator(&self) -> S {
self.integrator
}
}
#[derive(Debug, Clone, Copy)]
pub struct TrackingAntiWindup<S: ControlScalar> {
kp: S,
ki: S,
kd: S,
integrator: S,
y_prev: S,
u_min: S,
u_max: S,
tt: S,
dt: S,
}
impl<S: ControlScalar> TrackingAntiWindup<S> {
pub fn new(
kp: S,
ki: S,
kd: S,
tt: 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 tt <= S::ZERO {
return Err(AntiWindupError::InvalidParameter);
}
if kp <= S::ZERO {
return Err(AntiWindupError::InvalidParameter);
}
if ki < S::ZERO || kd < S::ZERO {
return Err(AntiWindupError::InvalidParameter);
}
Ok(Self {
kp,
ki,
kd,
integrator: S::ZERO,
y_prev: S::ZERO,
u_min,
u_max,
tt,
dt,
})
}
pub fn update(&mut self, r: S, y: S) -> Result<S, AntiWindupError> {
let e = r - y;
let dy = y - self.y_prev;
let deriv = if self.dt > S::ZERO {
self.kd * dy / self.dt
} else {
S::ZERO
};
let u_unsat = self.kp * e + self.integrator - deriv;
let v = u_unsat.clamp_val(self.u_min, self.u_max);
let sat_error = v - u_unsat;
self.integrator += self.dt * (self.ki * e + sat_error / self.tt);
self.y_prev = y;
Ok(v)
}
pub fn reset(&mut self) {
self.integrator = S::ZERO;
self.y_prev = S::ZERO;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn conditioning_unsaturated_integrator_grows() {
let mut ctrl =
ConditioningController::<f64>::new(1.0, 1.0, 1.0, -100.0, 100.0, 0.01).unwrap();
let v0 = ctrl.update(1.0, 0.0).unwrap();
let i0 = ctrl.integrator();
let _v1 = ctrl.update(1.0, 0.0).unwrap();
let i1 = ctrl.integrator();
assert!(v0.abs() < 100.0, "should not be saturated: {v0}");
assert!(i1 > i0, "integrator should grow: {i0} → {i1}");
}
#[test]
fn conditioning_saturated_back_calc_bounds_integrator() {
let mut ctrl = ConditioningController::<f64>::new(1.0, 1.0, 2.0, -1.0, 1.0, 0.01).unwrap();
for _ in 0..500 {
let _ = ctrl.update(100.0, 0.0).unwrap();
}
assert!(
ctrl.integrator().abs() < 100.0,
"integrator not bounded: {}",
ctrl.integrator()
);
}
#[test]
fn conditioning_ka_must_be_positive() {
let res = ConditioningController::<f64>::new(1.0, 1.0, 0.0, -10.0, 10.0, 0.01);
assert!(
matches!(res, Err(AntiWindupError::InvalidParameter)),
"expected InvalidParameter for ka=0"
);
let res2 = ConditioningController::<f64>::new(1.0, 1.0, -1.0, -10.0, 10.0, 0.01);
assert!(
matches!(res2, Err(AntiWindupError::InvalidParameter)),
"expected InvalidParameter for ka<0"
);
}
#[test]
fn conditioning_reset() {
let mut ctrl =
ConditioningController::<f64>::new(1.0, 1.0, 1.0, -10.0, 10.0, 0.01).unwrap();
for _ in 0..20 {
let _ = ctrl.update(5.0, 0.0).unwrap();
}
ctrl.reset();
assert_eq!(ctrl.integrator(), 0.0);
}
#[test]
fn conditioning_invalid_limits() {
let res = ConditioningController::<f64>::new(1.0, 1.0, 1.0, 5.0, 3.0, 0.01);
assert!(
matches!(res, Err(AntiWindupError::InvalidParameter)),
"expected InvalidParameter for u_min > u_max"
);
}
#[test]
fn tracking_aw_saturated_bounded() {
let mut ctrl = TrackingAntiWindup::<f64>::new(1.0, 1.0, 0.0, 0.1, -1.0, 1.0, 0.01).unwrap();
for _ in 0..500 {
let _ = ctrl.update(50.0, 0.0).unwrap();
}
assert!(
ctrl.integrator.abs() < 200.0,
"integrator not bounded: {}",
ctrl.integrator
);
}
#[test]
fn tracking_aw_reset() {
let mut ctrl =
TrackingAntiWindup::<f64>::new(1.0, 1.0, 0.0, 0.5, -10.0, 10.0, 0.01).unwrap();
for _ in 0..20 {
let _ = ctrl.update(5.0, 0.0).unwrap();
}
ctrl.reset();
assert_eq!(ctrl.integrator, 0.0);
assert_eq!(ctrl.y_prev, 0.0);
}
#[test]
fn tracking_aw_invalid_tt() {
let res = TrackingAntiWindup::<f64>::new(1.0, 1.0, 0.0, 0.0, -10.0, 10.0, 0.01);
assert!(
matches!(res, Err(AntiWindupError::InvalidParameter)),
"expected InvalidParameter for tt=0"
);
}
#[test]
fn tracking_aw_output_clamped() {
let mut ctrl =
TrackingAntiWindup::<f64>::new(10.0, 0.0, 0.0, 1.0, -2.0, 2.0, 0.01).unwrap();
let v = ctrl.update(5.0, 0.0).unwrap();
assert!((v - 2.0).abs() < 1e-12, "v={v}");
}
}