mecha10_behavior_runtime/control/
pid.rs1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
11pub struct PidGains {
12 pub kp: f64,
14 pub ki: f64,
16 pub kd: f64,
18}
19
20impl Default for PidGains {
21 fn default() -> Self {
22 Self {
23 kp: 1.0,
24 ki: 0.0,
25 kd: 0.0,
26 }
27 }
28}
29
30#[derive(Debug, Clone)]
34pub struct PidController {
35 gains: PidGains,
36 integral: f64,
38 previous_error: Option<f64>,
42 output_limits: (f64, f64),
44 integral_limits: (f64, f64),
47}
48
49impl PidController {
50 pub fn new(gains: PidGains, output_limits: (f64, f64)) -> Self {
54 Self {
55 gains,
56 integral: 0.0,
57 previous_error: None,
58 output_limits,
59 integral_limits: output_limits,
60 }
61 }
62
63 pub fn with_integral_limits(mut self, integral_limits: (f64, f64)) -> Self {
65 self.integral_limits = integral_limits;
66 self
67 }
68
69 pub fn update(&mut self, setpoint: f64, measurement: f64, dt: f64) -> f64 {
73 let error = setpoint - measurement;
74
75 let derivative = if dt > 0.0 {
76 self.integral = (self.integral + error * dt).clamp(self.integral_limits.0, self.integral_limits.1);
77 let derivative = self
78 .previous_error
79 .map(|previous| (error - previous) / dt)
80 .unwrap_or(0.0);
81 self.previous_error = Some(error);
82 derivative
83 } else {
84 0.0
85 };
86
87 let output = self.gains.kp * error + self.gains.ki * self.integral + self.gains.kd * derivative;
88 output.clamp(self.output_limits.0, self.output_limits.1)
89 }
90
91 pub fn reset(&mut self) {
94 self.integral = 0.0;
95 self.previous_error = None;
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn proportional_only_tracks_error() {
105 let gains = PidGains {
106 kp: 2.0,
107 ki: 0.0,
108 kd: 0.0,
109 };
110 let mut pid = PidController::new(gains, (-100.0, 100.0));
111
112 let output = pid.update(10.0, 4.0, 0.1);
113 assert_eq!(output, 12.0);
114 }
115
116 #[test]
117 fn integral_accumulates_error_over_time() {
118 let gains = PidGains {
119 kp: 0.0,
120 ki: 1.0,
121 kd: 0.0,
122 };
123 let mut pid = PidController::new(gains, (-100.0, 100.0));
124
125 let first = pid.update(1.0, 0.0, 1.0);
126 let second = pid.update(1.0, 0.0, 1.0);
127
128 assert_eq!(first, 1.0);
129 assert_eq!(second, 2.0);
130 }
131
132 #[test]
133 fn derivative_reacts_to_rate_of_change_and_is_zero_on_first_call() {
134 let gains = PidGains {
135 kp: 0.0,
136 ki: 0.0,
137 kd: 1.0,
138 };
139 let mut pid = PidController::new(gains, (-100.0, 100.0));
140
141 let first = pid.update(10.0, 0.0, 1.0);
142 assert_eq!(first, 0.0);
143
144 let second = pid.update(10.0, 5.0, 1.0);
145 assert_eq!(second, -5.0);
146 }
147
148 #[test]
149 fn output_is_clamped_to_configured_limits() {
150 let gains = PidGains {
151 kp: 100.0,
152 ki: 0.0,
153 kd: 0.0,
154 };
155 let mut pid = PidController::new(gains, (-1.0, 1.0));
156
157 assert_eq!(pid.update(10.0, 0.0, 1.0), 1.0);
158 assert_eq!(pid.update(-10.0, 0.0, 1.0), -1.0);
159 }
160
161 #[test]
162 fn integral_is_clamped_to_prevent_windup() {
163 let gains = PidGains {
164 kp: 0.0,
165 ki: 1.0,
166 kd: 0.0,
167 };
168 let mut pid = PidController::new(gains, (-100.0, 100.0)).with_integral_limits((-2.0, 2.0));
169
170 for _ in 0..5 {
171 pid.update(5.0, 0.0, 1.0);
172 }
173
174 assert_eq!(pid.update(5.0, 0.0, 1.0), 2.0);
175 }
176
177 #[test]
178 fn non_positive_dt_skips_integral_and_derivative_terms() {
179 let gains = PidGains {
180 kp: 1.0,
181 ki: 1.0,
182 kd: 1.0,
183 };
184 let mut pid = PidController::new(gains, (-100.0, 100.0));
185
186 pid.update(10.0, 0.0, 1.0);
187 let output = pid.update(10.0, 0.0, 0.0);
188
189 assert_eq!(output, 20.0);
190 }
191
192 #[test]
193 fn reset_clears_integral_and_derivative_state() {
194 let gains = PidGains {
195 kp: 0.0,
196 ki: 1.0,
197 kd: 1.0,
198 };
199 let mut pid = PidController::new(gains, (-100.0, 100.0));
200
201 pid.update(5.0, 0.0, 1.0);
202 pid.reset();
203
204 let output = pid.update(5.0, 0.0, 1.0);
205 assert_eq!(output, 5.0);
206 }
207
208 #[test]
209 fn converges_to_setpoint_over_repeated_ticks() {
210 let gains = PidGains {
211 kp: 0.6,
212 ki: 0.3,
213 kd: 0.05,
214 };
215 let mut pid = PidController::new(gains, (-10.0, 10.0));
216
217 let mut measurement = 0.0_f64;
218 let setpoint = 5.0;
219 let dt = 0.1;
220
221 for _ in 0..200 {
222 let output = pid.update(setpoint, measurement, dt);
223 measurement += output * dt;
224 }
225
226 assert!(
227 (measurement - setpoint).abs() < 0.1,
228 "expected convergence near {setpoint}, got {measurement}"
229 );
230 }
231}