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}