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}