idsp/iir/
pid.rs

1use miniconf::{Leaf, Tree};
2use num_traits::{AsPrimitive, Float};
3use serde::{Deserialize, Serialize};
4
5use crate::{iir::Biquad, Coefficient};
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 _ {
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: [Leaf<T>; 5],
242    #[tree(defer = "self.value[Action::I2 as usize]", typ = "Leaf<T>")]
243    i2: (),
244    #[tree(defer = "self.value[Action::I as usize]", typ = "Leaf<T>")]
245    i: (),
246    #[tree(defer = "self.value[Action::P as usize]", typ = "Leaf<T>")]
247    p: (),
248    #[tree(defer = "self.value[Action::D as usize]", typ = "Leaf<T>")]
249    d: (),
250    #[tree(defer = "self.value[Action::D2 as usize]", typ = "Leaf<T>")]
251    d2: (),
252}
253
254impl<T: Float> Gain<T> {
255    fn new(value: T) -> Self {
256        Self {
257            value: [Leaf(value); 5],
258            i2: (),
259            i: (),
260            p: (),
261            d: (),
262            d2: (),
263        }
264    }
265}
266
267/// PID Controller parameters
268#[derive(Clone, Debug, Tree)]
269pub struct Pid<T: Float> {
270    /// Feedback term order
271    pub order: Leaf<Order>,
272    /// Gain
273    ///
274    /// * Sequence: [I², I, P, D, D²]
275    /// * Units: output/intput * second**order where Action::I2 has order=-2
276    /// * Gains outside the range `order..=order + 3` are ignored
277    /// * P gain sign determines sign of all gains
278    pub gain: Gain<T>,
279    /// Gain imit
280    ///
281    /// * Sequence: [I², I, P, D, D²]
282    /// * Units: output/intput
283    /// * P gain limit is ignored
284    /// * Limits outside the range `order..order + 3` are ignored
285    /// * P gain sign determines sign of all gain limits
286    pub limit: Gain<T>,
287    /// Setpoint
288    ///
289    /// Units: input
290    pub setpoint: Leaf<T>,
291    /// Output lower limit
292    ///
293    /// Units: output
294    pub min: Leaf<T>,
295    /// Output upper limit
296    ///
297    /// Units: output
298    pub max: Leaf<T>,
299}
300
301impl<T: Float> Default for Pid<T> {
302    fn default() -> Self {
303        Self {
304            order: Leaf(Order::default()),
305            gain: Gain::new(T::zero()),
306            limit: Gain::new(T::infinity()),
307            setpoint: Leaf(T::zero()),
308            min: Leaf(T::neg_infinity()),
309            max: Leaf(T::infinity()),
310        }
311    }
312}
313
314impl<T: Float> Pid<T> {
315    /// Return the `Biquad`
316    ///
317    /// Builder intermediate type `I`, coefficient type C
318    pub fn build<C, I>(&self, period: T, b_scale: T, y_scale: T) -> Biquad<C>
319    where
320        C: Coefficient + AsPrimitive<C> + AsPrimitive<I>,
321        T: AsPrimitive<I> + AsPrimitive<C>,
322        I: Float + 'static + AsPrimitive<C>,
323    {
324        let p = *self.gain.value[Action::P as usize];
325        let mut biquad: Biquad<C> = PidBuilder::<I> {
326            gain: self.gain.value.map(|g| (b_scale * g.copysign(p)).as_()),
327            limit: self.limit.value.map(|l| {
328                // infinite gain limit is meaningful but json can only do null/nan
329                let l = if l.is_nan() { T::infinity() } else { *l };
330                (b_scale * l.copysign(p)).as_()
331            }),
332            period: period.as_(),
333            order: *self.order,
334        }
335        .build()
336        .into();
337        biquad.set_input_offset((-*self.setpoint * y_scale).as_());
338        biquad.set_min((*self.min * y_scale).as_());
339        biquad.set_max((*self.max * y_scale).as_());
340        biquad
341    }
342}
343
344#[cfg(test)]
345mod test {
346    use crate::iir::*;
347
348    #[test]
349    fn pid() {
350        let b: Biquad<f32> = PidBuilder::default()
351            .period(1.0)
352            .gain(Action::I, 1e-3)
353            .gain(Action::P, 1.0)
354            .gain(Action::D, 1e2)
355            .limit(Action::I, 1e3)
356            .limit(Action::D, 1e1)
357            .build()
358            .into();
359        let want = [
360            9.18190826,
361            -18.27272561,
362            9.09090826,
363            -1.90909074,
364            0.90909083,
365        ];
366        for (ba_have, ba_want) in b.ba().iter().zip(want.iter()) {
367            assert!(
368                (ba_have / ba_want - 1.0).abs() < 2.0 * f32::EPSILON,
369                "have {:?} != want {want:?}",
370                b.ba(),
371            );
372        }
373    }
374
375    #[test]
376    fn pid_i32() {
377        let b: Biquad<i32> = PidBuilder::default()
378            .period(1.0)
379            .gain(Action::I, 1e-5)
380            .gain(Action::P, 1e-2)
381            .gain(Action::D, 1e0)
382            .limit(Action::I, 1e1)
383            .limit(Action::D, 1e-1)
384            .build()
385            .into();
386        println!("{b:?}");
387    }
388
389    #[test]
390    fn units() {
391        let ki = 5e-2;
392        let tau = 3e-3;
393        let b: Biquad<f32> = PidBuilder::default()
394            .period(tau)
395            .gain(Action::I, ki)
396            .build()
397            .into();
398        let mut xy = [0.0; 4];
399        for i in 1..10 {
400            let y_have = b.update(&mut xy, 1.0);
401            let y_want = (i as f32) * tau * ki;
402            assert!(
403                (y_have / y_want - 1.0).abs() < 3.0 * f32::EPSILON,
404                "{i}: have {y_have} != {y_want}"
405            );
406        }
407    }
408}