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
272/// Units for a biquad
273///
274/// In desired (e.g. SI) units per machine (e.g. full scale or LSB) unit
275#[derive(Clone, Debug)]
276pub struct Units<T> {
277    /// Update period
278    ///
279    /// One update interval corresponds to this many physical units (e.g. seconds).
280    pub t: T,
281    /// Input unit
282    ///
283    /// Unit input in machine units corresponds to this many physical units.
284    pub x: T,
285    /// Output unit
286    ///
287    /// Unit output in machine units corresponds to this many physical units
288    pub y: T,
289}
290
291impl<T: Float> Default for Units<T> {
292    fn default() -> Self {
293        Self {
294            t: T::one(),
295            x: T::one(),
296            y: T::one(),
297        }
298    }
299}
300
301/// PID Controller parameters
302#[derive(Clone, Debug, Tree)]
303#[tree(meta(doc, typename))]
304pub struct Pid<T> {
305    /// Feedback term order
306    #[tree(with=miniconf::leaf)]
307    pub order: Order,
308    /// Gain
309    ///
310    /// * Sequence: [I², I, P, D, D²]
311    /// * Units: output/intput * second**order where Action::I2 has order=-2
312    /// * Gains outside the range `order..=order + 3` are ignored
313    /// * P gain sign determines sign of all gains
314    pub gain: Gains<T>,
315    /// Gain imit
316    ///
317    /// * Sequence: [I², I, P, D, D²]
318    /// * Units: output/intput
319    /// * P gain limit is ignored
320    /// * Limits outside the range `order..order + 3` are ignored
321    /// * P gain sign determines sign of all gain limits
322    pub limit: Gains<T>,
323    /// Setpoint
324    ///
325    /// Units: input
326    pub setpoint: T,
327    /// Output lower limit
328    ///
329    /// Units: output
330    pub min: T,
331    /// Output upper limit
332    ///
333    /// Units: output
334    pub max: T,
335}
336
337impl<T: Float + Default> Default for Pid<T> {
338    fn default() -> Self {
339        Self {
340            order: Order::default(),
341            gain: Gains::default(),
342            limit: Gains {
343                value: [T::infinity(); 5],
344                ..Default::default()
345            },
346            setpoint: T::zero(),
347            min: T::neg_infinity(),
348            max: T::infinity(),
349        }
350    }
351}
352
353impl<T, Y, C> Build<BiquadClamp<C, Y>> for Pid<T>
354where
355    Y: 'static + Copy + Mul<C, Output = Y> + Div<C, Output = Y>,
356    C: Add<Output = C> + Copy,
357    T: AsPrimitive<Y> + Float,
358    BiquadClamp<C, Y>: From<[C; 5]>,
359    Builder<T>: Build<[C; 5], Context = T>,
360{
361    type Context = Units<T>;
362    /// Return the `Biquad`
363    ///
364    /// Builder intermediate type `I`, coefficient type C
365    fn build(&self, units: &Units<T>) -> BiquadClamp<C, Y> {
366        let yu = units.y.recip();
367        let yx = units.x * yu;
368        let p = self.gain.value[Action::P as usize];
369        let mut biquad: BiquadClamp<C, Y> = Builder {
370            gain: self.gain.value.map(|g| yx * g.copysign(p)),
371            limit: self.limit.value.map(|mut l| {
372                // infinite gain limit is meaningful but json can only do null/nan
373                if l.is_nan() {
374                    l = T::infinity()
375                }
376                yx * l.copysign(p)
377            }),
378            order: self.order,
379        }
380        .build(&units.t)
381        .into();
382        biquad.set_input_offset((-self.setpoint * units.x.recip()).as_());
383        biquad.min = (self.min * yu).as_();
384        biquad.max = (self.max * yu).as_();
385        biquad
386    }
387}
388
389#[cfg(test)]
390mod test {
391    use dsp_process::SplitProcess;
392
393    use crate::iir::{pid::Build, *};
394
395    #[test]
396    fn pid() {
397        let b: Biquad<f32> = pid::Builder::default()
398            .gain(pid::Action::I, 1e-3)
399            .gain(pid::Action::P, 1.0)
400            .gain(pid::Action::D, 1e2)
401            .limit(pid::Action::I, 1e3)
402            .limit(pid::Action::D, 1e1)
403            .build(&1.0);
404        let want = [
405            9.18190826,
406            -18.27272561,
407            9.09090826,
408            1.90909074,
409            -0.90909083,
410        ];
411        for (ba_have, ba_want) in b.ba.iter().zip(want.iter()) {
412            assert!(
413                (ba_have / ba_want - 1.0).abs() < 2.0 * f32::EPSILON,
414                "have {:?} != want {want:?}",
415                b.ba,
416            );
417        }
418    }
419
420    #[test]
421    fn pid_i32() {
422        use dsp_fixedpoint::Q32;
423        let b: Biquad<Q32<29>> = pid::Builder::<f32>::default()
424            .gain(pid::Action::I, 1e-5)
425            .gain(pid::Action::P, 1e-2)
426            .gain(pid::Action::D, 1e0)
427            .limit(pid::Action::I, 1e1)
428            .limit(pid::Action::D, 1e-1)
429            .build(&1.0);
430        println!("{b:?}");
431    }
432
433    #[test]
434    fn units() {
435        let ki = 5e-2;
436        let tau = 3e-3;
437        let b: Biquad<f32> = pid::Builder::default().gain(pid::Action::I, ki).build(&tau);
438        let mut xy = DirectForm1::default();
439        for i in 1..10 {
440            let y_have = b.process(&mut xy, 1.0);
441            let y_want = (i as f32) * tau * ki;
442            assert!(
443                (y_have / y_want - 1.0).abs() < 3.0 * f32::EPSILON,
444                "{i}: have {y_have} != {y_want}"
445            );
446        }
447    }
448}