Skip to main content

rust_robotics_control/
pid_controller.rs

1//! PID Controller with anti-windup and output clamping.
2use rust_robotics_core::Controller;
3
4/// Configuration for [`PIDController`].
5#[derive(Debug, Clone, Copy)]
6pub struct PIDConfig {
7    /// Proportional gain.
8    pub kp: f64,
9    /// Integral gain.
10    pub ki: f64,
11    /// Derivative gain.
12    pub kd: f64,
13    /// Time step \[s\].
14    pub dt: f64,
15    /// Maximum absolute value of the integral term (anti-windup).
16    pub max_integral: f64,
17    /// Maximum absolute value of the controller output.
18    pub max_output: f64,
19}
20
21impl Default for PIDConfig {
22    fn default() -> Self {
23        Self {
24            kp: 1.0,
25            ki: 0.0,
26            kd: 0.0,
27            dt: 0.01,
28            max_integral: 10.0,
29            max_output: f64::INFINITY,
30        }
31    }
32}
33
34/// Discrete-time PID controller.
35///
36/// Implements the [`Controller`] trait with `State = f64`, `Reference = f64`,
37/// and `Output = f64`.
38///
39/// # Example
40///
41/// ```
42/// use rust_robotics_control::pid_controller::{PIDConfig, PIDController};
43/// use rust_robotics_core::Controller;
44///
45/// let mut pid = PIDController::with_gains(1.0, 0.0, 0.0, 0.01);
46/// let output = pid.compute(&0.0, &1.0);
47/// assert!((output - 1.0).abs() < 1e-10);
48/// ```
49#[derive(Debug, Clone)]
50pub struct PIDController {
51    /// Controller configuration.
52    pub config: PIDConfig,
53    integral: f64,
54    prev_error: f64,
55}
56
57impl PIDController {
58    /// Create a new `PIDController` with the given configuration.
59    pub fn new(config: PIDConfig) -> Self {
60        Self {
61            config,
62            integral: 0.0,
63            prev_error: 0.0,
64        }
65    }
66
67    /// Create a `PIDController` with common gains and time step.
68    pub fn with_gains(kp: f64, ki: f64, kd: f64, dt: f64) -> Self {
69        Self::new(PIDConfig {
70            kp,
71            ki,
72            kd,
73            dt,
74            ..PIDConfig::default()
75        })
76    }
77
78    /// Advance the controller by one step given a pre-computed `error`.
79    ///
80    /// This is a convenience wrapper around [`Controller::compute`] for
81    /// situations where the error is already known.
82    pub fn step(&mut self, error: f64) -> f64 {
83        let state = 0.0_f64;
84        let reference = error;
85        self.compute(&state, &reference)
86    }
87}
88
89impl Controller for PIDController {
90    type State = f64;
91    type Reference = f64;
92    type Output = f64;
93
94    /// Compute the PID control output.
95    ///
96    /// `error = reference - state`.  The integral is clamped to
97    /// `±max_integral` (anti-windup) and the output is clamped to
98    /// `±max_output`.
99    fn compute(&mut self, state: &f64, reference: &f64) -> f64 {
100        let error = reference - state;
101
102        self.integral = (self.integral + error * self.config.dt)
103            .clamp(-self.config.max_integral, self.config.max_integral);
104
105        let derivative = (error - self.prev_error) / self.config.dt;
106        self.prev_error = error;
107
108        let output =
109            self.config.kp * error + self.config.ki * self.integral + self.config.kd * derivative;
110
111        output.clamp(-self.config.max_output, self.config.max_output)
112    }
113
114    /// Reset the controller's internal state (integral and previous error).
115    fn reset(&mut self) {
116        self.integral = 0.0;
117        self.prev_error = 0.0;
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use rust_robotics_core::Controller;
125
126    #[test]
127    fn test_proportional_only() {
128        let mut pid = PIDController::with_gains(2.0, 0.0, 0.0, 0.01);
129        let output = pid.compute(&0.0, &3.0);
130        assert!((output - 6.0).abs() < 1e-10, "expected 6.0, got {output}");
131    }
132
133    #[test]
134    fn test_integral_accumulation() {
135        let mut pid = PIDController::with_gains(0.0, 1.0, 0.0, 0.1);
136        // Each step: integral += error * dt = 1.0 * 0.1 = 0.1
137        // output = ki * integral
138        for _ in 0..5 {
139            pid.compute(&0.0, &1.0);
140        }
141        let output = pid.compute(&0.0, &1.0);
142        // After 6 steps integral = 0.6, output = 0.6
143        assert!((output - 0.6).abs() < 1e-10, "expected 0.6, got {output}");
144    }
145
146    #[test]
147    fn test_anti_windup() {
148        let config = PIDConfig {
149            kp: 0.0,
150            ki: 1.0,
151            kd: 0.0,
152            dt: 1.0,
153            max_integral: 5.0,
154            max_output: f64::INFINITY,
155        };
156        let mut pid = PIDController::new(config);
157        // Drive integral far beyond the limit
158        for _ in 0..20 {
159            pid.compute(&0.0, &1.0);
160        }
161        // Output = ki * integral, integral clamped to max_integral = 5.0
162        let output = pid.compute(&0.0, &0.0);
163        assert!(
164            output.abs() <= 5.0 + 1e-10,
165            "anti-windup failed: output {output} exceeded max_integral"
166        );
167    }
168
169    #[test]
170    fn test_derivative() {
171        let mut pid = PIDController::with_gains(0.0, 0.0, 1.0, 0.1);
172        // First step: prev_error = 0, error = 1 → derivative = (1 - 0) / 0.1 = 10
173        let output = pid.compute(&0.0, &1.0);
174        assert!((output - 10.0).abs() < 1e-10, "expected 10.0, got {output}");
175        // Second step: error stays 1 → derivative = 0
176        let output2 = pid.compute(&0.0, &1.0);
177        assert!(output2.abs() < 1e-10, "expected 0.0, got {output2}");
178    }
179
180    #[test]
181    fn test_reset() {
182        let mut pid = PIDController::with_gains(1.0, 1.0, 1.0, 0.1);
183        for _ in 0..10 {
184            pid.compute(&0.0, &1.0);
185        }
186        pid.reset();
187        // After reset integral=0, prev_error=0 → pure P on next call
188        let output = pid.compute(&0.0, &2.0);
189        // P = kp * error = 1.0 * 2.0 = 2.0
190        // I = ki * (0 + 2.0 * 0.1) = 0.2
191        // D = kd * (2.0 - 0) / 0.1 = 20.0
192        let expected = 1.0 * 2.0 + 1.0 * (2.0 * 0.1) + 1.0 * (2.0 / 0.1);
193        assert!(
194            (output - expected).abs() < 1e-10,
195            "expected {expected}, got {output}"
196        );
197    }
198}