idsp/iir/
pid.rs

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