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}