Skip to main content

Biquad

Struct Biquad 

Source
pub struct Biquad<T> { /* private fields */ }
Expand description

A single second-order section (biquad) filter using Direct Form II Transposed.

Transfer function:

H(z) = (b0 + b1·z⁻¹ + b2·z⁻²) / (1 + a1·z⁻¹ + a2·z⁻²)

The denominator is stored normalized so a[0] = 1.

Implementations§

Source§

impl<T: FloatScalar> Biquad<T>

Source

pub fn new(b: [T; 3], a: [T; 3]) -> Self

Create a new biquad from numerator b and denominator a coefficients.

Normalizes by a[0] so the stored a[0] is always 1.

§Example
use numeris::control::Biquad;

let bq = Biquad::new([1.0, 2.0, 1.0], [1.0, -0.5, 0.1]);
let (b, a) = bq.coefficients();
assert_eq!(a[0], 1.0);
Source

pub fn try_new(b: [T; 3], a: [T; 3]) -> Result<Self, ControlError>

Fallible constructor: returns Err(ControlError::NearZeroDenominator) if a[0].abs() < 2 * T::epsilon().

§Example
use numeris::control::Biquad;

let bq = Biquad::try_new([1.0_f64, 2.0, 1.0], [1.0, -0.5, 0.1]).unwrap();
assert!(Biquad::try_new([1.0_f64, 0.0, 0.0], [0.0, 0.0, 0.0]).is_err());
Source

pub fn passthrough() -> Self

Identity (passthrough) filter: output equals input.

Source

pub fn tick(&mut self, x: T) -> T

Process a single input sample, returning the filtered output.

Uses Direct Form II Transposed for numerical stability.

Source

pub fn reset(&mut self)

Reset internal state to zero.

Source

pub fn process(&mut self, input: &[T], output: &mut [T])

Process a slice of input samples into an output slice.

§Panics

Panics if output.len() < input.len().

Source

pub fn process_inplace(&mut self, data: &mut [T])

Process a slice of samples in-place.

Source

pub fn coefficients(&self) -> ([T; 3], [T; 3])

Return the (b, a) coefficient arrays.

Examples found in repository?
docs/examples/gen_plots.rs (line 271)
259fn biquad_cascade_freq_response<const N: usize>(
260    cascade: &BiquadCascade<f64, N>,
261    freq: f64,
262    fs: f64,
263) -> f64 {
264    let omega = 2.0 * PI * freq / fs;
265    let (sin_w, cos_w) = omega.sin_cos();
266    let cos_2w = 2.0 * cos_w * cos_w - 1.0;
267    let sin_2w = 2.0 * sin_w * cos_w;
268
269    let mut mag_sq = 1.0;
270    for section in &cascade.sections {
271        let (b, a) = section.coefficients();
272        let nr = b[0] + b[1] * cos_w + b[2] * cos_2w;
273        let ni = -b[1] * sin_w - b[2] * sin_2w;
274        let dr = a[0] + a[1] * cos_w + a[2] * cos_2w;
275        let di = -a[1] * sin_w - a[2] * sin_2w;
276        mag_sq *= (nr * nr + ni * ni) / (dr * dr + di * di);
277    }
278    mag_sq.sqrt()
279}
280
281fn make_control_plot() -> String {
282    let fs = 8000.0;
283    let fc = 1000.0;
284
285    let bw2: BiquadCascade<f64, 1> = butterworth_lowpass(2, fc, fs).unwrap();
286    let bw4: BiquadCascade<f64, 2> = butterworth_lowpass(4, fc, fs).unwrap();
287    let bw6: BiquadCascade<f64, 3> = butterworth_lowpass(6, fc, fs).unwrap();
288
289    const N: usize = 500;
290    let mut freqs = vec![0.0; N];
291    let mut db2 = vec![0.0; N];
292    let mut db4 = vec![0.0; N];
293    let mut db6 = vec![0.0; N];
294
295    let f_min: f64 = 10.0;
296    let f_max: f64 = 3900.0;
297    for i in 0..N {
298        let f = f_min * (f_max / f_min).powf(i as f64 / (N - 1) as f64);
299        freqs[i] = f;
300        db2[i] = 20.0 * biquad_cascade_freq_response(&bw2, f, fs).log10();
301        db4[i] = 20.0 * biquad_cascade_freq_response(&bw4, f, fs).log10();
302        db6[i] = 20.0 * biquad_cascade_freq_response(&bw6, f, fs).log10();
303    }
304
305    let traces = format!(
306        "[{{\"type\":\"scatter\",\"mode\":\"lines\",\"name\":\"2nd order\",\
307          \"x\":{},\"y\":{},\"line\":{{\"width\":2.5}}}},\
308         {{\"type\":\"scatter\",\"mode\":\"lines\",\"name\":\"4th order\",\
309          \"x\":{},\"y\":{},\"line\":{{\"width\":2.5}}}},\
310         {{\"type\":\"scatter\",\"mode\":\"lines\",\"name\":\"6th order\",\
311          \"x\":{},\"y\":{},\"line\":{{\"width\":2.5}}}}]",
312        fmt_arr(&freqs), fmt_arr(&db2),
313        fmt_arr(&freqs), fmt_arr(&db4),
314        fmt_arr(&freqs), fmt_arr(&db6),
315    );
316
317    let layout = decorate_layout_ex(
318        "Butterworth Lowpass — f<sub>c</sub> = 1 kHz, f<sub>s</sub> = 8 kHz",
319        "Frequency (Hz)",
320        ",\"type\":\"log\"",
321        "Magnitude (dB)",
322        ",\"range\":[-80,5]",
323        &format!(
324            ",\"shapes\":[{{\"type\":\"line\",\"x0\":{f_min},\"x1\":{f_max},\
325             \"y0\":-3,\"y1\":-3,\"line\":{{\"dash\":\"dot\",\"color\":\"rgba(160,80,80,0.5)\",\"width\":1.5}}}}]"
326        ),
327    );
328
329    plotly_snippet("plot-control", &traces, &layout, 420)
330}
331
332// ─── Control: Lead/Lag compensator Bode ───────────────────────────────────
333
334fn biquad_freq_response(b: &[f64; 3], a: &[f64; 3], freq: f64, fs: f64) -> (f64, f64) {
335    let omega = 2.0 * PI * freq / fs;
336    let (sin_w, cos_w) = omega.sin_cos();
337    let cos_2w = 2.0 * cos_w * cos_w - 1.0;
338    let sin_2w = 2.0 * sin_w * cos_w;
339    let nr = b[0] + b[1] * cos_w + b[2] * cos_2w;
340    let ni = -b[1] * sin_w - b[2] * sin_2w;
341    let dr = a[0] + a[1] * cos_w + a[2] * cos_2w;
342    let di = -a[1] * sin_w - a[2] * sin_2w;
343    let mag = ((nr * nr + ni * ni) / (dr * dr + di * di)).sqrt();
344    let phase = (ni.atan2(nr) - di.atan2(dr)).to_degrees();
345    (mag, phase)
346}
347
348fn make_lead_lag_plot() -> String {
349    let fs = 1000.0;
350    let lead = lead_compensator(std::f64::consts::FRAC_PI_4, 50.0, 1.0, fs).unwrap();
351    let lag = lag_compensator(10.0, 5.0, fs).unwrap();
352
353    let (b_lead, a_lead) = lead.coefficients();
354    let (b_lag, a_lag) = lag.coefficients();
355
356    const N: usize = 400;
357    let f_min: f64 = 0.1;
358    let f_max: f64 = 490.0;
359    let mut freqs = vec![0.0; N];
360    let mut lead_db = vec![0.0; N];
361    let mut lead_ph = vec![0.0; N];
362    let mut lag_db = vec![0.0; N];
363    let mut lag_ph = vec![0.0; N];
364
365    for i in 0..N {
366        let f = f_min * (f_max / f_min).powf(i as f64 / (N - 1) as f64);
367        freqs[i] = f;
368        let (m, p) = biquad_freq_response(&b_lead, &a_lead, f, fs);
369        lead_db[i] = 20.0 * m.log10();
370        lead_ph[i] = p;
371        let (m, p) = biquad_freq_response(&b_lag, &a_lag, f, fs);
372        lag_db[i] = 20.0 * m.log10();
373        lag_ph[i] = p;
374    }
375
376    // Magnitude plot
377    let traces_mag = format!(
378        "[{{\"type\":\"scatter\",\"mode\":\"lines\",\"name\":\"Lead (45° @ 50 Hz)\",\
379          \"x\":{},\"y\":{},\"line\":{{\"width\":2.5}}}},\
380         {{\"type\":\"scatter\",\"mode\":\"lines\",\"name\":\"Lag (10× DC @ 5 Hz)\",\
381          \"x\":{},\"y\":{},\"line\":{{\"width\":2.5}}}}]",
382        fmt_arr(&freqs), fmt_arr(&lead_db),
383        fmt_arr(&freqs), fmt_arr(&lag_db),
384    );
385    let layout_mag = decorate_layout_ex(
386        "Lead / Lag Compensators — Magnitude",
387        "Frequency (Hz)",
388        ",\"type\":\"log\"",
389        "Magnitude (dB)",
390        "",
391        "",
392    );
393
394    // Phase plot
395    let traces_ph = format!(
396        "[{{\"type\":\"scatter\",\"mode\":\"lines\",\"name\":\"Lead phase\",\
397          \"x\":{},\"y\":{},\"line\":{{\"width\":2.5}}}},\
398         {{\"type\":\"scatter\",\"mode\":\"lines\",\"name\":\"Lag phase\",\
399          \"x\":{},\"y\":{},\"line\":{{\"width\":2.5}}}}]",
400        fmt_arr(&freqs), fmt_arr(&lead_ph),
401        fmt_arr(&freqs), fmt_arr(&lag_ph),
402    );
403    let layout_ph = decorate_layout_ex(
404        "Lead / Lag Compensators — Phase",
405        "Frequency (Hz)",
406        ",\"type\":\"log\"",
407        "Phase (°)",
408        "",
409        "",
410    );
411
412    let mut html = plotly_snippet("plot-lead-lag-mag", &traces_mag, &layout_mag, 380);
413    html.push_str(&plotly_snippet(
414        "plot-lead-lag-phase",
415        &traces_ph,
416        &layout_ph,
417        380,
418    ));
419    html
420}

Trait Implementations§

Source§

impl<T: Clone> Clone for Biquad<T>

Source§

fn clone(&self) -> Biquad<T>

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<T: Debug> Debug for Biquad<T>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<T: Copy> Copy for Biquad<T>

Auto Trait Implementations§

§

impl<T> Freeze for Biquad<T>
where T: Freeze,

§

impl<T> RefUnwindSafe for Biquad<T>
where T: RefUnwindSafe,

§

impl<T> Send for Biquad<T>
where T: Send,

§

impl<T> Sync for Biquad<T>
where T: Sync,

§

impl<T> Unpin for Biquad<T>
where T: Unpin,

§

impl<T> UnsafeUnpin for Biquad<T>
where T: UnsafeUnpin,

§

impl<T> UnwindSafe for Biquad<T>
where T: UnwindSafe,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<SS, SP> SupersetOf<SS> for SP
where SS: SubsetOf<SP>,

Source§

fn to_subset(&self) -> Option<SS>

The inverse inclusion map: attempts to construct self from the equivalent element of its superset. Read more
Source§

fn is_in_subset(&self) -> bool

Checks if self is actually part of its subset T (and can be converted to it).
Source§

fn to_subset_unchecked(&self) -> SS

Use with care! Same as self.to_subset but without any property checks. Always succeeds.
Source§

fn from_subset(element: &SS) -> SP

The inclusion map: converts self to the equivalent element of its superset.
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.