AvPowerSpectra not yet working, but going in right direction
This commit is contained in:
parent
0a847318f3
commit
565829f1fa
200
src/ps/aps.rs
200
src/ps/aps.rs
@ -7,7 +7,10 @@ use anyhow::{bail, Result};
|
|||||||
/// Can be provided as a percentage of the block size, or as a number of
|
/// Can be provided as a percentage of the block size, or as a number of
|
||||||
/// samples.
|
/// samples.
|
||||||
pub enum Overlap {
|
pub enum Overlap {
|
||||||
|
/// Overlap specified as a percentage of the total FFT length. Value should
|
||||||
|
/// be 0<=pct<100.
|
||||||
Percentage(Flt),
|
Percentage(Flt),
|
||||||
|
/// Number of samples to overlap
|
||||||
Number(usize),
|
Number(usize),
|
||||||
}
|
}
|
||||||
impl Default for Overlap {
|
impl Default for Overlap {
|
||||||
@ -16,42 +19,136 @@ impl Default for Overlap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Result from [AvPowerspectra.compute()]
|
||||||
|
pub enum ApsResult {
|
||||||
|
/// Returns all intermediate results. Useful when evolution over time should
|
||||||
|
/// be visible. I.e. in case of spectrograms, but also in case the
|
||||||
|
/// converging process to the average should be made visible.
|
||||||
|
AllIntermediateResults(Vec<CrossPowerSpecra>),
|
||||||
|
|
||||||
|
/// Give only last result back, the most recent value.
|
||||||
|
OnlyLastResult(CrossPowerSpecra),
|
||||||
|
|
||||||
|
/// No new data available. Nothing given back.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The 'mode' used in computing averaged power spectra. When providing data in
|
||||||
|
/// blocks to the [AvPowerSpectra] the resulting 'current estimate' responds
|
||||||
|
/// differently, depending on the model.
|
||||||
|
pub enum ApsMode {
|
||||||
|
/// Averaged over all data provided. New averages can be created by calling
|
||||||
|
/// `AvPowerSpectra::reset()`
|
||||||
|
AllAveraging,
|
||||||
|
/// In this mode, the `AvPowerSpectra` works a bit like a sound level meter,
|
||||||
|
/// where new data is weighted with old data, and old data exponentially
|
||||||
|
/// backs off. This mode only makes sense when `tau >> nfft/fs`
|
||||||
|
ExponentialWeighting {
|
||||||
|
/// Sampling frequency in [Hz]
|
||||||
|
fs: Flt,
|
||||||
|
/// Time weighting constant, its inverse is its approximate -3 dB point
|
||||||
|
/// for forgetting old history.
|
||||||
|
tau: Flt,
|
||||||
|
},
|
||||||
|
/// Spectrogram mode. Only returns the latest estimates.
|
||||||
|
Spectrogram,
|
||||||
|
}
|
||||||
|
impl Default for ApsMode {
|
||||||
|
fn default() -> Self {
|
||||||
|
ApsMode::AllAveraging
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Averaged power spectra computing engine
|
/// Averaged power spectra computing engine
|
||||||
struct AvPowerSpectra {
|
/// Used to compute power spectra estimations on
|
||||||
|
/// long datasets, where nfft << length of data. This way, the variance of a
|
||||||
|
/// single periodogram is suppressed with increasing number of averages.
|
||||||
|
///
|
||||||
|
/// For more information, see the book on numerical recipes.
|
||||||
|
///
|
||||||
|
pub struct AvPowerSpectra {
|
||||||
|
// Power spectra estimator for single block
|
||||||
ps: PowerSpectra,
|
ps: PowerSpectra,
|
||||||
overlap_skip: usize,
|
|
||||||
// Time constant for time-weighted power spectra. If None, it averages out
|
mode: ApsMode,
|
||||||
// over all data.
|
|
||||||
fs_tau: Option<Flt>,
|
// The number of samples to keep in the time buffer when overlapping time
|
||||||
/// The number of frames already used in the average
|
// blocks
|
||||||
|
overlap_keep: usize,
|
||||||
|
|
||||||
|
/// The number of blocks of length [self.nfft()] already used in the average
|
||||||
N: usize,
|
N: usize,
|
||||||
|
|
||||||
|
/// Storage for sample data.
|
||||||
|
timebuf: TimeBuffer,
|
||||||
|
|
||||||
// Current estimation of the power spectra
|
// Current estimation of the power spectra
|
||||||
cur_est: Cmat
|
cur_est: Cmat,
|
||||||
}
|
}
|
||||||
impl AvPowerSpectra {
|
impl AvPowerSpectra {
|
||||||
fn get_overlap_skip(nfft: usize, overlap: Option<Overlap>) -> Result<usize> {
|
/// The FFT Length of estimating (cross)power spectra
|
||||||
let overlap = overlap.unwrap_or_default();
|
pub fn nfft(&self) -> usize {
|
||||||
|
self.ps.nfft()
|
||||||
|
}
|
||||||
|
/// Returns the amount of samples to `keep` in the time buffer when
|
||||||
|
/// overlapping time segments using [TimeBuffer].
|
||||||
|
fn get_overlap_keep(nfft: usize, overlap: Overlap) -> Result<usize> {
|
||||||
|
let overlap_keep = match overlap {
|
||||||
|
Overlap::Number(i) if i >= nfft => {
|
||||||
|
bail!("Invalid overlap number of samples. Should be < nfft, which is {nfft}.")
|
||||||
|
}
|
||||||
|
// Keep 1 sample, if overlap is 1 sample etc.
|
||||||
|
Overlap::Number(i) if i < nfft => i,
|
||||||
|
|
||||||
let overlap_skip = match overlap {
|
// If overlap percentage is >= 100, or < 0.0 its an error
|
||||||
Overlap::Number(i) if i >= nfft => bail!("Invalid overlap number of samples"),
|
Overlap::Percentage(p) if p >= 100. || p < 0.0 => {
|
||||||
Overlap::Number(i) if i < nfft => nfft - i,
|
|
||||||
Overlap::Percentage(p) if p >= 100. => {
|
|
||||||
bail!("Invalid overlap percentage. Should be >= 0. And < 100.")
|
bail!("Invalid overlap percentage. Should be >= 0. And < 100.")
|
||||||
}
|
}
|
||||||
Overlap::Percentage(p) => nfft - ((p * nfft as Flt) / 100.) as usize,
|
// If overlap percentage is 0, this gives
|
||||||
|
Overlap::Percentage(p) => ((p * nfft as Flt) / 100.) as usize,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
if overlap_skip == 0 || overlap_skip > nfft {
|
if overlap_keep >= nfft {
|
||||||
bail!("Computed overlap results in invalid number of overlap samples. Please make sure the FFT length is large enough, when high overlap percentages are required.");
|
bail!("Computed overlap results in invalid number of overlap samples. Please make sure the FFT length is large enough, when high overlap percentages are required.");
|
||||||
}
|
}
|
||||||
Ok(overlap_skip)
|
Ok(overlap_keep)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create new averaged power spectra estimator for weighing over the full
|
||||||
|
/// amount of data supplied (no exponential spectra weighting) using
|
||||||
|
/// sensible defaults (Hann window, 50% overlap).
|
||||||
|
///
|
||||||
|
/// # Args
|
||||||
|
///
|
||||||
|
/// * `nfft` - FFT Length
|
||||||
|
pub fn new_simple(nfft: usize) -> Result<AvPowerSpectra> {
|
||||||
|
return AvPowerSpectra::new(nfft, None, None, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create power spectra estimator which weighs either all data
|
||||||
|
/// (`fs_tau=None`), or uses exponential weighting. Other parameters can be
|
||||||
|
/// provided, like the overlap (Some(Overlap)). Otherwise: all parameters
|
||||||
|
/// have sensible defaults.
|
||||||
|
///
|
||||||
|
/// # Exponential weighting
|
||||||
|
///
|
||||||
|
/// This can be used to `follow` a power spectrum as a function of time.
|
||||||
|
/// New data is weighted more heavily. Note that this way of looking at
|
||||||
|
/// spectra is not 'exact', and therefore should not be used for
|
||||||
|
/// spectrograms.
|
||||||
|
///
|
||||||
|
/// # Args
|
||||||
|
///
|
||||||
|
/// - `nfft` - The discrete Fourier Transform length used in the estimation.
|
||||||
|
/// - `windowtype` - Window Type. The window type to use Hann, etc.
|
||||||
|
/// - `overlap` - Amount of overlap in
|
||||||
|
/// - `mode` - The mode in which the
|
||||||
|
///
|
||||||
pub fn new(
|
pub fn new(
|
||||||
nfft: usize,
|
nfft: usize,
|
||||||
wt: Option<WindowType>,
|
windowtype: Option<WindowType>,
|
||||||
overlap: Option<Overlap>,
|
overlap: Option<Overlap>,
|
||||||
fs_tau: Option<Flt>,
|
mode: Option<ApsMode>,
|
||||||
) -> Result<AvPowerSpectra> {
|
) -> Result<AvPowerSpectra> {
|
||||||
if nfft % 2 != 0 {
|
if nfft % 2 != 0 {
|
||||||
bail!("NFFT should be even")
|
bail!("NFFT should be even")
|
||||||
@ -59,22 +156,73 @@ impl AvPowerSpectra {
|
|||||||
if nfft == 0 {
|
if nfft == 0 {
|
||||||
bail!("Invalid NFFT")
|
bail!("Invalid NFFT")
|
||||||
}
|
}
|
||||||
|
let windowtype = windowtype.unwrap_or_default();
|
||||||
|
let window = Window::new(windowtype, nfft);
|
||||||
|
let mode = mode.unwrap_or_default();
|
||||||
|
let overlap = overlap.unwrap_or_default();
|
||||||
|
|
||||||
if let Some(x) = fs_tau {
|
// Perform some checks on ApsMode
|
||||||
if x <= 0.0 {
|
if let ApsMode::ExponentialWeighting { fs, tau } = mode {
|
||||||
bail!("Invalid time weighting constant [s]. Should be > 0 if given.")
|
if fs <= 0.0 {
|
||||||
|
bail!("Invalid sampling frequency given as parameter");
|
||||||
|
}
|
||||||
|
if tau <= 0.0 {
|
||||||
|
bail!("Invalid time weighting constant [s]. Should be > 0 if given.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let window = Window::new(wt.unwrap_or_default(), nfft);
|
|
||||||
let ps = PowerSpectra::newFromWindow(window);
|
let ps = PowerSpectra::newFromWindow(window);
|
||||||
let overlap_skip = Self::get_overlap_skip(nfft, overlap)?;
|
let overlap_keep = Self::get_overlap_keep(nfft, overlap)?;
|
||||||
|
|
||||||
Ok(AvPowerSpectra {
|
Ok(AvPowerSpectra {
|
||||||
ps,
|
ps,
|
||||||
overlap_skip,
|
overlap_keep,
|
||||||
fs_tau,
|
mode,
|
||||||
N: 0,
|
N: 0,
|
||||||
cur_est: Cmat::default((0,0))
|
cur_est: Cmat::default((0, 0)),
|
||||||
|
timebuf: TimeBuffer::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
/// Computes average (cross)power spectra, and returns only the most recent
|
||||||
|
/// estimate, if any can be given back. Only gives back a result when enough
|
||||||
|
/// data is available.
|
||||||
|
///
|
||||||
|
/// # Args
|
||||||
|
///
|
||||||
|
/// * `timedata``: New available time data. Number of columns is number of
|
||||||
|
/// channels, number of rows is number of frames (samples per channel).
|
||||||
|
/// * `giveInterMediateResults`
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// If timedata.ncols() does not match number of columns in already present
|
||||||
|
/// data.
|
||||||
|
fn compute<'a, T>(&mut self, timedata: T, giveInterMediateResults: bool) -> ApsResult
|
||||||
|
where
|
||||||
|
T: AsArray<'a, Flt, Ix2>,
|
||||||
|
{
|
||||||
|
// Push new data in the time buffer.
|
||||||
|
self.timebuf.push(timedata);
|
||||||
|
|
||||||
|
// Iterate over all blocks that can come,
|
||||||
|
while let Some(timeblock) = self.timebuf.pop(self.nfft(), self.overlap_keep) {
|
||||||
|
// Compute cross-power spectra for current time block
|
||||||
|
let Cpsnew = self.ps.compute(&timeblock);
|
||||||
|
// Ok, mode-determined
|
||||||
|
match self.mode {
|
||||||
|
ApsMode::AllAveraging => {}
|
||||||
|
ApsMode::ExponentialWeighting { fs, tau } => {}
|
||||||
|
ApsMode::Spectrogram => {}
|
||||||
|
}
|
||||||
|
// if self.cur_est.len() > 0
|
||||||
|
}
|
||||||
|
ApsResult::None
|
||||||
|
}
|
||||||
|
/// See [AvPowerSpectra](compute())
|
||||||
|
fn compute_last<'a, T>(&mut self, timedata: T) -> ApsResult
|
||||||
|
where
|
||||||
|
T: AsArray<'a, Flt, Ix2>,
|
||||||
|
{
|
||||||
|
return self.compute(timedata, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,162 @@
|
|||||||
|
//! A time buffer is used as intermediate storage for samples, to spare up
|
||||||
|
//! enough values to do 'something' with. Typical application: getting enough
|
||||||
|
//! samples to perform an FFT. The use case is computing average power spectra.
|
||||||
use super::*;
|
use super::*;
|
||||||
/// A time buffer is used as intermediate storage for samples, to spare up
|
use crate::config::*;
|
||||||
/// enough values to do 'something' with. Typical application: getting enough
|
use std::collections::VecDeque;
|
||||||
/// samples to perform an FFT. The use case is computing average power spectra.
|
|
||||||
///
|
|
||||||
struct TimeBuffer {
|
|
||||||
|
|
||||||
}
|
/// TimeBuffer, storage to add blocks of data in a ring buffer, that can be
|
||||||
|
/// extracted by blocks of other size. Also, we can keep samples in a buffer to
|
||||||
|
/// create, for example, overlapping windows of time data.
|
||||||
|
pub struct TimeBuffer {
|
||||||
|
data: Vec<VecDeque<Flt>>,
|
||||||
|
}
|
||||||
|
impl TimeBuffer {
|
||||||
|
/// Allocate new empty TimeBuffer.
|
||||||
|
pub fn new() -> TimeBuffer {
|
||||||
|
TimeBuffer { data: Vec::new() }
|
||||||
|
}
|
||||||
|
/// Reset the buffer. Clears all data in it.
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
*self = Self::new();
|
||||||
|
}
|
||||||
|
/// Push new data to the buffer. Keep
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// When the number of columns does not match with the first time this
|
||||||
|
/// method is called.
|
||||||
|
pub fn push<'a, T>(&mut self, new_data: T)
|
||||||
|
where
|
||||||
|
T: AsArray<'a, Flt, Ix2>,
|
||||||
|
{
|
||||||
|
let new_data = new_data.into();
|
||||||
|
let nch = new_data.shape()[1];
|
||||||
|
if self.data.len() == 0 {
|
||||||
|
self.data = vec![VecDeque::new(); nch];
|
||||||
|
}
|
||||||
|
if self.data.len() != nch {
|
||||||
|
panic!("BUG: Number of channels does not match with time buffer");
|
||||||
|
}
|
||||||
|
for (new_dat, buf) in new_data.columns().into_iter().zip(self.data.iter_mut()) {
|
||||||
|
let slice = new_dat.as_slice().unwrap();
|
||||||
|
buf.extend(slice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Return the number of samples that is currently stored
|
||||||
|
pub fn nsamples(&self) -> usize {
|
||||||
|
if let Some(q) = self.data.get(0) {
|
||||||
|
return q.len();
|
||||||
|
};
|
||||||
|
0
|
||||||
|
}
|
||||||
|
/// Pop samples from the queue, only returns Some(Dmat) when there are
|
||||||
|
/// enough samples to return. Never returns less samples than
|
||||||
|
/// `nsamples_requested`.
|
||||||
|
pub fn pop(&mut self, nsamples_requested: usize, nsamples_keep: usize) -> Option<Dmat> {
|
||||||
|
if self.data.len() == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if nsamples_keep > nsamples_requested {
|
||||||
|
panic!("BUG: Cannot keep more samples than requested to return");
|
||||||
|
}
|
||||||
|
|
||||||
|
debug_assert!(self.data.len() > 0);
|
||||||
|
let c1 = unsafe { self.data.get_unchecked(0) };
|
||||||
|
|
||||||
|
let nsamples_available = c1.len();
|
||||||
|
debug_assert!(nsamples_available == self.nsamples());
|
||||||
|
if nsamples_available < nsamples_requested {
|
||||||
|
println!("Less available than requested");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Number of channels
|
||||||
|
let nchannels = self.data.len();
|
||||||
|
|
||||||
|
// Create output array, .f() means fortran contiguous array
|
||||||
|
let mut res = Dmat::zeros((nsamples_requested, nchannels).f());
|
||||||
|
{
|
||||||
|
for (mut col, dat) in res.columns_mut().into_iter().zip(&mut self.data) {
|
||||||
|
// This expect should never happen, as above it is called with .f().
|
||||||
|
let col_slice = col
|
||||||
|
.as_slice_mut()
|
||||||
|
.expect("Data is not contiguous on the sample axis!");
|
||||||
|
|
||||||
|
// Split data of current channel in two slices of the underlying data
|
||||||
|
let (dat_slice1, dat_slice2) = dat.as_slices();
|
||||||
|
|
||||||
|
if dat_slice1.len() >= nsamples_requested {
|
||||||
|
// Only copy from the first slice
|
||||||
|
col_slice.copy_from_slice(&dat_slice1[..nsamples_requested]);
|
||||||
|
} else {
|
||||||
|
let slice1len = dat_slice1.len();
|
||||||
|
// Copy from first slice
|
||||||
|
col_slice[..slice1len].copy_from_slice(&dat_slice1);
|
||||||
|
// Copy rest from second slice
|
||||||
|
col_slice[slice1len..nsamples_requested]
|
||||||
|
.copy_from_slice(&dat_slice2[..nsamples_requested - slice1len]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// From documentation:
|
||||||
|
// Removes the specified range from the deque in bulk, returning all removed
|
||||||
|
// elements as an iterator. If the iterator is dropped before being fully
|
||||||
|
// consumed, it drops the remaining removed elements.
|
||||||
|
dat.drain(0..nsamples_requested - nsamples_keep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let c1 = unsafe { self.data.get_unchecked(0) };
|
||||||
|
let nsamples_available = c1.len();
|
||||||
|
debug_assert!(self
|
||||||
|
.data
|
||||||
|
.iter()
|
||||||
|
.map(|ch| ch.len()) // Iterator over all channels, transform to the amount of samples in each channel.
|
||||||
|
.all(|v| v == nsamples_available));
|
||||||
|
|
||||||
|
Some(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::{Dcol, Dmat};
|
||||||
|
use ndarray::s;
|
||||||
|
|
||||||
|
use super::TimeBuffer;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timebuffer1() {
|
||||||
|
let mut t1 = Dmat::zeros((10, 1));
|
||||||
|
t1[[9, 0]] = 1.0;
|
||||||
|
let mut tb = TimeBuffer::new();
|
||||||
|
|
||||||
|
tb.push(&t1);
|
||||||
|
let out = tb.pop(10, 2).unwrap();
|
||||||
|
assert_eq!(out.len(), 10);
|
||||||
|
let out = tb.pop(3, 0);
|
||||||
|
assert!(out.is_none());
|
||||||
|
let out = tb.pop(2, 0).unwrap();
|
||||||
|
// println!("{:?}", out);
|
||||||
|
assert_eq!(out.len(), 2);
|
||||||
|
assert_eq!(tb.nsamples(), 0);
|
||||||
|
assert_eq!(out[[1, 0]], 1.0);
|
||||||
|
|
||||||
|
tb.reset();
|
||||||
|
assert_eq!(tb.nsamples(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timebuffer2() {
|
||||||
|
let mut tb = TimeBuffer::new();
|
||||||
|
let tlin = Dcol::linspace(0., 100., 101);
|
||||||
|
// println!("{:?}", tlin);
|
||||||
|
let mut t2 = Dmat::zeros((101, 0));
|
||||||
|
t2.push_column(tlin.view()).unwrap();
|
||||||
|
t2.push_column(tlin.view()).unwrap();
|
||||||
|
tb.push(&t2);
|
||||||
|
assert_eq!(tb.nsamples(), 101);
|
||||||
|
|
||||||
|
let tres = tb.pop(50, 49).unwrap();
|
||||||
|
assert_eq!(tres.shape(), [50, 2]);
|
||||||
|
assert_eq!(t2.slice(s![..50, ..2]), tres);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user