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}