use crate::arithmetic::Fx;
pub struct Pid {
kp: Fx,
ki: Fx,
kd: Fx,
output_min: Fx,
output_max: Fx,
integral: Fx,
prev_error: Option<Fx>,
}
impl Pid {
pub fn new(kp: Fx, ki: Fx, kd: Fx, output_min: Fx, output_max: Fx) -> Self {
Self {
kp,
ki,
kd,
output_min,
output_max,
integral: Fx::ZERO,
prev_error: None,
}
}
pub fn step(&mut self, error: Fx, dt: Fx) -> Fx {
let candidate_integral = self.integral + error * dt;
let derivative = match self.prev_error {
Some(prev) if !dt.is_zero() => (error - prev).div(dt),
_ => Fx::ZERO,
};
self.prev_error = Some(error);
let unclamped = self.kp * error + self.ki * candidate_integral + self.kd * derivative;
let clamped = self.clamp(unclamped);
if clamped == unclamped {
self.integral = candidate_integral;
}
clamped
}
fn clamp(&self, x: Fx) -> Fx {
if x < self.output_min {
self.output_min
} else if x > self.output_max {
self.output_max
} else {
x
}
}
pub fn reset(&mut self) {
self.integral = Fx::ZERO;
self.prev_error = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fx(n: i64, d: i64) -> Fx {
Fx::from_ratio(n, d)
}
#[test]
fn proportional_only_scales_the_error() {
let mut pid = Pid::new(
fx(1, 2),
Fx::ZERO,
Fx::ZERO,
Fx::from_int(-100),
Fx::from_int(100),
);
let out = pid.step(Fx::from_int(10), Fx::ONE);
assert_eq!(out, Fx::from_int(5));
}
#[test]
fn integral_accumulates_across_steps() {
let mut pid = Pid::new(
Fx::ZERO,
fx(1, 1),
Fx::ZERO,
Fx::from_int(-100),
Fx::from_int(100),
);
let first = pid.step(Fx::from_int(1), Fx::ONE);
let second = pid.step(Fx::from_int(1), Fx::ONE);
assert_eq!(first, Fx::from_int(1));
assert_eq!(second, Fx::from_int(2));
}
#[test]
fn derivative_reacts_to_the_change_in_error() {
let mut pid = Pid::new(
Fx::ZERO,
Fx::ZERO,
Fx::ONE,
Fx::from_int(-100),
Fx::from_int(100),
);
let _ = pid.step(Fx::from_int(0), Fx::ONE);
let second = pid.step(Fx::from_int(5), Fx::ONE);
assert_eq!(second, Fx::from_int(5));
}
#[test]
fn first_step_has_no_derivative_term() {
let mut pid = Pid::new(
Fx::ZERO,
Fx::ZERO,
Fx::ONE,
Fx::from_int(-100),
Fx::from_int(100),
);
let out = pid.step(Fx::from_int(7), Fx::ONE);
assert_eq!(out, Fx::ZERO);
}
#[test]
fn output_clamps_to_bounds() {
let mut pid = Pid::new(
Fx::from_int(10),
Fx::ZERO,
Fx::ZERO,
Fx::ZERO,
Fx::from_int(1),
);
let out = pid.step(Fx::from_int(5), Fx::ONE);
assert_eq!(out, Fx::from_int(1));
let out_low = pid.step(Fx::from_int(-5), Fx::ONE);
assert_eq!(out_low, Fx::ZERO);
}
#[test]
fn saturated_integral_does_not_wind_up_further() {
let mut pid = Pid::new(
Fx::from_int(10),
Fx::ONE,
Fx::ZERO,
Fx::ZERO,
Fx::from_int(1),
);
pid.step(Fx::from_int(5), Fx::ONE);
pid.step(Fx::from_int(5), Fx::ONE);
pid.step(Fx::from_int(5), Fx::ONE);
assert_eq!(
pid.integral,
Fx::ZERO,
"integral must not accumulate while saturated"
);
}
#[test]
fn reset_clears_integral_and_derivative_history() {
let mut pid = Pid::new(
Fx::ZERO,
Fx::ONE,
Fx::ONE,
Fx::from_int(-100),
Fx::from_int(100),
);
pid.step(Fx::from_int(3), Fx::ONE);
pid.reset();
let out = pid.step(Fx::from_int(3), Fx::ONE);
assert_eq!(out, Fx::from_int(3));
}
#[test]
fn zero_dt_skips_the_derivative_term_without_dividing_by_zero() {
let mut pid = Pid::new(
Fx::ZERO,
Fx::ZERO,
Fx::ONE,
Fx::from_int(-100),
Fx::from_int(100),
);
pid.step(Fx::from_int(1), Fx::ONE);
let out = pid.step(Fx::from_int(9), Fx::ZERO);
assert_eq!(out, Fx::ZERO);
}
}