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}