idsp/iir/
biquad.rs

1use miniconf::Tree;
2use num_traits::{AsPrimitive, Float};
3use serde::{Deserialize, Serialize};
4
5use crate::Coefficient;
6
7/// Biquad IIR filter
8///
9/// A biquadratic IIR filter supports up to two zeros and two poles in the transfer function.
10/// It can be used to implement a wide range of responses to input signals.
11///
12/// The Biquad performs the following operation to compute a new output sample `y0` from a new
13/// input sample `x0` given its configuration and previous samples:
14///
15/// `y0 = clamp(b0*x0 + b1*x1 + b2*x2 - a1*y1 - a2*y2 + u, min, max)`
16///
17/// This implementation here saves storage and improves caching opportunities by decoupling
18/// filter configuration (coefficients, limits and offset) from filter state
19/// and thus supports both (a) sharing a single filter between multiple states ("channels") and (b)
20/// rapid switching of filters (tuning, transfer) for a given state without copying either
21/// state of configuration.
22///
23/// # Filter architecture
24///
25/// Direct Form 1 (DF1) and Direct Form 2 transposed (DF2T) are the only IIR filter
26/// structures with an (effective bin the case of TDF2) single summing junction
27/// this allows clamping of the output before feedback.
28///
29/// DF1 allows atomic coefficient change because only inputs and outputs are pipelined.
30/// The summing junction pipelining of TDF2 would require incremental
31/// coefficient changes and is thus less amenable to online tuning.
32///
33/// DF2T needs less state storage (2 instead of 4). This is in addition to the coefficient
34/// storage (5 plus 2 limits plus 1 offset)
35///
36/// DF2T is less efficient and accurate for fixed-point architectures as quantization
37/// happens at each intermediate summing junction in addition to the output quantization. This is
38/// especially true for common `i64 + i32 * i32 -> i64` MACC architectures.
39/// One could use wide state storage for fixed point DF2T but that would negate the storage
40/// and processing advantages.
41///
42/// # Coefficients
43///
44/// `ba: [T; 5] = [b0, b1, b2, a1, a2]` is the coefficients type.
45/// To represent the IIR coefficients, this contains the feed-forward
46/// coefficients `b0, b1, b2` followed by the feed-back coefficients
47/// `a1, a2`, all five normalized such that `a0 = 1`.
48///
49/// The summing junction of the filter also receives an offset `u`.
50///
51/// The filter applies clamping such that `min <= y <= max`.
52///
53/// See [`crate::iir::Filter`] and [`crate::iir::PidBuilder`] for ways to generate coefficients.
54///
55/// # Fixed point
56///
57/// Coefficient scaling (see [`Coefficient`]) is fixed and optimized such that -2 is exactly
58/// representable. This is tailored to low-passes, PID, II etc, where the integration rule is
59/// [1, -2, 1].
60///
61/// There are two guard bits in the accumulator before clamping/limiting.
62/// While this isn't enough to cover the worst case accumulator, it does catch many real world
63/// overflow cases.
64///
65/// # State
66///
67/// To represent the IIR state (input and output memory) during [`Biquad::update()`]
68/// the DF1 state contains the two previous inputs and output `[x1, x2, y1, y2]`
69/// concatenated. Lower indices correspond to more recent samples.
70///
71/// In the DF2T case the state contains `[b1*x1 + b2*x2 - a1*y1 - a2*y2, b2*x1 - a2*y1]`
72///
73/// In the DF1 case with first order noise shaping, the state contains `[x1, x2, y1, y2, e1]`
74/// where `e0` is the accumulated quantization error.
75///
76/// # PID controller
77///
78/// The IIR coefficients can be mapped to other transfer function
79/// representations, for example PID controllers as described in
80/// <https://hackmd.io/IACbwcOTSt6Adj3_F9bKuw> and
81/// <https://arxiv.org/abs/1508.06319>.
82///
83/// Using a Biquad as a template for a PID controller achieves several important properties:
84///
85/// * Its transfer function is universal in the sense that any biquadratic
86///   transfer function can be implemented (high-passes, gain limits, second
87///   order integrators with inherent anti-windup, notches etc) without code
88///   changes preserving all features.
89/// * It inherits a universal implementation of "integrator anti-windup", also
90///   and especially in the presence of set-point changes and in the presence
91///   of proportional or derivative gain without any back-off that would reduce
92///   steady-state output range.
93/// * It has universal derivative-kick (undesired, unlimited, and un-physical
94///   amplification of set-point changes by the derivative term) avoidance.
95/// * An offset at the input of an IIR filter (a.k.a. "set-point") is
96///   equivalent to an offset at the summing junction (in output units).
97///   They are related by the overall (DC feed-forward) gain of the filter.
98/// * It stores only previous outputs and inputs. These have direct and
99///   invariant interpretation (independent of coefficients and offset).
100///   Therefore it can trivially implement bump-less transfer between any
101///   coefficients/offset sets.
102/// * Cascading multiple IIR filters allows stable and robust
103///   implementation of transfer functions beyond biquadratic terms.
104#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, PartialOrd, Tree)]
105pub struct Biquad<T> {
106    /// Flattened coefficients `[b0, b1, b2, a1, a2]`
107    ///
108    /// The normalization `a0` is determined by the `Coefficient` implementation of the
109    /// underlying type.
110    ba: [T; 5],
111    /// Summing junction offset
112    u: T,
113    /// Summing junction lower limit
114    min: T,
115    /// Summing junction upper limit
116    max: T,
117}
118
119impl<T: Coefficient> Default for Biquad<T> {
120    fn default() -> Self {
121        Self {
122            ba: [T::ZERO; 5],
123            u: T::ZERO,
124            min: T::MIN,
125            max: T::MAX,
126        }
127    }
128}
129
130impl<T: Coefficient> From<[T; 5]> for Biquad<T> {
131    fn from(ba: [T; 5]) -> Self {
132        Self {
133            ba,
134            ..Default::default()
135        }
136    }
137}
138
139impl<T, C> From<&[[C; 3]; 2]> for Biquad<T>
140where
141    T: Coefficient + AsPrimitive<C>,
142    C: Float + AsPrimitive<T>,
143{
144    fn from(ba: &[[C; 3]; 2]) -> Self {
145        let ia0 = ba[1][0].recip();
146        Self::from([
147            T::quantize(ba[0][0] * ia0),
148            T::quantize(ba[0][1] * ia0),
149            T::quantize(ba[0][2] * ia0),
150            // b[3]: a0*ia0
151            T::quantize(ba[1][1] * ia0),
152            T::quantize(ba[1][2] * ia0),
153        ])
154    }
155}
156
157impl<T, C> From<&Biquad<T>> for [[C; 3]; 2]
158where
159    T: Coefficient + AsPrimitive<C>,
160    C: 'static + Copy,
161{
162    fn from(value: &Biquad<T>) -> Self {
163        let ba = value.ba();
164        [
165            [ba[0].as_(), ba[1].as_(), ba[2].as_()],
166            [T::ONE.as_(), ba[3].as_(), ba[4].as_()],
167        ]
168    }
169}
170
171impl<T: Coefficient> Biquad<T> {
172    /// A "hold" filter that ingests input and maintains output
173    ///
174    /// ```
175    /// # use idsp::iir::*;
176    /// let mut xy = [0.0, 1.0, 2.0, 3.0];
177    /// let x0 = 7.0;
178    /// let y0 = Biquad::HOLD.update(&mut xy, x0);
179    /// assert_eq!(y0, 2.0);
180    /// assert_eq!(xy, [x0, 0.0, y0, y0]);
181    /// ```
182    pub const HOLD: Self = Self {
183        ba: [T::ZERO, T::ZERO, T::ZERO, T::NEG_ONE, T::ZERO],
184        u: T::ZERO,
185        min: T::MIN,
186        max: T::MAX,
187    };
188
189    /// A unity gain filter
190    ///
191    /// ```
192    /// # use idsp::iir::*;
193    /// let x0 = 3.0;
194    /// let y0 = Biquad::IDENTITY.update(&mut [0.0; 4], x0);
195    /// assert_eq!(y0, x0);
196    /// ```
197    pub const IDENTITY: Self = Self::proportional(T::ONE);
198
199    /// A filter with the given proportional gain at all frequencies
200    ///
201    /// ```
202    /// # use idsp::iir::*;
203    /// let x0 = 2.0;
204    /// let k = 5.0;
205    /// let y0 = Biquad::proportional(k).update(&mut [0.0; 4], x0);
206    /// assert_eq!(y0, x0 * k);
207    /// ```
208    pub const fn proportional(k: T) -> Self {
209        Self {
210            ba: [k, T::ZERO, T::ZERO, T::ZERO, T::ZERO],
211            u: T::ZERO,
212            min: T::MIN,
213            max: T::MAX,
214        }
215    }
216
217    /// Filter coefficients
218    ///
219    /// IIR filter tap gains (`ba`) are an array `[b0, b1, b2, a1, a2]` such that
220    /// [`Biquad::update()`] returns
221    /// `y0 = clamp(b0*x0 + b1*x1 + b2*x2 - a1*y1 - a2*y2 + u, min, max)`.
222    ///
223    /// ```
224    /// # use idsp::Coefficient;
225    /// # use idsp::iir::*;
226    /// assert_eq!(Biquad::<i32>::IDENTITY.ba()[0], <i32 as Coefficient>::ONE);
227    /// assert_eq!(Biquad::<i32>::HOLD.ba()[3], -<i32 as Coefficient>::ONE);
228    /// ```
229    pub fn ba(&self) -> &[T; 5] {
230        &self.ba
231    }
232
233    /// Mutable reference to the filter coefficients.
234    ///
235    /// See [`Biquad::ba()`].
236    ///
237    /// ```
238    /// # use idsp::Coefficient;
239    /// # use idsp::iir::*;
240    /// let mut i = Biquad::default();
241    /// i.ba_mut()[0] = <i32 as Coefficient>::ONE;
242    /// assert_eq!(i, Biquad::IDENTITY);
243    /// ```
244    pub fn ba_mut(&mut self) -> &mut [T; 5] {
245        &mut self.ba
246    }
247
248    /// Summing junction offset
249    ///
250    /// This offset is applied to the output `y0` summing junction
251    /// on top of the feed-forward (`b`) and feed-back (`a`) terms.
252    /// The feedback samples are taken at the summing junction and
253    /// thus also include (and feed back) this offset.
254    pub fn u(&self) -> T {
255        self.u
256    }
257
258    /// Set the summing junction offset
259    ///
260    /// See [`Biquad::u()`].
261    ///
262    /// ```
263    /// # use idsp::iir::*;
264    /// let mut i = Biquad::default();
265    /// i.set_u(5);
266    /// assert_eq!(i.update(&mut [0; 4], 0), 5);
267    /// ```
268    pub fn set_u(&mut self, u: T) {
269        self.u = u;
270    }
271
272    /// Lower output limit
273    ///
274    /// Guaranteed minimum output value.
275    /// The value is inclusive.
276    /// The clamping also cleanly affects the feedback terms.
277    ///
278    /// For fixed point types, during the comparison,
279    /// the lowest two bits of value and limit are truncated.
280    ///
281    /// ```
282    /// # use idsp::iir::*;
283    /// assert_eq!(Biquad::<i32>::default().min(), i32::MIN);
284    /// ```
285    pub fn min(&self) -> T {
286        self.min
287    }
288
289    /// Set the lower output limit
290    ///
291    /// See [`Biquad::min()`].
292    ///
293    /// ```
294    /// # use idsp::iir::*;
295    /// let mut i = Biquad::default();
296    /// i.set_min(4);
297    /// assert_eq!(i.update(&mut [0; 4], 0), 4);
298    /// ```
299    pub fn set_min(&mut self, min: T) {
300        self.min = min;
301    }
302
303    /// Upper output limit
304    ///
305    /// Guaranteed maximum output value.
306    /// The value is inclusive.
307    /// The clamping also cleanly affects the feedback terms.
308    ///
309    /// For fixed point types, during the comparison,
310    /// the lowest two bits of value and limit are truncated.
311    /// The behavior is as if those two bits were 0 in the case
312    /// of `min` and one in the case of `max`.
313    ///
314    /// ```
315    /// # use idsp::iir::*;
316    /// assert_eq!(Biquad::<i32>::default().max(), i32::MAX);
317    /// ```
318    pub fn max(&self) -> T {
319        self.max
320    }
321
322    /// Set the upper output limit
323    ///
324    /// See [`Biquad::max()`].
325    ///
326    /// ```
327    /// # use idsp::iir::*;
328    /// let mut i = Biquad::default();
329    /// i.set_max(-5);
330    /// assert_eq!(i.update(&mut [0; 4], 0), -5);
331    /// ```
332    pub fn set_max(&mut self, max: T) {
333        self.max = max;
334    }
335
336    /// Compute the overall (DC/proportional feed-forward) gain.
337    ///
338    /// ```
339    /// # use idsp::iir::*;
340    /// assert_eq!(Biquad::proportional(3.0).forward_gain(), 3.0);
341    /// ```
342    ///
343    /// # Returns
344    /// The sum of the `b` feed-forward coefficients.
345    pub fn forward_gain(&self) -> T {
346        self.ba[0] + self.ba[1] + self.ba[2]
347    }
348
349    /// Compute input-referred (`x`) offset.
350    ///
351    /// ```
352    /// # use idsp::Coefficient;
353    /// # use idsp::iir::*;
354    /// let mut i = Biquad::proportional(3);
355    /// i.set_u(3);
356    /// assert_eq!(i.input_offset(), <i32 as Coefficient>::ONE);
357    /// ```
358    pub fn input_offset(&self) -> T {
359        self.u.div_scaled(self.forward_gain())
360    }
361
362    /// Convert input (`x`) offset to equivalent summing junction offset (`u`) and apply.
363    ///
364    /// In the case of a "PID" controller the response behavior of the controller
365    /// to the offset is "stabilizing", and not "tracking": its frequency response
366    /// is exclusively according to the lowest non-zero [`crate::iir::Action`] gain.
367    /// There is no high order ("faster") response as would be the case for a "tracking"
368    /// controller.
369    ///
370    /// ```
371    /// # use idsp::iir::*;
372    /// let mut i = Biquad::proportional(3.0);
373    /// i.set_input_offset(2.0);
374    /// let x0 = 0.5;
375    /// let y0 = i.update(&mut [0.0; 4], x0);
376    /// assert_eq!(y0, (x0 + i.input_offset()) * i.forward_gain());
377    /// ```
378    ///
379    /// ```
380    /// # use idsp::Coefficient;
381    /// # use idsp::iir::*;
382    /// let mut i = Biquad::proportional(-<i32 as Coefficient>::ONE);
383    /// i.set_input_offset(1);
384    /// assert_eq!(i.u(), -1);
385    /// ```
386    ///
387    /// # Arguments
388    /// * `offset`: Input (`x`) offset.
389    pub fn set_input_offset(&mut self, offset: T) {
390        self.u = offset.mul_scaled(self.forward_gain());
391    }
392
393    /// Direct Form 1 Update
394    ///
395    /// Ingest a new input value into the filter, update the filter state, and
396    /// return the new output. Only the state `xy` is modified.
397    ///
398    /// ## `N=4` Direct Form 1
399    ///
400    /// `xy` contains:
401    /// * On entry: `[x1, x2, y1, y2]`
402    /// * On exit:  `[x0, x1, y0, y1]`
403    ///
404    /// ```
405    /// # use idsp::iir::*;
406    /// let mut xy = [0.0, 1.0, 2.0, 3.0];
407    /// let x0 = 4.0;
408    /// let y0 = Biquad::IDENTITY.update(&mut xy, x0);
409    /// assert_eq!(y0, x0);
410    /// assert_eq!(xy, [x0, 0.0, y0, 2.0]);
411    /// ```
412    ///
413    /// ## `N=5` Direct Form 1 with first order noise shaping
414    ///
415    /// ```
416    /// # use idsp::iir::*;
417    /// let mut xy = [1, 2, 3, 4, 5];
418    /// let x0 = 6;
419    /// let y0 = Biquad::IDENTITY.update(&mut xy, x0);
420    /// assert_eq!(y0, x0);
421    /// assert_eq!(xy, [x0, 1, y0, 3, 5]);
422    /// ```
423    ///
424    /// `xy` contains:
425    /// * On entry: `[x1, x2, y1, y2, e1]`
426    /// * On exit:  `[x0, x1, y0, y1, e0]`
427    ///
428    /// Note: This is only useful for fixed point filters.
429    ///
430    /// ## `N=2` Direct Form 2 transposed
431    ///
432    /// Note: This is only useful for floating point filters.
433    /// Don't use this for fixed point: Quantization happens at each state store operation.
434    /// Ideally the state would be `[T::ACCU; 2]` but then for fixed point it would use equal amount
435    /// of storage compared to DF1 for no gain in performance and loss in functionality.
436    /// There are also no guard bits here.
437    ///
438    /// `xy` contains:
439    /// * On entry: `[b1*x1 + b2*x2 - a1*y1 - a2*y2, b2*x1 - a2*y1]`
440    /// * On exit:  `[b1*x0 + b2*x1 - a1*y0 - a2*y1, b2*x0 - a2*y0]`
441    ///
442    /// ```
443    /// # use idsp::iir::*;
444    /// let mut xy = [0.0, 1.0];
445    /// let x0 = 3.0;
446    /// let y0 = Biquad::IDENTITY.update(&mut xy, x0);
447    /// assert_eq!(y0, x0);
448    /// assert_eq!(xy, [1.0, 0.0]);
449    /// ```
450    ///
451    /// # Arguments
452    /// * `xy` - Current filter state.
453    /// * `x0` - New input.
454    ///
455    /// # Returns
456    /// The new output `y0 = clamp(b0*x0 + b1*x1 + b2*x2 - a1*y1 - a2*y2 + u, min, max)`
457    pub fn update<const N: usize>(&self, xy: &mut [T; N], x0: T) -> T {
458        match N {
459            // DF1
460            4 => {
461                let s = self.ba[0].as_() * x0.as_()
462                    + self.ba[1].as_() * xy[0].as_()
463                    + self.ba[2].as_() * xy[1].as_()
464                    - self.ba[3].as_() * xy[2].as_()
465                    - self.ba[4].as_() * xy[3].as_();
466                let (y0, _) = self.u.macc(s, self.min, self.max, T::ZERO);
467                xy[1] = xy[0];
468                xy[0] = x0;
469                xy[3] = xy[2];
470                xy[2] = y0;
471                y0
472            }
473            // DF1 with noise shaping for fixed point
474            5 => {
475                let s = self.ba[0].as_() * x0.as_()
476                    + self.ba[1].as_() * xy[0].as_()
477                    + self.ba[2].as_() * xy[1].as_()
478                    - self.ba[3].as_() * xy[2].as_()
479                    - self.ba[4].as_() * xy[3].as_();
480                let (y0, e0) = self.u.macc(s, self.min, self.max, xy[4]);
481                xy[4] = e0;
482                xy[1] = xy[0];
483                xy[0] = x0;
484                xy[3] = xy[2];
485                xy[2] = y0;
486                y0
487            }
488            // DF2T for floating point
489            2 => {
490                let y0 = (xy[0] + self.ba[0].mul_scaled(x0)).clip(self.min, self.max);
491                xy[0] = xy[1] + self.ba[1].mul_scaled(x0) - self.ba[3].mul_scaled(y0);
492                xy[1] = self.u + self.ba[2].mul_scaled(x0) - self.ba[4].mul_scaled(y0);
493                y0
494            }
495            _ => unimplemented!(),
496        }
497    }
498}