lasprs 0.9.1

Library for Acoustic Signal Processing (Rust edition, with optional Python bindings via pyo3)
Documentation
use super::*;
use crate::config::*;
use anyhow::{bail, Error, Result};
use derive_builder::Builder;

/// All settings used for computing averaged power spectra using Welch' method.
#[derive(Builder, Clone, Debug)]
#[cfg_attr(feature = "python-bindings", pyclass)]
#[builder(build_fn(validate = "Self::validate", error = "Error"))]
pub struct ApsSettings {
    /// Mode of computation, see [ApsMode].
    #[builder(default)]
    pub mode: ApsMode,
    /// Overlap in time segments. See [Overlap].
    #[builder(default)]
    pub overlap: Overlap,
    /// Window applied to time segments. See [WindowType].
    #[builder(default)]
    pub windowType: WindowType,
    /// Kind of freqency weighting. Defaults to Z
    #[builder(default)]
    pub freqWeightingType: FreqWeighting,
    /// FFT Length
    pub nfft: usize,
    /// Sampling frequency
    pub fs: Flt,
}

impl ApsSettingsBuilder {
    fn validate(&self) -> Result<()> {
        if self.fs.is_none() {
            bail!("Sampling frequency not given");
        }
        let fs = self.fs.unwrap();

        if !fs.is_normal() {
            bail!("Sampling frequency not a normal number")
        }
        if fs <= 0.0 {
            bail!("Invalid sampling frequency given as parameter");
        }

        if self.nfft.is_none() {
            bail!("nfft not specified")
        };
        let nfft = self.nfft.unwrap();
        if nfft % 2 != 0 {
            bail!("NFFT should be even")
        }
        if nfft == 0 {
            bail!("Invalid NFFT, should be > 0.")
        }
        // Perform some checks on ApsMode
        if let Some(ApsMode::ExponentialWeighting { tau }) = self.mode {
            if tau <= 0.0 {
                bail!("Invalid time weighting constant [s]. Should be > 0 if given.");
            }
        }

        Ok(())
    }
}

#[cfg(feature = "python-bindings")]
#[cfg_attr(feature = "python-bindings", pymethods)]
impl ApsSettings {
    #[new]
    fn new(
        mode: ApsMode,
        overlap: Overlap,
        windowType: WindowType,
        freqWeightingType: FreqWeighting,
        nfft: usize,
        fs: Flt,
    ) -> ApsSettings {
        ApsSettings {
            mode,
            overlap,
            windowType,
            freqWeightingType,
            nfft,
            fs,
        }
    }
}

impl ApsSettings {
    /// Returns the amount of samples to keep in overlapping blocks of power
    /// spectra.
    pub fn get_overlap_keep(&self) -> usize {
        self.validate_get_overlap_keep().unwrap()
    }

    /// Returns the amount of samples to `keep` in the time buffer when
    /// overlapping time segments using [TimeBuffer].
    fn validate_get_overlap_keep(&self) -> Result<usize> {
        let nfft = self.nfft;
        let overlap_keep = match self.overlap {
            Overlap::Number { N } if N >= nfft => {
                bail!("Invalid overlap number of samples. Should be < nfft, which is {nfft}.")
            }
            // Keep 1 sample, if overlap is 1 sample etc.
            Overlap::Number { N } if N < nfft => N,

            // If overlap percentage is >= 100, or < 0.0 its an error
            Overlap::Percentage { pct } if !(0.0..100.).contains(&pct) => {
                bail!("Invalid overlap percentage. Should be >= 0. And < 100.")
            }
            // If overlap percentage is 0, this gives
            Overlap::Percentage { pct } => ((pct * nfft as Flt) / 100.) as usize,
            Overlap::NoOverlap {} => 0,
            _ => unreachable!(),
        };
        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.");
        }
        Ok(overlap_keep)
    }

    /// Return a reasonable acoustic default with a frequency resolution around
    /// ~ 10 Hz, where nfft is still an integer power of 2.
    ///
    /// # Errors
    ///
    /// If `fs` is something odd, i.e. < 1 kHz, or higher than 1 MHz.
    ///
    pub fn reasonableAcousticDefault(fs: Flt, mode: ApsMode) -> Result<ApsSettings> {
        if !(1e3..=1e6).contains(&fs) {
            bail!("Sampling frequency for reasonable acoustic data is >= 1 kHz and <= 1 MHz.");
        }
        let fs_div_10_rounded = (fs / 10.) as u32;

        // 2^30 is about 1 million. We search for a two-power of an nfft that is
        // the closest to fs/10. The frequency resolution is about fs/nfft.
        let nfft = (0..30).map(|i| 2u32.pow(i) - fs_div_10_rounded).fold(
            // Start wth a value that is always too large
            fs as u32 * 10,
            |cur, new| cur.min(new),
        ) as usize;

        Ok(ApsSettings {
            mode,
            fs,
            nfft,
            windowType: WindowType::default(),
            overlap: Overlap::default(),
            freqWeightingType: FreqWeighting::default(),
        })
    }

    /// Return sampling frequency
    pub fn fs(&self) -> Flt {
        self.fs
    }

    /// Return Nyquist frequency
    pub fn fnyq(&self) -> Flt {
        self.fs / 2.
    }

    /// Returns a single-sided frequency array corresponding to points in Power
    /// spectra computation.
    pub fn getFreq(&self) -> Array1<Flt> {
        let df = self.fs / self.nfft as Flt;
        let K = self.nfft / 2 + 1;
        Array1::linspace(0., (K - 1) as Flt * df, K)
    }
}