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>
impl<T: FloatScalar> Biquad<T>
Sourcepub fn new(b: [T; 3], a: [T; 3]) -> Self
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);Sourcepub fn try_new(b: [T; 3], a: [T; 3]) -> Result<Self, ControlError>
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());Sourcepub fn passthrough() -> Self
pub fn passthrough() -> Self
Identity (passthrough) filter: output equals input.
Sourcepub fn tick(&mut self, x: T) -> T
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.
Sourcepub fn process(&mut self, input: &[T], output: &mut [T])
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().
Sourcepub fn process_inplace(&mut self, data: &mut [T])
pub fn process_inplace(&mut self, data: &mut [T])
Process a slice of samples in-place.
Sourcepub fn coefficients(&self) -> ([T; 3], [T; 3])
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§
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> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
Source§fn to_subset(&self) -> Option<SS>
fn to_subset(&self) -> Option<SS>
The inverse inclusion map: attempts to construct
self from the equivalent element of its
superset. Read moreSource§fn is_in_subset(&self) -> bool
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
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
fn from_subset(element: &SS) -> SP
The inclusion map: converts
self to the equivalent element of its superset.