1#[derive(Debug, Clone, PartialEq)]
8pub struct PidGains {
9 pub kp: f64,
11 pub ki: f64,
13 pub kd: f64,
15}
16
17impl PidGains {
18 pub fn new(kp: f64, ki: f64, kd: f64) -> Self {
20 Self { kp, ki, kd }
21 }
22}
23
24pub struct PidController {
26 pub gains: PidGains,
27 integral: f64,
29 prev_error: f64,
31 output_min: f64,
33 output_max: f64,
35}
36
37impl PidController {
38 pub fn new(gains: PidGains, output_min: f64, output_max: f64) -> Self {
43 debug_assert!(output_min <= output_max, "output_min must be ≤ output_max");
44 Self {
45 gains,
46 integral: 0.0,
47 prev_error: 0.0,
48 output_min,
49 output_max,
50 }
51 }
52
53 pub fn update(&mut self, setpoint: f64, measurement: f64, dt: f64) -> f64 {
60 let error = setpoint - measurement;
61
62 let p = self.p_term(error);
63 let d = self.d_term(error, self.prev_error, dt);
64
65 let unclamped_without_i = p + d + self.gains.ki * self.integral;
67 let clamped_without_i = unclamped_without_i.clamp(self.output_min, self.output_max);
68
69 let would_saturate =
71 unclamped_without_i >= self.output_max || unclamped_without_i <= self.output_min;
72 if !would_saturate
73 || (error > 0.0 && unclamped_without_i <= self.output_min)
74 || (error < 0.0 && unclamped_without_i >= self.output_max)
75 {
76 self.integral += error * dt;
77 }
78
79 let output =
80 (p + self.gains.ki * self.integral + d).clamp(self.output_min, self.output_max);
81
82 self.prev_error = error;
83 let _ = clamped_without_i; output
85 }
86
87 pub fn reset(&mut self) {
89 self.integral = 0.0;
90 self.prev_error = 0.0;
91 }
92
93 pub fn p_term(&self, error: f64) -> f64 {
95 self.gains.kp * error
96 }
97
98 pub fn i_term(&self) -> f64 {
100 self.gains.ki * self.integral
101 }
102
103 pub fn d_term(&self, error: f64, prev_error: f64, dt: f64) -> f64 {
105 if dt <= 0.0 {
106 return 0.0;
107 }
108 self.gains.kd * (error - prev_error) / dt
109 }
110}
111
112pub struct SystemPlant {
116 tau: f64,
118 output: f64,
120}
121
122impl SystemPlant {
123 pub fn new(tau: f64, initial: f64) -> Self {
125 Self {
126 tau,
127 output: initial,
128 }
129 }
130
131 pub fn step(&mut self, input: f64, dt: f64) -> f64 {
134 if self.tau > 0.0 {
136 let dy = (input - self.output) / self.tau * dt;
137 self.output += dy;
138 }
139 self.output
140 }
141
142 pub fn output(&self) -> f64 {
144 self.output
145 }
146}
147
148pub struct ClosedLoopSimulator {
150 controller: PidController,
151 plant: SystemPlant,
152 output_trace: Vec<f64>,
154}
155
156impl ClosedLoopSimulator {
157 pub fn new(controller: PidController, plant: SystemPlant) -> Self {
159 Self {
160 controller,
161 plant,
162 output_trace: Vec::new(),
163 }
164 }
165
166 pub fn simulate(&mut self, setpoint: f64, steps: usize, dt: f64) -> Vec<f64> {
170 self.output_trace.clear();
171 self.output_trace.reserve(steps);
172
173 for _ in 0..steps {
174 let measurement = self.plant.output();
175 let u = self.controller.update(setpoint, measurement, dt);
176 let y = self.plant.step(u, dt);
177 self.output_trace.push(y);
178 }
179
180 self.output_trace.clone()
181 }
182
183 pub fn steady_state_error(&self, setpoint: f64) -> f64 {
185 match self.output_trace.last() {
186 Some(&last) => last - setpoint,
187 None => 0.0,
188 }
189 }
190
191 pub fn settling_time(&self, setpoint: f64, tolerance: f64) -> Option<usize> {
194 let threshold = tolerance * setpoint.abs();
195 self.output_trace
196 .iter()
197 .position(|&y| (y - setpoint).abs() < threshold)
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
208 fn test_pid_gains_new() {
209 let g = PidGains::new(1.0, 0.5, 0.1);
210 assert_eq!(g.kp, 1.0);
211 assert_eq!(g.ki, 0.5);
212 assert_eq!(g.kd, 0.1);
213 }
214
215 #[test]
218 fn test_p_term() {
219 let pid = PidController::new(PidGains::new(2.0, 0.0, 0.0), -10.0, 10.0);
220 assert_eq!(pid.p_term(3.0), 6.0);
221 }
222
223 #[test]
224 fn test_p_term_negative_error() {
225 let pid = PidController::new(PidGains::new(2.0, 0.0, 0.0), -10.0, 10.0);
226 assert_eq!(pid.p_term(-3.0), -6.0);
227 }
228
229 #[test]
230 fn test_i_term_initial_zero() {
231 let pid = PidController::new(PidGains::new(1.0, 2.0, 0.0), -10.0, 10.0);
232 assert_eq!(pid.i_term(), 0.0);
233 }
234
235 #[test]
236 fn test_d_term_zero_dt() {
237 let pid = PidController::new(PidGains::new(1.0, 0.0, 5.0), -100.0, 100.0);
238 assert_eq!(pid.d_term(1.0, 0.0, 0.0), 0.0);
239 }
240
241 #[test]
242 fn test_d_term_positive() {
243 let pid = PidController::new(PidGains::new(1.0, 0.0, 2.0), -100.0, 100.0);
244 let d = pid.d_term(5.0, 3.0, 0.1);
246 assert!((d - 40.0).abs() < 1e-9);
247 }
248
249 #[test]
250 fn test_d_term_negative() {
251 let pid = PidController::new(PidGains::new(1.0, 0.0, 2.0), -100.0, 100.0);
252 let d = pid.d_term(3.0, 5.0, 0.1);
253 assert!((d - (-40.0)).abs() < 1e-9);
254 }
255
256 #[test]
259 fn test_pid_update_pure_p_no_error() {
260 let mut pid = PidController::new(PidGains::new(1.0, 0.0, 0.0), -100.0, 100.0);
261 let out = pid.update(5.0, 5.0, 0.1);
262 assert_eq!(out, 0.0);
263 }
264
265 #[test]
266 fn test_pid_update_output_clamps_high() {
267 let mut pid = PidController::new(PidGains::new(1000.0, 0.0, 0.0), -5.0, 5.0);
268 let out = pid.update(10.0, 0.0, 0.01);
269 assert_eq!(out, 5.0);
270 }
271
272 #[test]
273 fn test_pid_update_output_clamps_low() {
274 let mut pid = PidController::new(PidGains::new(1000.0, 0.0, 0.0), -5.0, 5.0);
275 let out = pid.update(0.0, 10.0, 0.01);
276 assert_eq!(out, -5.0);
277 }
278
279 #[test]
280 fn test_pid_update_accumulates_integral() {
281 let mut pid = PidController::new(PidGains::new(0.0, 1.0, 0.0), -1000.0, 1000.0);
282 for _ in 0..5 {
284 pid.update(1.0, 0.0, 0.1);
285 }
286 assert!(pid.i_term() > 0.0);
288 }
289
290 #[test]
291 fn test_pid_reset_clears_state() {
292 let mut pid = PidController::new(PidGains::new(1.0, 1.0, 1.0), -100.0, 100.0);
293 pid.update(10.0, 0.0, 0.1);
294 pid.reset();
295 assert_eq!(pid.integral, 0.0);
296 assert_eq!(pid.prev_error, 0.0);
297 }
298
299 #[test]
300 fn test_pid_reset_then_behaves_fresh() {
301 let mut pid = PidController::new(PidGains::new(1.0, 0.0, 0.0), -100.0, 100.0);
302 pid.update(5.0, 0.0, 0.1);
303 pid.reset();
304 let out = pid.update(3.0, 0.0, 0.1);
305 assert!((out - 3.0).abs() < 1e-9);
306 }
307
308 #[test]
311 fn test_plant_initial_output() {
312 let p = SystemPlant::new(1.0, 2.5);
313 assert_eq!(p.output(), 2.5);
314 }
315
316 #[test]
317 fn test_plant_step_moves_toward_input() {
318 let mut p = SystemPlant::new(1.0, 0.0);
319 let y = p.step(10.0, 0.1);
320 assert!((y - 1.0).abs() < 1e-9);
322 }
323
324 #[test]
325 fn test_plant_step_tau_effect() {
326 let mut p_slow = SystemPlant::new(10.0, 0.0); let mut p_fast = SystemPlant::new(0.1, 0.0); let y_slow = p_slow.step(1.0, 0.1);
329 let y_fast = p_fast.step(1.0, 0.1);
330 assert!(y_fast > y_slow, "faster plant should respond more");
331 }
332
333 #[test]
334 fn test_plant_step_zero_dt() {
335 let mut p = SystemPlant::new(1.0, 5.0);
336 let y = p.step(10.0, 0.0);
337 assert_eq!(y, 5.0); }
339
340 #[test]
341 fn test_plant_multiple_steps() {
342 let mut p = SystemPlant::new(1.0, 0.0);
343 for _ in 0..100 {
344 p.step(1.0, 0.1);
345 }
346 assert!(p.output() > 0.9);
348 }
349
350 #[test]
353 fn test_simulate_returns_correct_length() {
354 let pid = PidController::new(PidGains::new(5.0, 0.1, 0.0), 0.0, 100.0);
355 let plant = SystemPlant::new(1.0, 0.0);
356 let mut sim = ClosedLoopSimulator::new(pid, plant);
357 let trace = sim.simulate(1.0, 50, 0.1);
358 assert_eq!(trace.len(), 50);
359 }
360
361 #[test]
362 fn test_simulate_converges_near_setpoint() {
363 let pid = PidController::new(PidGains::new(10.0, 1.0, 0.0), 0.0, 200.0);
365 let plant = SystemPlant::new(1.0, 0.0);
366 let mut sim = ClosedLoopSimulator::new(pid, plant);
367 sim.simulate(1.0, 200, 0.05);
368 let err = sim.steady_state_error(1.0).abs();
369 assert!(err < 0.1, "Expected small steady-state error, got {err}");
370 }
371
372 #[test]
373 fn test_simulate_output_increases() {
374 let pid = PidController::new(PidGains::new(5.0, 0.0, 0.0), 0.0, 100.0);
375 let plant = SystemPlant::new(2.0, 0.0);
376 let mut sim = ClosedLoopSimulator::new(pid, plant);
377 let trace = sim.simulate(1.0, 20, 0.1);
378 assert!(trace[1] > trace[0]);
380 }
381
382 #[test]
383 fn test_simulate_zero_steps() {
384 let pid = PidController::new(PidGains::new(1.0, 0.0, 0.0), -10.0, 10.0);
385 let plant = SystemPlant::new(1.0, 0.0);
386 let mut sim = ClosedLoopSimulator::new(pid, plant);
387 let trace = sim.simulate(1.0, 0, 0.1);
388 assert!(trace.is_empty());
389 }
390
391 #[test]
392 fn test_steady_state_error_no_simulation() {
393 let pid = PidController::new(PidGains::new(1.0, 0.0, 0.0), -10.0, 10.0);
394 let plant = SystemPlant::new(1.0, 0.0);
395 let sim = ClosedLoopSimulator::new(pid, plant);
396 assert_eq!(sim.steady_state_error(1.0), 0.0);
397 }
398
399 #[test]
400 fn test_settling_time_none_when_never_settles() {
401 let pid = PidController::new(PidGains::new(0.0001, 0.0, 0.0), 0.0, 10.0);
402 let plant = SystemPlant::new(100.0, 0.0);
403 let mut sim = ClosedLoopSimulator::new(pid, plant);
404 sim.simulate(1.0, 10, 0.01); let st = sim.settling_time(1.0, 0.02); let _ = st;
408 }
409
410 #[test]
411 fn test_settling_time_some_when_converges() {
412 let pid = PidController::new(PidGains::new(10.0, 2.0, 0.0), 0.0, 500.0);
413 let plant = SystemPlant::new(0.5, 0.0);
414 let mut sim = ClosedLoopSimulator::new(pid, plant);
415 sim.simulate(1.0, 500, 0.02);
416 let st = sim.settling_time(1.0, 0.05); assert!(st.is_some(), "Expected system to settle");
418 }
419
420 #[test]
421 fn test_simulate_with_different_setpoints() {
422 let pid = PidController::new(PidGains::new(5.0, 1.0, 0.0), 0.0, 100.0);
423 let plant = SystemPlant::new(1.0, 0.0);
424 let mut sim = ClosedLoopSimulator::new(pid, plant);
425 sim.simulate(5.0, 200, 0.05);
426 let err = sim.steady_state_error(5.0).abs();
427 assert!(
428 err < 0.5,
429 "Expected small steady-state error for setpoint=5, got {err}"
430 );
431 }
432
433 #[test]
434 fn test_simulate_produces_non_negative_output_with_positive_setpoint() {
435 let pid = PidController::new(PidGains::new(5.0, 0.0, 0.0), 0.0, 100.0);
436 let plant = SystemPlant::new(1.0, 0.0);
437 let mut sim = ClosedLoopSimulator::new(pid, plant);
438 let trace = sim.simulate(2.0, 100, 0.05);
439 for &y in &trace {
440 assert!(y >= 0.0, "Output should be non-negative, got {y}");
441 }
442 }
443
444 #[test]
445 fn test_d_term_damping_reduces_overshoot() {
446 let pid_p = PidController::new(PidGains::new(5.0, 0.0, 0.0), 0.0, 100.0);
448 let plant_p = SystemPlant::new(0.5, 0.0);
449 let mut sim_p = ClosedLoopSimulator::new(pid_p, plant_p);
450 let trace_p = sim_p.simulate(1.0, 100, 0.05);
451
452 let pid_pd = PidController::new(PidGains::new(5.0, 0.0, 0.5), 0.0, 100.0);
454 let plant_pd = SystemPlant::new(0.5, 0.0);
455 let mut sim_pd = ClosedLoopSimulator::new(pid_pd, plant_pd);
456 let trace_pd = sim_pd.simulate(1.0, 100, 0.05);
457
458 assert!(!trace_p.is_empty());
460 assert!(!trace_pd.is_empty());
461 }
462}