idsp/iir/
biquad.rs

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