use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, PartialEq)]
pub enum TwoDofError {
InvalidParameter,
}
impl core::fmt::Display for TwoDofError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Invalid 2-DOF controller parameter")
}
}
#[derive(Debug, Clone)]
pub struct TwoDofController<S: ControlScalar> {
kp: S,
ki: S,
kd: S,
b: S,
c: S,
integral: S,
y_prev: S,
r_prev: S,
dt: S,
u_min: S,
u_max: S,
}
impl<S: ControlScalar> TwoDofController<S> {
#[allow(clippy::too_many_arguments)]
pub fn new(
kp: S,
ki: S,
kd: S,
b: S,
c: S,
u_min: S,
u_max: S,
dt: S,
) -> Result<Self, TwoDofError> {
if kp <= S::ZERO {
return Err(TwoDofError::InvalidParameter);
}
if ki < S::ZERO || kd < S::ZERO {
return Err(TwoDofError::InvalidParameter);
}
if b < S::ZERO || b > S::ONE {
return Err(TwoDofError::InvalidParameter);
}
if c < S::ZERO || c > S::ONE {
return Err(TwoDofError::InvalidParameter);
}
if dt <= S::ZERO {
return Err(TwoDofError::InvalidParameter);
}
if u_min >= u_max {
return Err(TwoDofError::InvalidParameter);
}
Ok(Self {
kp,
ki,
kd,
b,
c,
integral: S::ZERO,
y_prev: S::ZERO,
r_prev: S::ZERO,
dt,
u_min,
u_max,
})
}
pub fn update(&mut self, r: S, y: S) -> Result<S, TwoDofError> {
let error = r - y;
let p_term = self.kp * (self.b * r - y);
let r_dot = (r - self.r_prev) / self.dt;
let y_dot = (y - self.y_prev) / self.dt;
let d_term = self.kd * (self.c * r_dot - y_dot);
let u_pd = p_term + d_term;
let u_unsat = u_pd + self.ki * self.integral;
let u = u_unsat.clamp_val(self.u_min, self.u_max);
let saturated_high = u_unsat > self.u_max;
let saturated_low = u_unsat < self.u_min;
let should_integrate = !saturated_high && !saturated_low
|| (saturated_high && error < S::ZERO)
|| (saturated_low && error > S::ZERO);
if should_integrate {
self.integral += error * self.dt;
}
self.y_prev = y;
self.r_prev = r;
Ok(u)
}
pub fn integral_state(&self) -> S {
self.integral
}
pub fn reset(&mut self) {
self.integral = S::ZERO;
self.y_prev = S::ZERO;
self.r_prev = S::ZERO;
}
pub fn set_limits(&mut self, u_min: S, u_max: S) -> Result<(), TwoDofError> {
if u_min >= u_max {
return Err(TwoDofError::InvalidParameter);
}
self.u_min = u_min;
self.u_max = u_max;
Ok(())
}
pub fn kp(&self) -> S {
self.kp
}
pub fn ki(&self) -> S {
self.ki
}
pub fn kd(&self) -> S {
self.kd
}
pub fn b(&self) -> S {
self.b
}
pub fn c(&self) -> S {
self.c
}
}
#[derive(Debug, Clone)]
pub struct ReferencePrefilter<S: ControlScalar> {
tau: S,
y: S,
dt: S,
}
impl<S: ControlScalar> ReferencePrefilter<S> {
pub fn new(tau: S, dt: S) -> Result<Self, TwoDofError> {
if tau <= S::ZERO {
return Err(TwoDofError::InvalidParameter);
}
if dt <= S::ZERO {
return Err(TwoDofError::InvalidParameter);
}
Ok(Self {
tau,
y: S::ZERO,
dt,
})
}
pub fn filter(&mut self, r: S) -> S {
let alpha = self.dt / self.tau;
self.y += alpha * (r - self.y);
self.y
}
pub fn reset(&mut self) {
self.y = S::ZERO;
}
pub fn output(&self) -> S {
self.y
}
pub fn tau(&self) -> S {
self.tau
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn b1_is_standard_error_proportional() {
let kp = 3.0_f64;
let mut ctrl = TwoDofController::<f64>::new(
kp, 0.0, 0.0, 1.0, 0.0, -100.0, 100.0, 0.01,
)
.expect("valid params");
let r = 1.0_f64;
let y = 0.4_f64;
let u = ctrl.update(r, y).expect("update ok");
let expected = kp * (r - y);
assert!((u - expected).abs() < 1e-10, "Expected {expected}, got {u}");
}
#[test]
fn b0_no_proportional_on_reference() {
let kp = 2.0_f64;
let mut ctrl = TwoDofController::<f64>::new(
kp, 0.0, 0.0, 0.0, 0.0, -100.0, 100.0, 0.01,
)
.expect("valid params");
let u = ctrl.update(1.0, 0.0).expect("update ok");
assert!(
u.abs() < 1e-10,
"With b=0, y=0, r=1: u should be 0 (no P-kick on reference), got {u}"
);
}
#[test]
fn anti_windup_clamps_output() {
let mut ctrl = TwoDofController::<f64>::new(
1.0, 100.0, 0.0, 1.0, 0.0, -1.0, 1.0, 0.01,
)
.expect("valid params");
for _ in 0..200 {
let u = ctrl.update(10.0, 0.0).expect("update ok");
assert!(
u <= 1.0 + 1e-10,
"Output must not exceed u_max=1.0, got {u}"
);
}
let integral = ctrl.integral_state();
assert!(
integral.abs() < 1000.0,
"Integral should be bounded by anti-windup, got {integral}"
);
}
#[test]
fn prefilter_time_constant() {
let mut pf = ReferencePrefilter::<f64>::new(1.0, 0.01).expect("valid params");
let mut y = 0.0_f64;
for _ in 0..100 {
y = pf.filter(1.0);
}
let expected_approx = 1.0 - (-1.0_f64).exp(); assert!(
(y - expected_approx).abs() < 0.01,
"After 1 time constant, prefilter output should be ≈{expected_approx:.4}, got {y:.4}"
);
}
#[test]
fn reset_zeroes_states() {
let mut ctrl = TwoDofController::<f64>::new(1.0, 1.0, 0.1, 1.0, 0.0, -10.0, 10.0, 0.01)
.expect("valid params");
for _ in 0..50 {
let _ = ctrl.update(1.0, 0.5).expect("update ok");
}
assert!(
ctrl.integral_state().abs() > 1e-6,
"Integral should be nonzero before reset"
);
ctrl.reset();
assert_eq!(
ctrl.integral_state(),
0.0,
"Integral should be zero after reset"
);
}
#[test]
fn prefilter_reset() {
let mut pf = ReferencePrefilter::<f64>::new(1.0, 0.01).expect("valid params");
for _ in 0..50 {
pf.filter(1.0);
}
assert!(pf.output() > 0.1, "Output should be nonzero before reset");
pf.reset();
assert_eq!(pf.output(), 0.0, "Output should be zero after reset");
}
#[test]
fn step_tracking_converges() {
let mut ctrl = TwoDofController::<f64>::new(5.0, 2.0, 0.0, 1.0, 0.0, -50.0, 50.0, 0.01)
.expect("valid params");
let setpoint = 1.0_f64;
let mut y = 0.0_f64;
for _ in 0..500 {
let u = ctrl.update(setpoint, y).expect("update ok");
y = 0.9 * y + 0.1 * u;
}
assert!(
(y - setpoint).abs() < 0.05,
"System should converge to setpoint, got y={y:.4}"
);
}
#[test]
fn kp_validation() {
let r = TwoDofController::<f64>::new(0.0, 1.0, 0.0, 1.0, 0.0, -1.0, 1.0, 0.01);
assert!(
matches!(r, Err(TwoDofError::InvalidParameter)),
"kp=0.0 should be rejected"
);
let r2 = TwoDofController::<f64>::new(-1.0, 1.0, 0.0, 1.0, 0.0, -1.0, 1.0, 0.01);
assert!(
matches!(r2, Err(TwoDofError::InvalidParameter)),
"kp<0 should be rejected"
);
}
#[test]
fn bc_range_validation() {
let r1 = TwoDofController::<f64>::new(1.0, 0.0, 0.0, 1.5, 0.0, -1.0, 1.0, 0.01);
assert!(
matches!(r1, Err(TwoDofError::InvalidParameter)),
"b>1 should be rejected"
);
let r2 = TwoDofController::<f64>::new(1.0, 0.0, 0.0, 1.0, -0.1, -1.0, 1.0, 0.01);
assert!(
matches!(r2, Err(TwoDofError::InvalidParameter)),
"c<0 should be rejected"
);
}
#[test]
fn limit_validation() {
let r = TwoDofController::<f64>::new(1.0, 0.0, 0.0, 1.0, 0.0, 5.0, 1.0, 0.01);
assert!(
matches!(r, Err(TwoDofError::InvalidParameter)),
"u_min>=u_max should be rejected"
);
}
}