idsp/iir/
pid.rs

1//! PID controller IIR filters
2
3use core::ops::{Add, AddAssign, Div, Mul, SubAssign};
4
5use miniconf::Tree;
6use num_traits::{AsPrimitive, Float};
7use serde::{Deserialize, Serialize};
8
9use crate::{
10    Build,
11    iir::{Biquad, BiquadClamp},
12};
13
14/// Feedback term order
15#[derive(Clone, Debug, Copy, Serialize, Deserialize, Default, PartialEq, PartialOrd)]
16pub enum Order {
17    /// Proportional
18    P = 2,
19    /// Integrator
20    #[default]
21    I = 1,
22    /// Double integrator
23    I2 = 0,
24}
25
26/// PID controller builder
27///
28/// Builds `Biquad` from action gains, gain limits, input offset and output limits.
29///
30/// ```
31/// # use idsp::{iir::*, Build};
32/// let b: Biquad<f32> = pid::Builder::default()
33///     .gain(pid::Action::I, 1e-3)
34///     .gain(pid::Action::P, 1.0)
35///     .gain(pid::Action::D, 1e2)
36///     .limit(pid::Action::I, 1e3)
37///     .limit(pid::Action::D, 1e1)
38///     .build(&1e-3);
39/// ```
40#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
41pub struct Builder<T> {
42    order: Order,
43    gain: [T; 5],
44    limit: [T; 5],
45}
46
47impl<T: Float> Default for Builder<T> {
48    fn default() -> Self {
49        Self {
50            order: Order::default(),
51            gain: [T::zero(); 5],
52            limit: [T::infinity(); 5],
53        }
54    }
55}
56
57/// PID action
58///
59/// This enumerates the five possible PID style actions of a Biquad/second-order-section.
60#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
61pub enum Action {
62    /// Double integrating, -40 dB per decade
63    I2 = 0,
64    /// Integrating, -20 dB per decade
65    I = 1,
66    /// Proportional
67    P = 2,
68    /// Derivative, 20 dB per decade
69    D = 3,
70    /// Double derivative, 40 dB per decade
71    D2 = 4,
72}
73
74impl<T: Float> Builder<T> {
75    /// Feedback term order
76    ///
77    /// # Arguments
78    /// * `order`: The maximum feedback term order.
79    pub fn order(mut self, order: Order) -> Self {
80        self.order = order;
81        self
82    }
83
84    /// Gain for a given action
85    ///
86    /// Gain units are `output/input * time.powi(order)` where
87    /// * `output` are output (`y`) units
88    /// * `input` are input (`x`) units
89    /// * `time` are sample period units, e.g. SI seconds
90    /// * `order` is the action order: the frequency exponent
91    ///   (`-1` for integrating, `0` for proportional, etc.)
92    ///
93    /// Gains are accurate in the low frequency limit. Towards Nyquist, the
94    /// frequency response is warped.
95    ///
96    /// Note that limit signs and gain signs should match.
97    ///
98    /// ```
99    /// # use idsp::{iir::*, Build};
100    /// # use dsp_process::SplitProcess;
101    /// let tau = 1e-3;
102    /// let ki = 1e-4;
103    /// let i: Biquad<f32> = pid::Builder::default().gain(pid::Action::I, ki).build(&tau);
104    /// let x0 = 5.0;
105    /// let y0 = i.process(&mut DirectForm1::default(), x0);
106    /// assert!((y0 / (x0 * tau * ki) - 1.0).abs() < 2.0 * f32::EPSILON);
107    /// ```
108    ///
109    /// # Arguments
110    /// * `action`: Action to control
111    /// * `gain`: Gain value
112    pub fn gain(mut self, action: Action, gain: T) -> Self {
113        self.gain[action as usize] = gain;
114        self
115    }
116
117    /// Gain limit for a given action
118    ///
119    /// Gain limit units are `output/input`. See also [`Builder::gain()`].
120    /// Multiple gains and limits may interact and lead to peaking.
121    ///
122    /// Note that limit signs and gain signs should match and that the
123    /// default limits are positive infinity.
124    ///
125    /// ```
126    /// # use idsp::{iir::*, Build};
127    /// # use dsp_process::SplitProcess;
128    /// let ki_limit = 1e3;
129    /// let i: Biquad<f32> = pid::Builder::default()
130    ///     .gain(pid::Action::I, 8.0)
131    ///     .limit(pid::Action::I, ki_limit)
132    ///     .build(&1.0);
133    /// let mut xy = DirectForm1::default();
134    /// let x0 = 5.0;
135    /// for _ in 0..1000 {
136    ///     i.process(&mut xy, x0);
137    /// }
138    /// let y0 = i.process(&mut xy, x0);
139    /// assert!((y0 / (x0 * ki_limit) - 1.0f32).abs() < 1e-3);
140    /// ```
141    ///
142    /// # Arguments
143    /// * `action`: Action to limit in gain
144    /// * `limit`: Gain limit
145    pub fn limit(mut self, action: Action, limit: T) -> Self {
146        self.limit[action as usize] = limit;
147        self
148    }
149}
150
151impl<T, C> Build<[C; 5]> for Builder<T>
152where
153    C: 'static + Copy + SubAssign + AddAssign,
154    T: Float + AsPrimitive<C>,
155{
156    type Context = T;
157    /// Compute coefficients and return `Biquad`.
158    ///
159    /// No attempt is made to detect NaNs, non-finite gains, non-positive period,
160    /// zero gain limits, or gain/limit sign mismatches.
161    /// These will consequently result in NaNs/infinities, peaking, or notches in
162    /// the Biquad coefficients.
163    ///
164    /// Gain limits for unused gain actions or for proportional action are ignored.
165    ///
166    /// ```
167    /// # use idsp::{iir::*, Build};
168    /// let i: Biquad<f32> = pid::Builder::default()
169    ///     .gain(pid::Action::P, 3.0)
170    ///     .order(pid::Order::P)
171    ///     .build(&1.0);
172    /// assert_eq!(i, Biquad::proportional(3.0f32));
173    /// ```
174    ///
175    /// # Arguments
176    /// * `t_unit`: Sample period in some units, e.g. SI seconds
177    ///
178    /// # Panic
179    /// Will panic in debug mode on fixed point coefficient overflow.
180    fn build(&self, period: &T) -> [C; 5] {
181        // Choose relevant gains and limits and scale
182        let mut z = period.powi(-(self.order as i32));
183        let mut gl = [[T::zero(); 2]; 3];
184        for (gl, (i, (gain, limit))) in gl
185            .iter_mut()
186            .zip(
187                self.gain
188                    .iter()
189                    .zip(self.limit.iter())
190                    .enumerate()
191                    .skip(self.order as usize),
192            )
193            .rev()
194        {
195            gl[0] = *gain * z;
196            gl[1] = if i == Action::P as usize {
197                T::one()
198            } else {
199                gl[0] / *limit
200            };
201            z = z * *period;
202        }
203
204        // Normalization
205        let a0i = (gl[0][1] + gl[1][1] + gl[2][1]).recip();
206
207        // Derivative/integration kernels
208        let kernels = [[1, 0, 0], [1, -1, 0], [1, -2, 1]];
209
210        // Coefficients
211        let mut ba = [[T::zero().as_(); 2]; 3];
212        for (gli, ki) in gl.into_iter().zip(kernels) {
213            // Quantize the gains and not the coefficients
214            let gli = gli.map(|c| (c * a0i).as_());
215            for (baj, kij) in ba.iter_mut().zip(ki) {
216                if kij > 0 {
217                    for _ in 0..kij {
218                        baj[0] += gli[0];
219                        baj[1] -= gli[1];
220                    }
221                } else {
222                    for _ in 0..-kij {
223                        baj[0] -= gli[0];
224                        baj[1] += gli[1];
225                    }
226                }
227            }
228        }
229
230        [ba[0][0], ba[1][0], ba[2][0], ba[1][1], ba[2][1]]
231    }
232}
233
234impl<C, T> Build<Biquad<C>> for Builder<T>
235where
236    Self: Build<[C; 5]>,
237    Biquad<C>: From<[C; 5]>,
238{
239    type Context = <Self as Build<[C; 5]>>::Context;
240
241    fn build(&self, ctx: &Self::Context) -> Biquad<C> {
242        self.build(ctx).into()
243    }
244}
245
246/// Named gains
247#[derive(Clone, Debug, Tree, Default)]
248#[allow(unused)]
249pub struct Gains<T> {
250    /// Gain values
251    ///
252    /// See [`Action`] for indices.
253    #[tree(skip)]
254    pub value: [T; 5],
255    /// Double integral
256    #[tree(defer = "self.value[Action::I2 as usize]", typ = "T")]
257    i2: (),
258    /// Integral
259    #[tree(defer = "self.value[Action::I as usize]", typ = "T")]
260    i: (),
261    /// Proportional
262    #[tree(defer = "self.value[Action::P as usize]", typ = "T")]
263    p: (),
264    /// Derivative
265    #[tree(defer = "self.value[Action::D as usize]", typ = "T")]
266    d: (),
267    /// Double derivative
268    #[tree(defer = "self.value[Action::D2 as usize]", typ = "T")]
269    d2: (),
270}
271
272impl<T> Gains<T> {
273    /// Create a new `Gains` given values.
274    pub fn new(value: [T; 5]) -> Self {
275        Self {
276            value,
277            i2: (),
278            i: (),
279            p: (),
280            d: (),
281            d2: (),
282        }
283    }
284}
285
286/// Units for a biquad
287///
288/// In desired (e.g. SI) units per machine (e.g. full scale or LSB) unit
289#[derive(Clone, Debug)]
290pub struct Units<T> {
291    /// Update period
292    ///
293    /// One update interval corresponds to this many physical units (e.g. seconds).
294    pub t: T,
295    /// Input unit
296    ///
297    /// Unit input in machine units corresponds to this many physical units.
298    pub x: T,
299    /// Output unit
300    ///
301    /// Unit output in machine units corresponds to this many physical units
302    pub y: T,
303}
304
305impl<T: Float> Default for Units<T> {
306    fn default() -> Self {
307        Self {
308            t: T::one(),
309            x: T::one(),
310            y: T::one(),
311        }
312    }
313}
314
315/// PID Controller parameters
316#[derive(Clone, Debug, Tree)]
317#[tree(meta(doc, typename))]
318pub struct Pid<T> {
319    /// Feedback term order
320    #[tree(with=miniconf::leaf)]
321    pub order: Order,
322    /// Gain
323    ///
324    /// * Sequence: [I², I, P, D, D²]
325    /// * Units: output/intput * second**order where Action::I2 has order=-2
326    /// * Gains outside the range `order..=order + 3` are ignored
327    /// * P gain sign determines sign of all gains
328    pub gain: Gains<T>,
329    /// Gain imit
330    ///
331    /// * Sequence: [I², I, P, D, D²]
332    /// * Units: output/intput
333    /// * P gain limit is ignored
334    /// * Limits outside the range `order..order + 3` are ignored
335    /// * P gain sign determines sign of all gain limits
336    pub limit: Gains<T>,
337    /// Setpoint
338    ///
339    /// Units: input
340    pub setpoint: T,
341    /// Output lower limit
342    ///
343    /// Units: output
344    pub min: T,
345    /// Output upper limit
346    ///
347    /// Units: output
348    pub max: T,
349}
350
351impl<T: Float + Default> Default for Pid<T> {
352    fn default() -> Self {
353        Self {
354            order: Order::default(),
355            gain: Gains::default(),
356            limit: Gains {
357                value: [T::infinity(); 5],
358                ..Default::default()
359            },
360            setpoint: T::zero(),
361            min: T::neg_infinity(),
362            max: T::infinity(),
363        }
364    }
365}
366
367impl<T, Y, C> Build<BiquadClamp<C, Y>> for Pid<T>
368where
369    Y: 'static + Copy + Mul<C, Output = Y> + Div<C, Output = Y>,
370    C: Add<Output = C> + Copy,
371    T: AsPrimitive<Y> + Float,
372    BiquadClamp<C, Y>: From<[C; 5]>,
373    Builder<T>: Build<[C; 5], Context = T>,
374{
375    type Context = Units<T>;
376    /// Return the `Biquad`
377    ///
378    /// Builder intermediate type `I`, coefficient type C
379    fn build(&self, units: &Units<T>) -> BiquadClamp<C, Y> {
380        let yu = units.y.recip();
381        let yx = units.x * yu;
382        let p = self.gain.value[Action::P as usize];
383        let mut biquad: BiquadClamp<C, Y> = Builder {
384            gain: self.gain.value.map(|g| yx * g.copysign(p)),
385            limit: self.limit.value.map(|mut l| {
386                // infinite gain limit is meaningful but json can only do null/nan
387                if l.is_nan() {
388                    l = T::infinity()
389                }
390                yx * l.copysign(p)
391            }),
392            order: self.order,
393        }
394        .build(&units.t)
395        .into();
396        biquad.set_input_offset((-self.setpoint * units.x.recip()).as_());
397        biquad.min = (self.min * yu).as_();
398        biquad.max = (self.max * yu).as_();
399        biquad
400    }
401}
402
403#[cfg(test)]
404mod test {
405    use dsp_process::SplitProcess;
406
407    use crate::iir::{pid::Build, *};
408
409    #[test]
410    fn pid() {
411        let b: Biquad<f32> = pid::Builder::default()
412            .gain(pid::Action::I, 1e-3)
413            .gain(pid::Action::P, 1.0)
414            .gain(pid::Action::D, 1e2)
415            .limit(pid::Action::I, 1e3)
416            .limit(pid::Action::D, 1e1)
417            .build(&1.0);
418        let want = [
419            9.18190826,
420            -18.27272561,
421            9.09090826,
422            1.90909074,
423            -0.90909083,
424        ];
425        for (ba_have, ba_want) in b.ba.iter().zip(want.iter()) {
426            assert!(
427                (ba_have / ba_want - 1.0).abs() < 2.0 * f32::EPSILON,
428                "have {:?} != want {want:?}",
429                b.ba,
430            );
431        }
432    }
433
434    #[test]
435    fn pid_i32() {
436        use dsp_fixedpoint::Q32;
437        let b: Biquad<Q32<29>> = pid::Builder::<f32>::default()
438            .gain(pid::Action::I, 1e-5)
439            .gain(pid::Action::P, 1e-2)
440            .gain(pid::Action::D, 1e0)
441            .limit(pid::Action::I, 1e1)
442            .limit(pid::Action::D, 1e-1)
443            .build(&1.0);
444        println!("{b:?}");
445    }
446
447    #[test]
448    fn units() {
449        let ki = 5e-2;
450        let tau = 3e-3;
451        let b: Biquad<f32> = pid::Builder::default().gain(pid::Action::I, ki).build(&tau);
452        let mut xy = DirectForm1::default();
453        for i in 1..10 {
454            let y_have = b.process(&mut xy, 1.0);
455            let y_want = (i as f32) * tau * ki;
456            assert!(
457                (y_have / y_want - 1.0).abs() < 3.0 * f32::EPSILON,
458                "{i}: have {y_have} != {y_want}"
459            );
460        }
461    }
462}