#![allow(dead_code)]
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct TwoDofPid<S: ControlScalar> {
pub kp: S,
pub ki: S,
pub kd: S,
pub b: S,
pub c: S,
pub output_min: S,
pub output_max: S,
integrator: S,
prev_c_r_minus_y: S,
initialized: bool,
}
impl<S: ControlScalar> TwoDofPid<S> {
pub fn new(kp: S, ki: S, kd: S, b: S, c: S, output_min: S, output_max: S) -> Self {
Self {
kp,
ki,
kd,
b,
c,
output_min,
output_max,
integrator: S::ZERO,
prev_c_r_minus_y: S::ZERO,
initialized: false,
}
}
pub fn update(&mut self, r: S, y: S, dt: S) -> S {
if dt <= S::ZERO {
return S::ZERO;
}
let error = r - y;
let p_term = self.kp * (self.b * r - y);
let c_r_minus_y = self.c * r - y;
let d_term = if self.initialized {
self.kd * (c_r_minus_y - self.prev_c_r_minus_y) / dt
} else {
S::ZERO
};
let u_unlimited = p_term + self.integrator + d_term;
let u_clamped = u_unlimited.clamp_val(self.output_min, self.output_max);
let saturated_high = u_unlimited > self.output_max;
let saturated_low = u_unlimited < self.output_min;
let integrate = !saturated_high || error < S::ZERO;
let integrate = integrate && (!saturated_low || error > S::ZERO);
if integrate {
self.integrator += self.ki * error * dt;
}
self.prev_c_r_minus_y = c_r_minus_y;
self.initialized = true;
u_clamped
}
pub fn reset(&mut self) {
self.integrator = S::ZERO;
self.prev_c_r_minus_y = S::ZERO;
self.initialized = false;
}
pub fn standard(kp: S, ki: S, kd: S, output_min: S, output_max: S) -> Self {
Self::new(kp, ki, kd, S::ONE, S::ONE, output_min, output_max)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn proportional_only_b_one() {
let mut pid = TwoDofPid::<f64>::standard(2.0, 0.0, 0.0, -100.0, 100.0);
let out = pid.update(10.0, 0.0, 0.01);
assert!((out - 20.0).abs() < 1e-12, "out={out}");
}
#[test]
fn b_zero_no_p_kick_on_setpoint_step() {
let mut pid = TwoDofPid::<f64>::new(2.0, 0.0, 0.0, 0.0, 0.0, -100.0, 100.0);
let out = pid.update(100.0, 0.0, 0.01);
assert!(out.abs() < 1e-12, "out={out}");
}
#[test]
fn integrator_accumulates() {
let mut pid = TwoDofPid::<f64>::new(0.0, 1.0, 0.0, 1.0, 1.0, -1000.0, 1000.0);
let dt = 0.1_f64;
let out1 = pid.update(5.0, 0.0, dt);
let out2 = pid.update(5.0, 0.0, dt);
let out3 = pid.update(5.0, 0.0, dt);
assert!(out1.abs() < 1e-12, "out1={out1}");
assert!((out2 - 0.5).abs() < 1e-12, "out2={out2}");
assert!((out3 - 1.0).abs() < 1e-12, "out3={out3}");
}
#[test]
fn output_clamped_to_limits() {
let mut pid = TwoDofPid::<f64>::standard(100.0, 0.0, 0.0, -5.0, 5.0);
let out = pid.update(10.0, 0.0, 0.01);
assert!((out - 5.0).abs() < 1e-12, "out={out}");
}
#[test]
fn reset_clears_state() {
let mut pid = TwoDofPid::<f64>::standard(1.0, 2.0, 0.0, -100.0, 100.0);
for _ in 0..10 {
pid.update(5.0, 0.0, 0.01);
}
pid.reset();
let out = pid.update(0.0, 0.0, 0.01);
assert!(out.abs() < 1e-12, "out after reset={out}");
}
#[test]
fn c_zero_no_d_kick_on_setpoint_step() {
let mut pid = TwoDofPid::<f64>::new(0.0, 0.0, 10.0, 1.0, 0.0, -100.0, 100.0);
pid.update(0.0, 0.0, 0.01); let out = pid.update(100.0, 0.0, 0.01);
assert!(out.abs() < 1e-12, "out={out}");
}
}