use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct PidGains {
pub kp: f64,
pub ki: f64,
pub kd: f64,
}
impl Default for PidGains {
fn default() -> Self {
Self {
kp: 1.0,
ki: 0.0,
kd: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct PidController {
gains: PidGains,
integral: f64,
previous_error: Option<f64>,
output_limits: (f64, f64),
integral_limits: (f64, f64),
}
impl PidController {
pub fn new(gains: PidGains, output_limits: (f64, f64)) -> Self {
Self {
gains,
integral: 0.0,
previous_error: None,
output_limits,
integral_limits: output_limits,
}
}
pub fn with_integral_limits(mut self, integral_limits: (f64, f64)) -> Self {
self.integral_limits = integral_limits;
self
}
pub fn update(&mut self, setpoint: f64, measurement: f64, dt: f64) -> f64 {
let error = setpoint - measurement;
let derivative = if dt > 0.0 {
self.integral = (self.integral + error * dt).clamp(self.integral_limits.0, self.integral_limits.1);
let derivative = self
.previous_error
.map(|previous| (error - previous) / dt)
.unwrap_or(0.0);
self.previous_error = Some(error);
derivative
} else {
0.0
};
let output = self.gains.kp * error + self.gains.ki * self.integral + self.gains.kd * derivative;
output.clamp(self.output_limits.0, self.output_limits.1)
}
pub fn reset(&mut self) {
self.integral = 0.0;
self.previous_error = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn proportional_only_tracks_error() {
let gains = PidGains {
kp: 2.0,
ki: 0.0,
kd: 0.0,
};
let mut pid = PidController::new(gains, (-100.0, 100.0));
let output = pid.update(10.0, 4.0, 0.1);
assert_eq!(output, 12.0);
}
#[test]
fn integral_accumulates_error_over_time() {
let gains = PidGains {
kp: 0.0,
ki: 1.0,
kd: 0.0,
};
let mut pid = PidController::new(gains, (-100.0, 100.0));
let first = pid.update(1.0, 0.0, 1.0);
let second = pid.update(1.0, 0.0, 1.0);
assert_eq!(first, 1.0);
assert_eq!(second, 2.0);
}
#[test]
fn derivative_reacts_to_rate_of_change_and_is_zero_on_first_call() {
let gains = PidGains {
kp: 0.0,
ki: 0.0,
kd: 1.0,
};
let mut pid = PidController::new(gains, (-100.0, 100.0));
let first = pid.update(10.0, 0.0, 1.0);
assert_eq!(first, 0.0);
let second = pid.update(10.0, 5.0, 1.0);
assert_eq!(second, -5.0);
}
#[test]
fn output_is_clamped_to_configured_limits() {
let gains = PidGains {
kp: 100.0,
ki: 0.0,
kd: 0.0,
};
let mut pid = PidController::new(gains, (-1.0, 1.0));
assert_eq!(pid.update(10.0, 0.0, 1.0), 1.0);
assert_eq!(pid.update(-10.0, 0.0, 1.0), -1.0);
}
#[test]
fn integral_is_clamped_to_prevent_windup() {
let gains = PidGains {
kp: 0.0,
ki: 1.0,
kd: 0.0,
};
let mut pid = PidController::new(gains, (-100.0, 100.0)).with_integral_limits((-2.0, 2.0));
for _ in 0..5 {
pid.update(5.0, 0.0, 1.0);
}
assert_eq!(pid.update(5.0, 0.0, 1.0), 2.0);
}
#[test]
fn non_positive_dt_skips_integral_and_derivative_terms() {
let gains = PidGains {
kp: 1.0,
ki: 1.0,
kd: 1.0,
};
let mut pid = PidController::new(gains, (-100.0, 100.0));
pid.update(10.0, 0.0, 1.0);
let output = pid.update(10.0, 0.0, 0.0);
assert_eq!(output, 20.0);
}
#[test]
fn reset_clears_integral_and_derivative_state() {
let gains = PidGains {
kp: 0.0,
ki: 1.0,
kd: 1.0,
};
let mut pid = PidController::new(gains, (-100.0, 100.0));
pid.update(5.0, 0.0, 1.0);
pid.reset();
let output = pid.update(5.0, 0.0, 1.0);
assert_eq!(output, 5.0);
}
#[test]
fn converges_to_setpoint_over_repeated_ticks() {
let gains = PidGains {
kp: 0.6,
ki: 0.3,
kd: 0.05,
};
let mut pid = PidController::new(gains, (-10.0, 10.0));
let mut measurement = 0.0_f64;
let setpoint = 5.0;
let dt = 0.1;
for _ in 0..200 {
let output = pid.update(setpoint, measurement, dt);
measurement += output * dt;
}
assert!(
(measurement - setpoint).abs() < 0.1,
"expected convergence near {setpoint}, got {measurement}"
);
}
}