pid_lite/lib.rs
1//! A small PID controller library.
2//!
3//! This crate implements the classic independent PID formulation.
4//!
5//! # Introduction
6//!
7//! PID controllers are an integral part of control systems, and provide a way to
8//! perform error correction. It's used to control things like throughput or
9//! resource allocation: as the resource approaches capacity, the returned
10//! correction decreases. And because it is aware of a time factor, it can deal
11//! with rapid changes as well.
12//!
13//! # Loop Tuning
14//!
15//! However PID controllers are not a silver bullet: they are a tool in a wider
16//! toolbox. To maximally benefit from them they need to be tuned to the
17//! workload. This is done through three parameters: `proportional_gain`,
18//! `integral_gain` and `derivative_gain`. Automated algorithms exist to tune
19//! these parameters based on sample workloads, but those are out of scope for
20//! this crate.
21//!
22//! [Read more on loop tuning](https://en.wikipedia.org/wiki/PID_controller#Loop_tuning).
23//!
24//! # No-std support
25//!
26//! `#[no_std]` support can be enabled by disabling the default crate-level
27//! features. This disables the `Controller::update` method which automatically
28//! calculates the time elapsed. Instead use the `Controller::update_elapsed`
29//! method which takes an externally calculated `Duration`.
30//!
31//! # Examples
32//!
33//! ```no_run
34//! use pid_lite::Controller;
35//! use std::thread;
36//! use std::time::Duration;
37//!
38//! let target = 80.0;
39//! let mut controller = Controller::new(target, 0.25, 0.01, 0.01);
40//!
41//! loop {
42//! let correction = controller.update(measure());
43//! apply_correction(correction);
44//! thread::sleep(Duration::from_secs(1));
45//! }
46//! # fn measure() -> f64 { todo!() }
47//! # fn apply_correction(_: f64) { todo!() }
48//! ```
49
50#![cfg_attr(not(feature = "std"), no_std)]
51#![forbid(unsafe_code)]
52#![deny(missing_debug_implementations, nonstandard_style)]
53#![warn(missing_docs, future_incompatible, unreachable_pub, rust_2018_idioms)]
54
55use core::time::Duration;
56#[cfg(feature = "std")]
57use std::time::Instant;
58
59/// PID controller
60///
61/// The `target` param sets the value we want to reach. The
62/// `proportional_gain`, `integral_gain` and `derivative_gain` parameters are all
63/// tuning parameters.
64///
65/// # Examples
66///
67/// ```no_run
68/// use pid_lite::Controller;
69/// use std::thread;
70/// use std::time::Duration;
71///
72/// let target = 80.0;
73/// let mut controller = Controller::new(target, 0.5, 0.1, 0.2);
74///
75/// loop {
76/// let correction = controller.update(measure());
77/// apply_correction(correction);
78/// thread::sleep(Duration::from_secs(1));
79/// }
80/// # fn measure() -> f64 { todo!() }
81/// # fn apply_correction(_: f64) { todo!() }
82/// ```
83
84#[derive(Debug)]
85#[cfg_attr(feature = "defmt", derive(defmt::Format))]
86pub struct Controller {
87 target: f64,
88
89 proportional_gain: f64,
90 integral_gain: f64,
91 derivative_gain: f64,
92
93 error_sum: f64,
94 last_error: f64,
95 #[cfg(feature = "std")]
96 last_instant: Option<Instant>,
97}
98
99impl Controller {
100 /// Create a new instance of `Controller`.
101 ///
102 /// # Examples
103 ///
104 /// ```
105 /// # #![allow(unused_assignments)]
106 /// use pid_lite::Controller;
107 ///
108 /// let target = 80.0;
109 /// let mut controller = Controller::new(target, 0.20, 0.02, 0.04);
110 /// ```
111 pub const fn new(
112 target: f64,
113 proportional_gain: f64,
114 integral_gain: f64,
115 derivative_gain: f64,
116 ) -> Self {
117 Self {
118 target,
119 proportional_gain,
120 integral_gain,
121 derivative_gain,
122 error_sum: 0.0,
123 last_error: 0.0,
124 #[cfg(feature = "std")]
125 last_instant: None,
126 }
127 }
128
129 /// Get the target.
130 ///
131 /// # Examples
132 ///
133 /// ```
134 /// # #![allow(unused_assignments)]
135 /// use pid_lite::Controller;
136 ///
137 /// let target = 80.0;
138 /// let mut controller = Controller::new(target, 0.20, 0.02, 0.04);
139 /// assert_eq!(controller.target(), 80.0);
140 /// ```
141 pub const fn target(&self) -> f64 {
142 self.target
143 }
144
145 /// Set the target.
146 ///
147 /// # Examples
148 ///
149 /// ```
150 /// # #![allow(unused_assignments)]
151 /// use pid_lite::Controller;
152 ///
153 /// let target = 80.0;
154 /// let mut controller = Controller::new(target, 0.20, 0.02, 0.04);
155 /// controller.set_target(60.0);
156 /// assert_eq!(controller.target(), 60.0);
157 /// ```
158 pub fn set_target(&mut self, target: f64) {
159 self.target = target;
160 }
161
162 /// Set the proportional gain
163 pub fn set_proportional_gain(&mut self, proportional_gain: f64) {
164 self.proportional_gain = proportional_gain;
165 }
166
167 /// Set the integral gain
168 pub fn set_integral_gain(&mut self, integral_gain: f64) {
169 self.integral_gain = integral_gain;
170 }
171
172 /// Set the derivative gain
173 pub fn set_derivative_gain(&mut self, derivative_gain: f64) {
174 self.derivative_gain = derivative_gain;
175 }
176
177 /// Push an entry into the controller.
178 ///
179 /// # Examples
180 ///
181 /// ```
182 /// # #![allow(unused_assignments)]
183 /// use pid_lite::Controller;
184 ///
185 /// let target = 80.0;
186 /// let mut controller = Controller::new(target, 0.0, 0.0, 0.0);
187 /// assert_eq!(controller.update(60.0), 0.0);
188 /// ```
189 ///
190 /// # Panics
191 ///
192 /// This function may panic if the `time_delta` in millis no longer fits in
193 /// an `f64`. This limit can be encountered when the PID controller is updated on the scale of
194 /// hours, rather than on the scale of minutes to milliseconds.
195 #[cfg(feature = "std")]
196 #[must_use = "A PID controller does nothing if the correction is not applied"]
197 pub fn update(&mut self, current_value: f64) -> f64 {
198 let now = Instant::now();
199 let elapsed = match self.last_instant {
200 Some(last_time) => now.duration_since(last_time),
201 None => Duration::from_millis(1),
202 };
203 self.last_instant = Some(now);
204 self.update_elapsed(current_value, elapsed)
205 }
206
207 /// Push an entry into the controller with a time delta since the last update.
208 ///
209 /// The `time_delta` value will be rounded down to the closest millisecond
210 /// with a minimum of 1 millisecond.
211 ///
212 /// # Examples
213 ///
214 /// ```
215 /// # #![allow(unused_assignments)]
216 /// use pid_lite::Controller;
217 /// use std::time::Duration;
218 ///
219 /// let target = 80.0;
220 /// let mut controller = Controller::new(target, 0.5, 0.1, 0.2);
221 /// let dur = Duration::from_millis(2);
222 /// assert_eq!(controller.update_elapsed(60.0, dur), 16.0);
223 /// ```
224 ///
225 /// # Panics
226 ///
227 /// This function may panic if the `time_delta` in millis no longer fits in
228 /// an `f64`. This limit can be encountered when the PID controller is updated on the scale of
229 /// hours, rather than on the scale of minutes to milliseconds.
230 #[must_use = "A PID controller does nothing if the correction is not applied"]
231 pub fn update_elapsed(&mut self, current_value: f64, elapsed: Duration) -> f64 {
232 let elapsed = (elapsed.as_millis() as f64).max(1.0);
233
234 let error = self.target - current_value;
235 let error_delta = (error - self.last_error) / elapsed;
236 self.error_sum += error * elapsed;
237 self.last_error = error;
238
239 let p = self.proportional_gain * error;
240 let i = self.integral_gain * self.error_sum;
241 let d = self.derivative_gain * error_delta;
242
243 p + i + d
244 }
245
246 /// Reset the internal state.
247 ///
248 /// # Examples
249 ///
250 /// ```
251 /// # #![allow(unused_assignments)]
252 /// use pid_lite::Controller;
253 /// use std::time::Duration;
254 ///
255 /// let target = 80.0;
256 /// let mut controller = Controller::new(target, 0.0, 0.0, 0.0);
257 /// let dur = Duration::from_secs(2);
258 /// let correction = controller.update_elapsed(60.0, dur);
259 ///
260 /// controller.reset();
261 /// ```
262 pub fn reset(&mut self) {
263 self.reset_inner();
264 }
265
266 #[cfg(feature = "std")]
267 fn reset_inner(&mut self) {
268 self.error_sum = 0.0;
269 self.last_error = 0.0;
270 self.last_instant = None;
271 }
272
273 #[cfg(not(feature = "std"))]
274 pub fn reset_inner(&mut self) {
275 self.error_sum = 0.0;
276 self.last_error = 0.0;
277 }
278}
279
280#[cfg(test)]
281mod test {
282 use super::*;
283
284 #[test]
285 fn base_correction() {
286 let target = 80.0;
287 let mut controller = Controller::new(target, 0.5, 0.1, 0.2);
288 let dur = Duration::from_millis(4);
289 assert_eq!(controller.update_elapsed(60.0, dur), 19.0);
290 }
291
292 #[test]
293 #[cfg(feature = "std")]
294 fn no_correction() {
295 let target = 80.0;
296 let mut controller = Controller::new(target, 0.0, 0.0, 0.0);
297 assert_eq!(controller.update(60.0), 0.0);
298 }
299
300 #[test]
301 #[cfg(feature = "std")]
302 fn updating_values() {
303 let target = 80.0;
304 let mut controller = Controller::new(target, 0.5, 0.1, 0.2);
305 let dur = Duration::from_millis(4);
306 assert_eq!(controller.update_elapsed(60.0, dur), 19.0);
307 controller.set_proportional_gain(0.8);
308 controller.set_derivative_gain(0.5);
309 assert_eq!(controller.update_elapsed(60.0, dur), 32.0);
310 }
311}