Intermediate commit. Added some stuff related to biquads, added some stuff related to power spectra estimation
This commit is contained in:
parent
158ea77c40
commit
7315939cbd
11
Cargo.toml
11
Cargo.toml
@ -78,10 +78,17 @@ uuid = { version = "1.6.1", features = ["v4"] , optional = true}
|
|||||||
# Command line argument parser, for CLI apps
|
# Command line argument parser, for CLI apps
|
||||||
clap = { version = "4.4.11", features = ["derive", "color", "help", "suggestions"] }
|
clap = { version = "4.4.11", features = ["derive", "color", "help", "suggestions"] }
|
||||||
|
|
||||||
|
# FFT's
|
||||||
|
realfft = "3.3.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
approx = "0.5.1"
|
||||||
|
ndarray-rand = "0.14.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# default = ["f64", "cpal-api", "record"]
|
default = ["f64", "cpal-api", "record"]
|
||||||
# Use this for debugging extensions
|
# Use this for debugging extensions
|
||||||
default = ["f64", "python-bindings", "record", "cpal-api"]
|
# default = ["f64", "python-bindings", "record", "cpal-api"]
|
||||||
|
|
||||||
cpal-api = ["dep:cpal"]
|
cpal-api = ["dep:cpal"]
|
||||||
record = ["dep:hdf5-sys", "dep:hdf5", "dep:chrono", "dep:uuid"]
|
record = ["dep:hdf5-sys", "dep:hdf5", "dep:chrono", "dep:uuid"]
|
||||||
|
@ -25,6 +25,7 @@ if #[cfg(feature = "python-bindings")] {
|
|||||||
pub use numpy::ndarray::{ArrayD, ArrayViewD, ArrayViewMutD};
|
pub use numpy::ndarray::{ArrayD, ArrayViewD, ArrayViewMutD};
|
||||||
pub use numpy::ndarray::prelude::*;
|
pub use numpy::ndarray::prelude::*;
|
||||||
pub use numpy::{IntoPyArray,PyArray, PyArray1, PyArrayDyn, PyArrayLike1, PyReadonlyArrayDyn};
|
pub use numpy::{IntoPyArray,PyArray, PyArray1, PyArrayDyn, PyArrayLike1, PyReadonlyArrayDyn};
|
||||||
|
pub use numpy::ndarray::Zip;
|
||||||
pub use pyo3::prelude::*;
|
pub use pyo3::prelude::*;
|
||||||
pub use pyo3::exceptions::PyValueError;
|
pub use pyo3::exceptions::PyValueError;
|
||||||
pub use pyo3::{pymodule, types::PyModule, PyResult};
|
pub use pyo3::{pymodule, types::PyModule, PyResult};
|
||||||
@ -32,8 +33,8 @@ if #[cfg(feature = "python-bindings")] {
|
|||||||
pub use pyo3;
|
pub use pyo3;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
pub use ndarray::prelude::*;
|
pub use ndarray::prelude::*;
|
||||||
|
pub use ndarray::Zip;
|
||||||
pub use ndarray::{Array1, Array2, ArrayView1};
|
pub use ndarray::{Array1, Array2, ArrayView1};
|
||||||
} }
|
} }
|
||||||
|
|
||||||
|
@ -1,12 +1,35 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::*;
|
use crate::config::*;
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{bail, Result};
|
||||||
use num::Complex;
|
use num::Complex;
|
||||||
|
|
||||||
#[cfg_attr(feature = "python-bindings", pyclass)]
|
#[cfg_attr(feature = "python-bindings", pyclass)]
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
/// # A biquad is a second order recursive filter structure.
|
/// # A biquad is a second order recursive filter structure.
|
||||||
///
|
///
|
||||||
|
/// This implementation only allows for normalized coefficients (a_0 = 1). It
|
||||||
|
/// performs the following relation of output to input:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// y[n] = - a_1 * y[n-1] - a_2 * y[n-2]
|
||||||
|
/// + b_0 * x[n] + b_1 * x[n-1] + b_2 * x[n-2]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The coefficients can be generated for typical standard type of biquad
|
||||||
|
/// filters, such as low pass, high pass, bandpass (first order), low shelf,
|
||||||
|
/// high shelf, peaking and notch filters.
|
||||||
|
///
|
||||||
|
/// The transfer function is:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// b_0 + b_1 z^-1 + b_2 * z^-2
|
||||||
|
/// H[z] = -----------------------------
|
||||||
|
/// 1 + a_1 z^-1 + a_2 * z^-2
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// And the frequency response can be found by filling in in above equation z =
|
||||||
|
/// exp(i*omega/fs), where fs is the sampling frequency and omega is the radian
|
||||||
|
/// frequency at which the transfer function is evaluated.
|
||||||
///
|
///
|
||||||
pub struct Biquad {
|
pub struct Biquad {
|
||||||
// State parameters
|
// State parameters
|
||||||
@ -71,6 +94,22 @@ impl Biquad {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct a Biquad with 0 initial state from coefficients given as
|
||||||
|
/// arguments.
|
||||||
|
///
|
||||||
|
/// *CAREFUL*: No checks are don on validity / stability of the created filter!
|
||||||
|
fn fromCoefs(b0: Flt, b1: Flt, b2: Flt, a1: Flt, a2: Flt) -> Biquad {
|
||||||
|
Biquad {
|
||||||
|
w1: 0.,
|
||||||
|
w2: 0.,
|
||||||
|
b0,
|
||||||
|
b1,
|
||||||
|
b2,
|
||||||
|
a1,
|
||||||
|
a2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create unit impulse response biquad filter. Input = output
|
/// Create unit impulse response biquad filter. Input = output
|
||||||
fn unit() -> Biquad {
|
fn unit() -> Biquad {
|
||||||
let filter_coefs = &[1., 0., 0., 1., 0., 0.];
|
let filter_coefs = &[1., 0., 0., 1., 0., 0.];
|
||||||
@ -101,19 +140,37 @@ impl Biquad {
|
|||||||
let facnum = 2. * fs * tau / (1. + 2. * fs * tau);
|
let facnum = 2. * fs * tau / (1. + 2. * fs * tau);
|
||||||
let facden = (1. - 2. * fs * tau) / (1. + 2. * fs * tau);
|
let facden = (1. - 2. * fs * tau) / (1. + 2. * fs * tau);
|
||||||
|
|
||||||
let coefs = [
|
Ok(Biquad::fromCoefs(
|
||||||
facnum, // b0
|
facnum, // b0
|
||||||
-facnum, // b1
|
-facnum, // b1
|
||||||
0., // b2
|
0., // b2,
|
||||||
1., // a0
|
|
||||||
facden, // a1
|
facden, // a1
|
||||||
0., // a2
|
0., // a2
|
||||||
];
|
))
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Biquad::new(&coefs).unwrap())
|
/// First order low pass filter (one pole in the real axis). No pre-warping
|
||||||
|
/// correction done.
|
||||||
|
pub fn firstOrderLowPass(fs: Flt, fc: Flt) -> Result<Biquad> {
|
||||||
|
match fc {
|
||||||
|
x if fc <= 0. => bail!("Cuton frequency should be > 0"),
|
||||||
|
x if fc >= fs / 2. => bail!("Cuton frequency should be smaller than Nyquist frequency"),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
let w0: Flt = 2. * pi * fc / fs;
|
||||||
|
let cw = Flt::cos(w0);
|
||||||
|
let b0: Flt = 2. * pi * fc * (cw + 1.) / (2. * pi * fc * cw + 2. * pi * fc - cw + 1.);
|
||||||
|
let b1: Flt = 2. * pi * fc * (cw + 1.) / (2. * pi * fc * cw + 2. * pi * fc - cw + 1.);
|
||||||
|
let b2: Flt = 0.;
|
||||||
|
let a1: Flt = (2. * pi * fc * cw + 2. * pi * fc + cw - 1.)
|
||||||
|
/ (2. * pi * fc * cw + 2. * pi * fc - cw + 1.);
|
||||||
|
let a2: Flt = 0.;
|
||||||
|
|
||||||
|
Ok(Biquad::fromCoefs(b0, b1, b2, a1, a2))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter input signal, output by overwriting input slice.
|
/// Filter input signal, output by overwriting input slice.
|
||||||
|
#[inline]
|
||||||
pub fn filter_inout(&mut self, inout: &mut [Flt]) {
|
pub fn filter_inout(&mut self, inout: &mut [Flt]) {
|
||||||
for sample in inout.iter_mut() {
|
for sample in inout.iter_mut() {
|
||||||
let w0 = *sample - self.a1 * self.w1 - self.a2 * self.w2;
|
let w0 = *sample - self.a1 * self.w1 - self.a2 * self.w2;
|
||||||
@ -125,6 +182,12 @@ impl Biquad {
|
|||||||
// println!("{:?}", inout);
|
// println!("{:?}", inout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl Default for Biquad {
|
||||||
|
/// Unit impulse (does not transform signal whatsoever)
|
||||||
|
fn default() -> Self {
|
||||||
|
Biquad::unit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Filter for Biquad {
|
impl Filter for Biquad {
|
||||||
fn filter(&mut self, input: &[Flt]) -> Vec<Flt> {
|
fn filter(&mut self, input: &[Flt]) -> Vec<Flt> {
|
||||||
@ -150,11 +213,13 @@ impl TransferFunction for Biquad {
|
|||||||
num / den
|
num / den
|
||||||
});
|
});
|
||||||
res
|
res
|
||||||
// re
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use approx::assert_abs_diff_eq;
|
||||||
|
use num::complex::ComplexFloat;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -164,4 +229,18 @@ mod test {
|
|||||||
let filtered = ser.filter(&inp);
|
let filtered = ser.filter(&inp);
|
||||||
assert_eq!(&filtered, &inp);
|
assert_eq!(&filtered, &inp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_firstOrderLowpass() {
|
||||||
|
let fs = 10.;
|
||||||
|
let fc = 1.;
|
||||||
|
let b = Biquad::firstOrderLowPass(fs, fc).unwrap();
|
||||||
|
let mut freq = Dcol::from_elem((3), 0.);
|
||||||
|
freq[1] = fc;
|
||||||
|
freq[2] = fs/2.;
|
||||||
|
let tf = b.tf(fs, freq.view());
|
||||||
|
assert_abs_diff_eq!(tf[0].re,1.);
|
||||||
|
assert_abs_diff_eq!(tf[0].im,0.);
|
||||||
|
assert_abs_diff_eq!(tf[1].abs(),1./Flt::sqrt(2.));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,11 @@ mod config;
|
|||||||
use config::*;
|
use config::*;
|
||||||
|
|
||||||
pub use config::Flt;
|
pub use config::Flt;
|
||||||
// pub mod window;
|
|
||||||
// pub mod ps;
|
|
||||||
pub mod filter;
|
pub mod filter;
|
||||||
pub mod daq;
|
pub mod daq;
|
||||||
|
pub mod ps;
|
||||||
pub mod siggen;
|
pub mod siggen;
|
||||||
|
mod math;
|
||||||
|
|
||||||
use filter::*;
|
use filter::*;
|
||||||
use daq::*;
|
use daq::*;
|
||||||
|
1
src/ps/aps.rs
Normal file
1
src/ps/aps.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
//! Averaged power spectra module
|
50
src/ps/fft.rs
Normal file
50
src/ps/fft.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//! Compute forward single sided amplitude spectra
|
||||||
|
use crate::config::*;
|
||||||
|
use realfft::{RealFftPlanner, RealToComplex};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FFT {
|
||||||
|
// The fft engine
|
||||||
|
fft: Arc<dyn RealToComplex<Flt>>,
|
||||||
|
// Copy over time data, as it is used as scratch data in the fft engine
|
||||||
|
timescratch: Vec<Flt>,
|
||||||
|
// rounded down nfft/2
|
||||||
|
half_nfft_rounded: usize,
|
||||||
|
nfftF: Flt,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FFT {
|
||||||
|
/// Create new FFT from given nfft
|
||||||
|
pub fn newFromNFFT(nfft: usize) -> FFT {
|
||||||
|
let mut planner = RealFftPlanner::<Flt>::new();
|
||||||
|
let fft = planner.plan_fft_forward(nfft);
|
||||||
|
Self::new(fft)
|
||||||
|
}
|
||||||
|
/// Create new fft engine from given fft engine
|
||||||
|
pub fn new(fft: Arc<dyn RealToComplex<Flt>>) -> FFT {
|
||||||
|
let nfft = fft.len();
|
||||||
|
let timescratch = vec![0.; nfft];
|
||||||
|
FFT {
|
||||||
|
fft,
|
||||||
|
timescratch,
|
||||||
|
half_nfft_rounded: nfft / 2,
|
||||||
|
nfftF: nfft as Flt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn process<'a, T>(&mut self, time: &[Flt], freq: T)
|
||||||
|
where
|
||||||
|
T: Into<ArrayViewMut<'a, Cflt, Ix1>>,
|
||||||
|
{
|
||||||
|
let mut freq = freq.into();
|
||||||
|
self.timescratch.copy_from_slice(time);
|
||||||
|
let _ = self
|
||||||
|
.fft
|
||||||
|
.process(&mut self.timescratch, freq.as_slice_mut().unwrap());
|
||||||
|
|
||||||
|
freq[0] /= self.nfftF;
|
||||||
|
freq[self.half_nfft_rounded] /= self.nfftF;
|
||||||
|
freq.slice_mut(s![1..self.half_nfft_rounded])
|
||||||
|
.par_mapv_inplace(|x| 2. * x / self.nfftF);
|
||||||
|
}
|
||||||
|
}
|
4
src/ps/mod.rs
Normal file
4
src/ps/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
//! Power spectra, averaged power spectra, etc. This module contains several
|
||||||
|
mod window;
|
||||||
|
mod ps;
|
||||||
|
mod fft;
|
302
src/ps/ps.rs
Normal file
302
src/ps/ps.rs
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
use crate::config::*;
|
||||||
|
use ndarray::parallel::prelude::*;
|
||||||
|
use num::pow::Pow;
|
||||||
|
use reinterpret::reinterpret_slice;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::usize;
|
||||||
|
|
||||||
|
use crate::Dcol;
|
||||||
|
|
||||||
|
use super::fft::FFT;
|
||||||
|
use super::window::*;
|
||||||
|
use std::mem::MaybeUninit;
|
||||||
|
|
||||||
|
use realfft::{RealFftPlanner, RealToComplex};
|
||||||
|
|
||||||
|
/// Singlesided cross-Power spectra computation engine.
|
||||||
|
struct PowerSpectra {
|
||||||
|
// Window used in estimator
|
||||||
|
pub window: Window,
|
||||||
|
// The window power, is corrected for in power spectra estimants
|
||||||
|
pub sqrt_win_pwr: Flt,
|
||||||
|
|
||||||
|
ffts: Vec<FFT>,
|
||||||
|
|
||||||
|
// Time-data buffer used for multiplying signals with Window
|
||||||
|
timedata: Array2<Flt>,
|
||||||
|
// Frequency domain buffer used for storage of signal FFt's in inbetween stage
|
||||||
|
freqdata: Array2<Cflt>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PowerSpectra {
|
||||||
|
/// Return the FFT length used in power spectra computations
|
||||||
|
pub fn nfft(&self) -> usize {
|
||||||
|
self.window.win.len()
|
||||||
|
}
|
||||||
|
/// Create new power spectra estimator. Uses FFT size from window length
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// - If win.len() != nfft
|
||||||
|
/// - if nfft == 0
|
||||||
|
pub fn newFromWindow(window: Window) -> PowerSpectra {
|
||||||
|
let nfft = window.win.len();
|
||||||
|
let win_pwr = window.win.mapv(|w| w.powi(2)).sum()/(nfft as Flt);
|
||||||
|
assert!(nfft > 0);
|
||||||
|
assert!(nfft % 2 == 0);
|
||||||
|
|
||||||
|
let mut planner = RealFftPlanner::<Flt>::new();
|
||||||
|
let fft = planner.plan_fft_forward(nfft);
|
||||||
|
|
||||||
|
let Fft = FFT::new(fft);
|
||||||
|
|
||||||
|
PowerSpectra {
|
||||||
|
window,
|
||||||
|
sqrt_win_pwr: Flt::sqrt(win_pwr),
|
||||||
|
ffts: vec![Fft],
|
||||||
|
timedata: Array2::zeros((nfft, 1)),
|
||||||
|
freqdata: Array2::zeros((nfft / 2 + 1, 1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute FFTs of input channel data.
|
||||||
|
fn compute_ffts(&mut self, timedata: ArrayView2<Flt>) -> &Array2<Cflt> {
|
||||||
|
let (n, nch) = timedata.dim();
|
||||||
|
let nfft = self.nfft();
|
||||||
|
assert!(n == nfft);
|
||||||
|
|
||||||
|
// Make sure enough fft engines are available
|
||||||
|
while nch > self.ffts.len() {
|
||||||
|
self.ffts.push(self.ffts.last().unwrap().clone());
|
||||||
|
self.freqdata
|
||||||
|
.push_column(Ccol::from_vec(vec![Cflt::new(0., 0.); nfft / 2 + 1]).view())
|
||||||
|
.unwrap();
|
||||||
|
self.timedata
|
||||||
|
.push_column(Dcol::zeros(nfft).view())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(n == self.nfft());
|
||||||
|
assert!(n == self.window.win.len());
|
||||||
|
let sqrt_win_pwr = self.sqrt_win_pwr;
|
||||||
|
|
||||||
|
// Multiply signals with window function, and compute fft's for each channel
|
||||||
|
Zip::from(timedata.axis_iter(Axis(1)))
|
||||||
|
.and(self.timedata.axis_iter_mut(Axis(1)))
|
||||||
|
.and(&mut self.ffts)
|
||||||
|
.and(self.freqdata.axis_iter_mut(Axis(1)))
|
||||||
|
.par_for_each(|time_in,mut time, fft, mut freq| {
|
||||||
|
|
||||||
|
// Multiply with window and copy over to local time data buffer
|
||||||
|
azip!((t in &mut time, &tin in time_in, &win in &self.window.win) *t=tin*win/sqrt_win_pwr);
|
||||||
|
|
||||||
|
let tslice = time.as_slice().unwrap();
|
||||||
|
let fslice = freq.as_slice_mut().unwrap();
|
||||||
|
fft.process(tslice, fslice);
|
||||||
|
});
|
||||||
|
|
||||||
|
&self.freqdata
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute cross power spectra from input time data. First axis is
|
||||||
|
/// frequency, second axis is channel i, third axis is channel j.
|
||||||
|
pub fn compute<'a, T>(&mut self, tdata: T) -> Array3<Cflt>
|
||||||
|
where
|
||||||
|
T: Into<ArrayView<'a, Flt, Ix2>>,
|
||||||
|
{
|
||||||
|
let tdata = tdata.into();
|
||||||
|
let clen = self.nfft() / 2 + 1;
|
||||||
|
let nchannel = tdata.ncols();
|
||||||
|
let win_pwr = self.sqrt_win_pwr;
|
||||||
|
|
||||||
|
// Compute fft of input data, and store in self.freqdata
|
||||||
|
let fd = self.compute_ffts(tdata);
|
||||||
|
let fdconj = fd.mapv(|c| c.conj());
|
||||||
|
|
||||||
|
let result = Array3::uninit((clen, nchannel, nchannel));
|
||||||
|
let mut result: Array3<Cflt> = unsafe { result.assume_init() };
|
||||||
|
|
||||||
|
// Loop over result axis one and channel i IN PARALLEL
|
||||||
|
Zip::from(result.axis_iter_mut(Axis(1)))
|
||||||
|
.and(fd.axis_iter(Axis(1)))
|
||||||
|
.par_for_each(|mut out, chi| {
|
||||||
|
// out: channel i of output 3D array, channel j all
|
||||||
|
// chi: channel i
|
||||||
|
Zip::from(out.axis_iter_mut(Axis(1)))
|
||||||
|
.and(fdconj.axis_iter(Axis(1)))
|
||||||
|
.for_each(|mut out, chj| {
|
||||||
|
// out: channel i, j
|
||||||
|
// chj: channel j conjugated
|
||||||
|
Zip::from(&mut out)
|
||||||
|
.and(chi)
|
||||||
|
.and(chj)
|
||||||
|
.for_each(|out, chi, chjc|
|
||||||
|
// Loop over frequency components
|
||||||
|
*out = 0.5 * chi * chjc
|
||||||
|
);
|
||||||
|
|
||||||
|
// The DC component has no 0.5 correction, as it only
|
||||||
|
// occurs ones in a (double-sided) power spectrum. So
|
||||||
|
// here we undo the 0.5 of 4 lines above here.
|
||||||
|
out[0] *= 2.;
|
||||||
|
out[clen-1] *= 2.;
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use approx::{abs_diff_eq, assert_relative_eq, assert_ulps_eq, ulps_eq};
|
||||||
|
// For absolute value
|
||||||
|
use num::complex::ComplexFloat;
|
||||||
|
use rand_distr::StandardNormal;
|
||||||
|
|
||||||
|
/// Generate a sine wave at the order i
|
||||||
|
fn generate_sinewave(nfft: usize,order: usize) -> Dcol {
|
||||||
|
Dcol::from_iter((0..nfft).map(|i| {
|
||||||
|
Flt::sin(i as Flt/(nfft) as Flt * order as Flt * 2.*pi)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/// Generate a sine wave at the order i
|
||||||
|
fn generate_cosinewave(nfft: usize,order: usize) -> Dcol {
|
||||||
|
Dcol::from_iter((0..nfft).map(|i| {
|
||||||
|
Flt::cos(i as Flt/(nfft) as Flt * order as Flt * 2.*pi)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
/// Test whether DC part of single-sided FFT has right properties
|
||||||
|
fn test_fft_DC() {
|
||||||
|
const nfft: usize = 10;
|
||||||
|
let rect = Window::new(WindowType::Rect, nfft);
|
||||||
|
let mut ps = PowerSpectra::newFromWindow(rect);
|
||||||
|
|
||||||
|
let td = Dmat::ones((nfft, 1));
|
||||||
|
|
||||||
|
let fd = ps.compute_ffts(td.view());
|
||||||
|
// println!("{:?}", fd);
|
||||||
|
assert_relative_eq!(fd[(0, 0)].re, 1.);
|
||||||
|
assert_relative_eq!(fd[(0, 0)].im, 0.);
|
||||||
|
let abs_fneq0 = fd.slice(s![1.., 0]).sum();
|
||||||
|
assert_relative_eq!(abs_fneq0.re, 0.);
|
||||||
|
assert_relative_eq!(abs_fneq0.im, 0.);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test whether AC part of single-sided FFT has right properties
|
||||||
|
#[test]
|
||||||
|
fn test_fft_AC() {
|
||||||
|
const nfft: usize = 256;
|
||||||
|
let rect = Window::new(WindowType::Rect, nfft);
|
||||||
|
let mut ps = PowerSpectra::newFromWindow(rect);
|
||||||
|
|
||||||
|
// Start with a time signal
|
||||||
|
let mut t: Dmat = Dmat::default((nfft, 0));
|
||||||
|
t.push_column(generate_sinewave(nfft,1).view())
|
||||||
|
.unwrap();
|
||||||
|
// println!("{:?}", t);
|
||||||
|
|
||||||
|
let fd = ps.compute_ffts(t.view());
|
||||||
|
// println!("{:?}", fd);
|
||||||
|
assert_relative_eq!(fd[(0, 0)].re, 0., epsilon = Flt::EPSILON * nfft as Flt);
|
||||||
|
assert_relative_eq!(fd[(0, 0)].im, 0., epsilon = Flt::EPSILON * nfft as Flt);
|
||||||
|
|
||||||
|
assert_relative_eq!(fd[(1, 0)].re, 0., epsilon = Flt::EPSILON * nfft as Flt);
|
||||||
|
assert_ulps_eq!(fd[(1, 0)].im, -1., epsilon = Flt::EPSILON * nfft as Flt);
|
||||||
|
|
||||||
|
// Sum of all terms at frequency index 2 to ...
|
||||||
|
let sum_higher_freqs_abs = Cflt::abs(fd.slice(s![2.., 0]).sum());
|
||||||
|
assert_ulps_eq!(
|
||||||
|
sum_higher_freqs_abs,
|
||||||
|
0.,
|
||||||
|
epsilon = Flt::EPSILON * nfft as Flt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Thest whether power spectra scale properly. Signals with amplitude of 1
|
||||||
|
/// should come back with a power of 0.5. DC offsets should come in as
|
||||||
|
/// value^2 at frequency index 0.
|
||||||
|
#[test]
|
||||||
|
fn test_ps_scale() {
|
||||||
|
|
||||||
|
const nfft: usize = 124;
|
||||||
|
let rect = Window::new(WindowType::Rect, nfft);
|
||||||
|
let mut ps = PowerSpectra::newFromWindow(rect);
|
||||||
|
|
||||||
|
// Start with a time signal
|
||||||
|
let mut t: Dmat = Dmat::default((nfft, 0));
|
||||||
|
t.push_column(generate_cosinewave(nfft,1).view())
|
||||||
|
.unwrap();
|
||||||
|
let dc_component = 0.25;
|
||||||
|
let dc_power = dc_component.pow(2);
|
||||||
|
t.mapv_inplace(|t| t + dc_component);
|
||||||
|
|
||||||
|
let power = ps.compute(t.view());
|
||||||
|
assert_relative_eq!(power[(0, 0,0)].re, dc_power, epsilon = Flt::EPSILON * nfft as Flt);
|
||||||
|
assert_relative_eq!(power[(1, 0,0)].re, 0.5, epsilon = Flt::EPSILON * nfft as Flt);
|
||||||
|
assert_relative_eq!(power[(1, 0,0)].im, 0.0, epsilon = Flt::EPSILON * nfft as Flt);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
use ndarray_rand::RandomExt;
|
||||||
|
// Test parseval's theorem for some random data
|
||||||
|
#[test]
|
||||||
|
fn test_parseval() {
|
||||||
|
|
||||||
|
const nfft: usize = 512;
|
||||||
|
let rect = Window::new(WindowType::Rect, nfft);
|
||||||
|
let mut ps = PowerSpectra::newFromWindow(rect);
|
||||||
|
|
||||||
|
// Start with a time signal
|
||||||
|
let t: Dmat = Dmat::random((nfft, 1), StandardNormal);
|
||||||
|
|
||||||
|
let tavg = t.sum()/(nfft as Flt);
|
||||||
|
let t_dc_power = tavg.powi(2);
|
||||||
|
// println!("dc power in time domain: {:?}", t_dc_power);
|
||||||
|
|
||||||
|
let signal_pwr = t.mapv(|t| t.powi(2)).sum()/(nfft as Flt);
|
||||||
|
// println!("Total signal power in time domain: {:?} ", signal_pwr);
|
||||||
|
|
||||||
|
let power = ps.compute(t.view());
|
||||||
|
// println!("freq domain power: {:?}", power);
|
||||||
|
|
||||||
|
let fpower = power.sum().abs();
|
||||||
|
|
||||||
|
assert_ulps_eq!(t_dc_power, power[(0,0,0)].abs(), epsilon = Flt::EPSILON * (nfft as Flt).powi(2));
|
||||||
|
assert_ulps_eq!(signal_pwr, fpower, epsilon = Flt::EPSILON * (nfft as Flt).powi(2));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test parseval's theorem for some random data
|
||||||
|
#[test]
|
||||||
|
fn test_parseval_with_window() {
|
||||||
|
|
||||||
|
const nfft: usize = 48000;
|
||||||
|
let rect = Window::new(WindowType::Hann, nfft);
|
||||||
|
let mut ps = PowerSpectra::newFromWindow(rect);
|
||||||
|
|
||||||
|
// Start with a time signal
|
||||||
|
let t: Dmat = Dmat::random((nfft, 1), StandardNormal);
|
||||||
|
|
||||||
|
let tavg = t.sum()/(nfft as Flt);
|
||||||
|
let t_dc_power = tavg.powi(2);
|
||||||
|
// println!("dc power in time domain: {:?}", t_dc_power);
|
||||||
|
|
||||||
|
let signal_pwr = t.mapv(|t| t.powi(2)).sum()/(nfft as Flt);
|
||||||
|
// println!("Total signal power in time domain: {:?} ", signal_pwr);
|
||||||
|
|
||||||
|
let power = ps.compute(t.view());
|
||||||
|
// println!("freq domain power: {:?}", power);
|
||||||
|
|
||||||
|
let fpower = power.sum().abs();
|
||||||
|
|
||||||
|
assert_ulps_eq!(t_dc_power, power[(0,0,0)].abs(), epsilon = Flt::EPSILON * (nfft as Flt).powi(2));
|
||||||
|
assert_ulps_eq!(signal_pwr, fpower, epsilon = Flt::EPSILON * (nfft as Flt).powi(2));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
168
src/ps/window.rs
Normal file
168
src/ps/window.rs
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
//! Window functions designed for Welch' method. Implementations are given for common window
|
||||||
|
//! functions, as well as optimal 'jump' values `R`, that result in a certain overlap.
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
use crate::config::*;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
use strum_macros::{Display};
|
||||||
|
|
||||||
|
fn linspace(nfft: usize) -> Dcol {
|
||||||
|
Dcol::linspace(0., nfft as Flt, nfft)
|
||||||
|
}
|
||||||
|
/// Von Hann window, often misnamed as the 'Hanning' window.
|
||||||
|
fn hann(nfft: usize) -> (Dcol, usize) {
|
||||||
|
let nfftF = nfft as Flt;
|
||||||
|
(
|
||||||
|
// The Window
|
||||||
|
linspace(nfft).mapv(|i| (pi * i / (nfftF+1.)).sin().powi(2)),
|
||||||
|
// The hopp size
|
||||||
|
(nfft) / 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn rect(nfft: usize) -> (Dcol, usize) {
|
||||||
|
(Dcol::ones(nfft), nfft)
|
||||||
|
}
|
||||||
|
fn blackman(N: usize) -> (Dcol, usize) {
|
||||||
|
let a0 = 7938. / 18608.;
|
||||||
|
let a1 = 9240. / 18608.;
|
||||||
|
let a2 = 1430. / 18608.;
|
||||||
|
let Nf = N as Flt;
|
||||||
|
let lin = linspace(N);
|
||||||
|
(
|
||||||
|
a0 - a1 * ((2. * pi / Nf) * lin.clone()).mapv(|x| x.cos())
|
||||||
|
+ a2 * ((4. * pi / Nf) * lin).mapv(|x| x.cos()),
|
||||||
|
// The hop size
|
||||||
|
N / 3,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn bartlett(nfft: usize) -> (Dcol, usize) {
|
||||||
|
let Nf = nfft as Flt;
|
||||||
|
(
|
||||||
|
(1. - (2. * (linspace(nfft) - (Nf - 1.) / 2.) / Nf)).mapv(|x| x.abs()),
|
||||||
|
// The hop size
|
||||||
|
nfft / 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn hamming(nfft: usize) -> (Dcol, usize) {
|
||||||
|
let alpha = 25.0 / 46.0;
|
||||||
|
let beta = (1. - alpha) / 2.;
|
||||||
|
let Nf = nfft as Flt;
|
||||||
|
(
|
||||||
|
alpha + 2. * beta * ((2. * pi / (Nf - 0.)) * linspace(nfft)).mapv(|x| x.sin()),
|
||||||
|
// The hop size
|
||||||
|
(nfft) / 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Window type descriptors. Used for storage
|
||||||
|
#[derive(Display, Clone, Debug)]
|
||||||
|
pub enum WindowType {
|
||||||
|
/// Von Hann window
|
||||||
|
Hann = 0,
|
||||||
|
/// Hamming window
|
||||||
|
Hamming = 1,
|
||||||
|
/// Boxcar / rectangular window
|
||||||
|
Rect = 2,
|
||||||
|
/// Bartlett window
|
||||||
|
Bartlett = 3,
|
||||||
|
/// Blackman window
|
||||||
|
Blackman = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Window (taper) computed from specified window type.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Window {
|
||||||
|
/// The enum from which it is generated
|
||||||
|
pub w: WindowType,
|
||||||
|
/// The actual window computed from specified nfft
|
||||||
|
pub win: Dcol,
|
||||||
|
/// The 'optimal' number of samples of shift per window average (hop size).
|
||||||
|
pub R: usize,
|
||||||
|
}
|
||||||
|
impl Window {
|
||||||
|
/// Create new window based on type and fft length. FFT length should be even. The (Window)
|
||||||
|
/// struct contains type and generated window in the `win` member.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// If nfft %2 != 0
|
||||||
|
pub fn new(w: WindowType, nfft: usize) -> Window {
|
||||||
|
if nfft % 2 != 0 {
|
||||||
|
panic!("Requires even nfft");
|
||||||
|
}
|
||||||
|
let (win, R) = match w {
|
||||||
|
WindowType::Hann => hann(nfft),
|
||||||
|
WindowType::Hamming => hamming(nfft),
|
||||||
|
WindowType::Rect => rect(nfft),
|
||||||
|
WindowType::Bartlett => bartlett(nfft),
|
||||||
|
WindowType::Blackman => blackman(nfft),
|
||||||
|
};
|
||||||
|
Window { w, win, R }
|
||||||
|
}
|
||||||
|
/// Convenience function that returns the length of the window.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.win.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn test_linspace() {
|
||||||
|
assert!(linspace(2)[0] == 0.);
|
||||||
|
// println!("{:?}", linspace(3));
|
||||||
|
assert!(linspace(3)[1] == 1.);
|
||||||
|
assert!(linspace(4).len() == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cola_hann() {
|
||||||
|
let nfft = 66;
|
||||||
|
let hann = Window::new(WindowType::Hann, nfft);
|
||||||
|
println!("{:?}", hann.win);
|
||||||
|
|
||||||
|
let mut hanntot = Dcol::zeros(hann.len() * 4);
|
||||||
|
|
||||||
|
assert!(2 * hann.R == nfft);
|
||||||
|
hanntot.slice_mut(s![0..nfft]).assign(&hann.win);
|
||||||
|
hanntot
|
||||||
|
.slice_mut(s![hann.R..nfft + hann.R])
|
||||||
|
.scaled_add(1.0, &hann.win);
|
||||||
|
|
||||||
|
hanntot
|
||||||
|
.slice_mut(s![nfft..2 * nfft])
|
||||||
|
.scaled_add(1.0, &hann.win);
|
||||||
|
hanntot
|
||||||
|
.slice_mut(s![nfft + hann.R..2 * nfft + hann.R])
|
||||||
|
.scaled_add(1.0, &hann.win);
|
||||||
|
|
||||||
|
hanntot
|
||||||
|
.slice_mut(s![2 * nfft..3 * nfft])
|
||||||
|
.scaled_add(1.0, &hann.win);
|
||||||
|
hanntot
|
||||||
|
.slice_mut(s![2 * nfft + hann.R..3 * nfft + hann.R])
|
||||||
|
.scaled_add(1.0, &hann.win);
|
||||||
|
|
||||||
|
println!("{:?}", hanntot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tets_cola_hamming() {
|
||||||
|
let nfft = 25;
|
||||||
|
let ham = Window::new(WindowType::Hamming, nfft);
|
||||||
|
let mut hamtot = Dcol::zeros(ham.len() * 3);
|
||||||
|
|
||||||
|
assert!(2 * ham.R == nfft);
|
||||||
|
hamtot.slice_mut(s![0..nfft]).scaled_add(1.0, &ham.win);
|
||||||
|
// println!("{:?}", hamtot);
|
||||||
|
hamtot
|
||||||
|
.slice_mut(s![ham.R..nfft + ham.R])
|
||||||
|
.scaled_add(1.0, &ham.win);
|
||||||
|
hamtot
|
||||||
|
.slice_mut(s![nfft..2 * nfft])
|
||||||
|
.scaled_add(1.0, &ham.win);
|
||||||
|
// println!("{:?}", hamtot);
|
||||||
|
// hantot.slice_mut(s![1+2*han1.R..nfft+1+2*han1.R]).scaled_add(1.0, &han2.win);
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,7 @@ use rand::prelude::*;
|
|||||||
use rand::rngs::ThreadRng;
|
use rand::rngs::ThreadRng;
|
||||||
use rand_distr::StandardNormal;
|
use rand_distr::StandardNormal;
|
||||||
|
|
||||||
|
/// Ratio between circumference and radius of a circle
|
||||||
const twopi: Flt = 2.0 * pi;
|
const twopi: Flt = 2.0 * pi;
|
||||||
|
|
||||||
/// Source for the signal generator. Implementations are sine waves, sweeps, noise.
|
/// Source for the signal generator. Implementations are sine waves, sweeps, noise.
|
||||||
@ -148,6 +149,7 @@ pub struct Siggen {
|
|||||||
// Output buffers (for filtered source signal)
|
// Output buffers (for filtered source signal)
|
||||||
chout_buf: Vec<Vec<Flt>>,
|
chout_buf: Vec<Vec<Flt>>,
|
||||||
}
|
}
|
||||||
|
#[cfg(feature="python-bindings")]
|
||||||
#[cfg_attr(feature = "python-bindings", pymethods)]
|
#[cfg_attr(feature = "python-bindings", pymethods)]
|
||||||
impl Siggen {
|
impl Siggen {
|
||||||
#[pyo3(name = "newWhiteNoise")]
|
#[pyo3(name = "newWhiteNoise")]
|
||||||
|
0
src/timebuffer.rs
Normal file
0
src/timebuffer.rs
Normal file
Loading…
Reference in New Issue
Block a user