Skip to main content

numeris/control/
biquad.rs

1use crate::traits::FloatScalar;
2use super::ControlError;
3
4/// A single second-order section (biquad) filter using Direct Form II Transposed.
5///
6/// Transfer function:
7/// ```text
8/// H(z) = (b0 + b1·z⁻¹ + b2·z⁻²) / (1 + a1·z⁻¹ + a2·z⁻²)
9/// ```
10///
11/// The denominator is stored normalized so `a[0] = 1`.
12#[derive(Debug, Clone, Copy)]
13pub struct Biquad<T> {
14    b: [T; 3],
15    a: [T; 3], // [1, a1, a2]
16    z: [T; 2], // DFII-T state
17}
18
19impl<T: FloatScalar> Biquad<T> {
20    /// Create a new biquad from numerator `b` and denominator `a` coefficients.
21    ///
22    /// Normalizes by `a[0]` so the stored `a[0]` is always 1.
23    ///
24    /// # Example
25    ///
26    /// ```
27    /// use numeris::control::Biquad;
28    ///
29    /// let bq = Biquad::new([1.0, 2.0, 1.0], [1.0, -0.5, 0.1]);
30    /// let (b, a) = bq.coefficients();
31    /// assert_eq!(a[0], 1.0);
32    /// ```
33    pub fn new(b: [T; 3], a: [T; 3]) -> Self {
34        let a0 = a[0];
35        Self {
36            b: [b[0] / a0, b[1] / a0, b[2] / a0],
37            a: [T::one(), a[1] / a0, a[2] / a0],
38            z: [T::zero(); 2],
39        }
40    }
41
42    /// Fallible constructor: returns `Err(ControlError::NearZeroDenominator)` if
43    /// `a[0].abs() < 2 * T::epsilon()`.
44    ///
45    /// # Example
46    ///
47    /// ```
48    /// use numeris::control::Biquad;
49    ///
50    /// let bq = Biquad::try_new([1.0_f64, 2.0, 1.0], [1.0, -0.5, 0.1]).unwrap();
51    /// assert!(Biquad::try_new([1.0_f64, 0.0, 0.0], [0.0, 0.0, 0.0]).is_err());
52    /// ```
53    pub fn try_new(b: [T; 3], a: [T; 3]) -> Result<Self, ControlError> {
54        let two_eps = T::epsilon() * (T::one() + T::one());
55        if a[0].abs() < two_eps {
56            return Err(ControlError::NearZeroDenominator);
57        }
58        Ok(Self::new(b, a))
59    }
60
61    /// Identity (passthrough) filter: output equals input.
62    pub fn passthrough() -> Self {
63        Self {
64            b: [T::one(), T::zero(), T::zero()],
65            a: [T::one(), T::zero(), T::zero()],
66            z: [T::zero(); 2],
67        }
68    }
69
70    /// Process a single input sample, returning the filtered output.
71    ///
72    /// Uses Direct Form II Transposed for numerical stability.
73    #[inline]
74    pub fn tick(&mut self, x: T) -> T {
75        let y = self.b[0] * x + self.z[0];
76        self.z[0] = self.b[1] * x - self.a[1] * y + self.z[1];
77        self.z[1] = self.b[2] * x - self.a[2] * y;
78        y
79    }
80
81    /// Reset internal state to zero.
82    pub fn reset(&mut self) {
83        self.z = [T::zero(); 2];
84    }
85
86    /// Process a slice of input samples into an output slice.
87    ///
88    /// # Panics
89    ///
90    /// Panics if `output.len() < input.len()`.
91    pub fn process(&mut self, input: &[T], output: &mut [T]) {
92        assert!(output.len() >= input.len());
93        for (i, &x) in input.iter().enumerate() {
94            output[i] = self.tick(x);
95        }
96    }
97
98    /// Process a slice of samples in-place.
99    pub fn process_inplace(&mut self, data: &mut [T]) {
100        for sample in data.iter_mut() {
101            *sample = self.tick(*sample);
102        }
103    }
104
105    /// Return the `(b, a)` coefficient arrays.
106    pub fn coefficients(&self) -> ([T; 3], [T; 3]) {
107        (self.b, self.a)
108    }
109}
110
111/// A cascade of `N` biquad (second-order) sections.
112///
113/// Filter order is `2*N` for all-second-order sections, or `2*N - 1` when the
114/// last section is a degenerate first-order biquad (`b2 = a2 = 0`).
115///
116/// # Example
117///
118/// ```
119/// use numeris::control::{butterworth_lowpass, BiquadCascade};
120///
121/// // 4th-order Butterworth → 2 biquad sections
122/// let mut lpf: BiquadCascade<f64, 2> = butterworth_lowpass(4, 1000.0, 8000.0).unwrap();
123/// let y = lpf.tick(1.0);
124/// ```
125#[derive(Debug, Clone, Copy)]
126pub struct BiquadCascade<T, const N: usize> {
127    pub sections: [Biquad<T>; N],
128}
129
130impl<T: FloatScalar, const N: usize> BiquadCascade<T, N> {
131    /// Process a single input sample through all sections in series.
132    #[inline]
133    pub fn tick(&mut self, x: T) -> T {
134        let mut y = x;
135        for section in self.sections.iter_mut() {
136            y = section.tick(y);
137        }
138        y
139    }
140
141    /// Reset all sections' internal state to zero.
142    pub fn reset(&mut self) {
143        for section in self.sections.iter_mut() {
144            section.reset();
145        }
146    }
147
148    /// Process a slice of input samples into an output slice.
149    ///
150    /// # Panics
151    ///
152    /// Panics if `output.len() < input.len()`.
153    pub fn process(&mut self, input: &[T], output: &mut [T]) {
154        assert!(output.len() >= input.len());
155        for (i, &x) in input.iter().enumerate() {
156            output[i] = self.tick(x);
157        }
158    }
159
160    /// Process a slice of samples in-place.
161    pub fn process_inplace(&mut self, data: &mut [T]) {
162        for sample in data.iter_mut() {
163            *sample = self.tick(*sample);
164        }
165    }
166
167    /// Actual filter order (detects degenerate first-order last section).
168    pub fn order(&self) -> usize {
169        if N == 0 {
170            return 0;
171        }
172        let last = &self.sections[N - 1];
173        let (b, a) = last.coefficients();
174        // Degenerate first-order: b2 == 0 and a2 == 0
175        if b[2] == T::zero() && a[2] == T::zero() {
176            2 * N - 1
177        } else {
178            2 * N
179        }
180    }
181}
182
183// ─────────────────────────────────────────────────────────────────────
184// Bilinear transform helpers (pub(super) — used by butterworth/chebyshev)
185// ─────────────────────────────────────────────────────────────────────
186
187/// Build a lowpass biquad from a conjugate analog pole pair `(σ ± jω)`.
188///
189/// `wa` is the pre-warped analog cutoff, `c = 2·fs`.
190pub(super) fn bilinear_lp_pair<T: FloatScalar>(sigma: T, omega: T, wa: T, c: T) -> Biquad<T> {
191    let two = T::one() + T::one();
192    let d = c * c - two * sigma * c + sigma * sigma + omega * omega;
193    let wa2 = wa * wa;
194    let b0 = wa2 / d;
195    let b1 = two * wa2 / d;
196    let b2 = b0;
197    let a1 = two * (sigma * sigma + omega * omega - c * c) / d;
198    let a2 = (c * c + two * sigma * c + sigma * sigma + omega * omega) / d;
199    Biquad::new([b0, b1, b2], [T::one(), a1, a2])
200}
201
202/// Build a highpass biquad from a conjugate analog pole pair `(σ ± jω)`.
203pub(super) fn bilinear_hp_pair<T: FloatScalar>(sigma: T, omega: T, _wa: T, c: T) -> Biquad<T> {
204    let two = T::one() + T::one();
205    let c2 = c * c;
206    let d = c2 - two * sigma * c + sigma * sigma + omega * omega;
207    let b0 = c2 / d;
208    let b1 = -(two * c2) / d;
209    let b2 = b0;
210    let a1 = two * (sigma * sigma + omega * omega - c2) / d;
211    let a2 = (c2 + two * sigma * c + sigma * sigma + omega * omega) / d;
212    Biquad::new([b0, b1, b2], [T::one(), a1, a2])
213}
214
215/// Build a lowpass biquad from a single real analog pole `σ` (odd-order case).
216pub(super) fn bilinear_lp_real<T: FloatScalar>(sigma: T, wa: T, c: T) -> Biquad<T> {
217    // sigma is negative for stable poles; wa > 0
218    // LP: b0 = wa/(c - sigma), b1 = b0, b2 = 0
219    //     a1 = (sigma + c) is wrong — let's derive carefully:
220    // H_a(s) = wa / (s - sigma)   [sigma < 0]
221    // Bilinear: s = c·(z-1)/(z+1)
222    // H(z) = wa / (c·(z-1)/(z+1) - sigma)
223    //       = wa·(z+1) / (c·(z-1) - sigma·(z+1))
224    //       = wa·(z+1) / ((c - sigma)z + (-c - sigma))
225    // Divide by (c - sigma):
226    //   b0 = wa/(c - sigma), b1 = wa/(c - sigma), b2 = 0
227    //   a0 = 1, a1 = (-c - sigma)/(c - sigma), a2 = 0
228    let denom = c - sigma;
229    let b0 = wa / denom;
230    Biquad::new([b0, b0, T::zero()], [T::one(), (-c - sigma) / denom, T::zero()])
231}
232
233/// Build a highpass biquad from a single real analog pole `σ` (odd-order case).
234pub(super) fn bilinear_hp_real<T: FloatScalar>(sigma: T, _wa: T, c: T) -> Biquad<T> {
235    // H_a(s) = s / (s - sigma)   [sigma < 0]
236    // Bilinear: s = c·(z-1)/(z+1)
237    // H(z) = c·(z-1)/(z+1) / (c·(z-1)/(z+1) - sigma)
238    //       = c·(z-1) / (c·(z-1) - sigma·(z+1))
239    //       = c·(z-1) / ((c-sigma)z + (-c-sigma))
240    // Divide by (c - sigma):
241    //   b0 = c/(c - sigma), b1 = -c/(c - sigma), b2 = 0
242    //   a0 = 1, a1 = (-c - sigma)/(c - sigma), a2 = 0
243    let denom = c - sigma;
244    let b0 = c / denom;
245    Biquad::new(
246        [b0, -b0, T::zero()],
247        [T::one(), (-c - sigma) / denom, T::zero()],
248    )
249}