From 8a573266dfa6296512900d0916b080814c88f2da Mon Sep 17 00:00:00 2001 From: "J.A. de Jong - Redu-Sone B.V., ASCEE V.O.F." Date: Sat, 26 Oct 2024 21:53:56 +0200 Subject: [PATCH] Removed dependency on ndarray_rand. Crate is not updated. Updated pyo3 to new version and updated enum pyclass derive macros. Switch to SmallRng for white noise random number generation. --- Cargo.toml | 17 +++++++++-------- src/daq/api/mod.rs | 5 +---- src/daq/datatype.rs | 5 +---- src/daq/mod.rs | 5 +---- src/daq/qty.rs | 4 +--- src/daq/record.rs | 14 +++++++++----- src/daq/streamerror.rs | 7 +------ src/math/mod.rs | 24 +++++++++++++++++++++++- src/ps/aps.rs | 16 +++++----------- src/ps/freqweighting.rs | 6 +----- src/ps/ps.rs | 8 ++++---- src/ps/window.rs | 5 +---- src/rt/ppm.rs | 4 ++-- src/siggen/source.rs | 9 ++++++--- src/siggen/sweep.rs | 6 +++--- src/slm/tw.rs | 10 +--------- 16 files changed, 69 insertions(+), 76 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4b14e04..3be8eb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,11 +16,15 @@ crate-type = ["cdylib", "rlib"] [dependencies] # Error handling -anyhow = "1.0.86" +anyhow = "1.0.91" # Numerics # Optional future feature for ndarray: blas -ndarray = { version = "0.15.6", features = ["rayon"] } +ndarray = { version = "0.16.1", features = ["rayon"] } + +# This is required for HDF5, as it apparently doesn't update anymore. +ndarray15p6 = { package = "ndarray", version = "0.15.6", features = ["rayon"] } + num = "0.4.3" # blas-src = { version = "0.8", features = ["openblas"] } # openblas-src = { version = "0.10", features = ["cblas", "system"] } @@ -29,15 +33,15 @@ num = "0.4.3" rayon = "1.10.0" # Python bindings -pyo3 = { version = "0.21.2", optional = true, features = [ +pyo3 = { version = "0.22.5", optional = true, features = [ "extension-module", "anyhow", ] } # Python bindings for Numpy arrays -numpy = { version = "0.21.0", optional = true } +numpy = { version = "0.22.0", optional = true } # White noise etc -rand = "0.8.5" +rand = { version = "0.8.5", features = ["small_rng"] } rand_distr = "0.4.3" # Cross-platform audio lib @@ -105,9 +109,6 @@ smallvec = "1.13.2" # Compile time constant floating point operations softfloat = "1.0.0" -[dev-dependencies] -ndarray-rand = "0.14.0" - [features] default = ["f64", "cpal-api", "record"] # Use this to test if everything works well in f32 diff --git a/src/daq/api/mod.rs b/src/daq/api/mod.rs index 487f7da..29f352b 100644 --- a/src/daq/api/mod.rs +++ b/src/daq/api/mod.rs @@ -32,10 +32,7 @@ pub trait Stream { } /// Stream API descriptor: type and corresponding text -// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy: -//#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] -// For now: -#[cfg_attr(feature = "python-bindings", pyclass)] +#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] #[derive(strum_macros::EnumMessage, Debug, Clone, PartialEq, Serialize, Deserialize, strum_macros::Display)] #[allow(dead_code)] pub enum StreamApiDescr { diff --git a/src/daq/datatype.rs b/src/daq/datatype.rs index 52679a9..9bfc193 100644 --- a/src/daq/datatype.rs +++ b/src/daq/datatype.rs @@ -7,10 +7,7 @@ use crate::config::*; /// Data type description for samples coming from a stream #[derive(strum_macros::EnumMessage, PartialEq, Copy, Debug, Clone, Serialize, Deserialize)] -// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy: -//#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] -// For now: -#[cfg_attr(feature = "python-bindings", pyclass)] +#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] #[allow(dead_code)] pub enum DataType { /// 32-bit floats diff --git a/src/daq/mod.rs b/src/daq/mod.rs index 9e9a3c7..408463a 100644 --- a/src/daq/mod.rs +++ b/src/daq/mod.rs @@ -50,10 +50,7 @@ use api::*; use crate::config::*; /// Stream types that can be started /// -// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy: -// #[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] -// For now: -#[cfg_attr(feature = "python-bindings", pyclass)] +#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] #[derive(PartialEq, Clone, Copy)] pub enum StreamType { /// Input-only stream diff --git a/src/daq/qty.rs b/src/daq/qty.rs index e5849d2..efb45ac 100644 --- a/src/daq/qty.rs +++ b/src/daq/qty.rs @@ -7,9 +7,7 @@ use strum_macros; use serde::{Serialize, Deserialize}; /// Physical quantities that are I/O of a Daq device. -// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy: -// #[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] -#[cfg_attr(feature = "python-bindings", pyclass)] +#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] #[derive(PartialEq, Serialize, Deserialize, strum_macros::EnumMessage, Debug, Clone, Copy)] #[allow(dead_code)] pub enum Qty { diff --git a/src/daq/record.rs b/src/daq/record.rs index fb6ad7d..3e81ca5 100644 --- a/src/daq/record.rs +++ b/src/daq/record.rs @@ -161,28 +161,32 @@ impl Recording { nchannels: usize, ) -> Result<()> { match data.getRaw() { + // The code below uses ndarray 0.15.6, which is the version required + // to communicate with rust-hdf5. It requires input to be C-ordered, + // or interleaved. This happens to be the default for ndarray as + // well. RawStreamData::Datai8(dat) => { - let arr = ndarray::ArrayView2::::from_shape((framesPerBlock, nchannels), dat)?; + let arr = ndarray15p6::ArrayView2::::from_shape((framesPerBlock, nchannels), dat)?; ds.write_slice(arr, (ctr, .., ..))?; } RawStreamData::Datai16(dat) => { let arr = - ndarray::ArrayView2::::from_shape((framesPerBlock, nchannels), dat)?; + ndarray15p6::ArrayView2::::from_shape((framesPerBlock, nchannels), dat)?; ds.write_slice(arr, (ctr, .., ..))?; } RawStreamData::Datai32(dat) => { let arr = - ndarray::ArrayView2::::from_shape((framesPerBlock, nchannels), dat)?; + ndarray15p6::ArrayView2::::from_shape((framesPerBlock, nchannels), dat)?; ds.write_slice(arr, (ctr, .., ..))?; } RawStreamData::Dataf32(dat) => { let arr = - ndarray::ArrayView2::::from_shape((framesPerBlock, nchannels), dat)?; + ndarray15p6::ArrayView2::::from_shape((framesPerBlock, nchannels), dat)?; ds.write_slice(arr, (ctr, .., ..))?; } RawStreamData::Dataf64(dat) => { let arr = - ndarray::ArrayView2::::from_shape((framesPerBlock, nchannels), dat)?; + ndarray15p6::ArrayView2::::from_shape((framesPerBlock, nchannels), dat)?; ds.write_slice(arr, (ctr, .., ..))?; } } diff --git a/src/daq/streamerror.rs b/src/daq/streamerror.rs index 5bc4a3c..c8fb8f2 100644 --- a/src/daq/streamerror.rs +++ b/src/daq/streamerror.rs @@ -2,13 +2,8 @@ use strum_macros::Display; use crate::config::*; /// Errors that happen in a stream +#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] #[derive(strum_macros::EnumMessage, PartialEq, Debug, Clone, Display, Copy)] - -// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy: -//#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] -// For now: -#[cfg_attr(feature = "python-bindings", pyclass)] - pub enum StreamError { /// Input overrun #[strum( diff --git a/src/math/mod.rs b/src/math/mod.rs index f0c36ab..1962d0e 100644 --- a/src/math/mod.rs +++ b/src/math/mod.rs @@ -2,7 +2,10 @@ //! //! use crate::config::*; -use ndarray::ArrayView1; +use ndarray::{ArrayView1, IntoDimension}; +use rand::{rngs::SmallRng, thread_rng, Rng, SeedableRng}; +use rand_distr::StandardNormal; +use smallvec::Array; /// Compute maximum value of an array of float values pub fn max(arr: ArrayView) -> Flt @@ -33,3 +36,22 @@ where { arr.fold(0., |acc, new| if new.abs() > acc { new.abs() } else { acc }) } + +/// Generate an array of *PSEUDO* random numbers from the standard normal +/// distribution. Typically used for white noise generation. To be used for +/// noise signals, not for anything that needs to be truly random. +/// +/// # Args +/// +/// - `shape` - Shape of the returned array +/// +pub fn randNormal(shape: Sh) -> ndarray::Array +where + Sh: ShapeBuilder, + D: Dimension +{ + // Explicit conversion to Fortran order + let mut rng = SmallRng::from_entropy(); + let shape = shape.f().into_shape_with_order(); + ArrayBase::from_shape_simple_fn(shape, || rng.sample(StandardNormal)) +} diff --git a/src/ps/aps.rs b/src/ps/aps.rs index eb5f879..54e7c8c 100644 --- a/src/ps/aps.rs +++ b/src/ps/aps.rs @@ -308,11 +308,9 @@ impl AvPowerSpectra { #[cfg(test)] mod test { use approx::assert_abs_diff_eq; - use ndarray_rand::rand_distr::Normal; - use ndarray_rand::RandomExt; use super::*; - use crate::config::*; + use crate::{config::*, math::randNormal}; use super::{ApsMode, AvPowerSpectra, CPSResult, Overlap, WindowType}; use Overlap::Percentage; @@ -359,8 +357,7 @@ mod test { let mut aps = AvPowerSpectra::new(settings); assert_eq!(aps.overlap_keep, 0); - let distr = Normal::new(1.0, 1.0).expect("Distribution cannot be built"); - let timedata_some = Dmat::random((nfft, 1), distr); + let timedata_some = randNormal((nfft,1)); let timedata_zeros = Dmat::zeros((nfft, 1)); // Clone here, as first_result reference is overwritten by subsequent @@ -382,8 +379,7 @@ mod test { #[test] fn test_tf1() { let nfft = 4800; - let distr = Normal::new(1.0, 1.0).unwrap(); - let mut timedata = Dmat::random((nfft, 1), distr); + let mut timedata = randNormal((nfft, 1)); timedata .push_column(timedata.column(0).mapv(|a| 2. * a).view()) .unwrap(); @@ -405,8 +401,7 @@ mod test { #[test] fn test_tf2() { let nfft = 4800; - let distr = Normal::new(1.0, 1.0).unwrap(); - let mut timedata = Dmat::random((nfft, 1), distr); + let mut timedata = randNormal((nfft, 1)); timedata .push_column(timedata.column(0).mapv(|a| 2. * a).view()) .unwrap(); @@ -431,8 +426,7 @@ mod test { #[test] fn test_ap() { let nfft = 1024; - let distr = Normal::new(1.0, 1.0).unwrap(); - let timedata = Dmat::random((150 * nfft, 1), distr); + let timedata = randNormal((150 * nfft, 1)); let timedata_mean_square = (&timedata * &timedata).sum() / (timedata.len() as Flt); for wt in [ diff --git a/src/ps/freqweighting.rs b/src/ps/freqweighting.rs index 5882604..fae58d3 100644 --- a/src/ps/freqweighting.rs +++ b/src/ps/freqweighting.rs @@ -2,11 +2,7 @@ use crate::config::*; use strum::IntoEnumIterator; use strum_macros::{Display, EnumIter, EnumMessage}; /// Sound level frequency weighting type (A, C, Z) - -// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy: -// #[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] -// For now: -#[cfg_attr(feature = "python-bindings", pyclass)] +#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] #[derive(Copy, Display, Debug, EnumMessage, Default, Clone, PartialEq, EnumIter)] pub enum FreqWeighting { /// A-weighting diff --git a/src/ps/ps.rs b/src/ps/ps.rs index 6b0847d..0514630 100644 --- a/src/ps/ps.rs +++ b/src/ps/ps.rs @@ -252,7 +252,6 @@ 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 { @@ -267,6 +266,8 @@ mod test { ) } + use crate::math::randNormal; + use super::*; #[test] /// Test whether DC part of single-sided FFT has right properties @@ -349,7 +350,6 @@ mod test { ); } - use ndarray_rand::RandomExt; // Test parseval's theorem for some random data #[test] fn test_parseval() { @@ -358,7 +358,7 @@ mod test { let mut ps = PowerSpectra::newFromWindow(rect); // Start with a time signal - let t: Dmat = Dmat::random((nfft, 1), StandardNormal); + let t: Dmat = randNormal((nfft,1)); let tavg = t.sum() / (nfft as Flt); let t_dc_power = tavg.powi(2); @@ -394,7 +394,7 @@ mod test { let mut ps = PowerSpectra::newFromWindow(window); // Start with a time signal - let t: Dmat = 2. * Dmat::random((nfft, 1), StandardNormal); + let t: Dmat = randNormal((nfft,1)); let tavg = t.sum() / (nfft as Flt); let t_dc_power = tavg.powi(2); diff --git a/src/ps/window.rs b/src/ps/window.rs index c3994f3..03dc1ff 100644 --- a/src/ps/window.rs +++ b/src/ps/window.rs @@ -73,11 +73,8 @@ fn hamming(N: usize) -> Dcol { /// * Blackman /// /// The [WindowType::default] is [WindowType::Hann]. +#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] #[derive(Display, Default, Copy, Clone, Debug, PartialEq, EnumMessage, EnumIter)] -// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy: -// #[cfg_attr(feature = "python-bindings", pyclass(eq))] -// For now: -#[cfg_attr(feature = "python-bindings", pyclass)] pub enum WindowType { /// Von Hann window #[default] diff --git a/src/rt/ppm.rs b/src/rt/ppm.rs index 1db36a8..8a52572 100644 --- a/src/rt/ppm.rs +++ b/src/rt/ppm.rs @@ -192,8 +192,8 @@ impl Drop for PPM { /// Enumerator denoting, for each channel what the level approximately is. Low, /// fine, high or clipped. -#[cfg_attr(feature = "python-bindings", pyclass)] -#[derive(Copy, Debug, Clone, Default)] +#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] +#[derive(Copy, Debug, PartialEq, Clone, Default)] pub enum ClipState { /// Level is rather low #[default] diff --git a/src/siggen/source.rs b/src/siggen/source.rs index 6101250..dde9251 100644 --- a/src/siggen/source.rs +++ b/src/siggen/source.rs @@ -42,7 +42,7 @@ impl Source { /// Create a white noise signal source pub fn newWhiteNoise() -> Source { Source { - src: Box::new(WhiteNoise {}), + src: Box::new(WhiteNoise { rng: SmallRng::from_entropy()}), } } @@ -121,11 +121,14 @@ impl SourceImpl for Silence { } /// White noise source. Can be colored by applying a color filter to the source #[derive(Clone, Debug)] -struct WhiteNoise {} +struct WhiteNoise { + // SmallRng is a cheap random number generator + rng: SmallRng +} impl SourceImpl for WhiteNoise { fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator) { sig.for_each(|s| { - *s = thread_rng().sample(StandardNormal); + *s = self.rng.sample(StandardNormal); }); } fn reset(&mut self, _fs: Flt) {} diff --git a/src/siggen/sweep.rs b/src/siggen/sweep.rs index b7fa9c3..c0ff477 100644 --- a/src/siggen/sweep.rs +++ b/src/siggen/sweep.rs @@ -1,6 +1,6 @@ //! Sweep signal generation code -use strum_macros::{Display, EnumMessage}; use strum::EnumMessage; +use strum_macros::{Display, EnumMessage}; use { crate::config::*, anyhow::{bail, Result}, @@ -10,8 +10,8 @@ const twopi: Flt = 2. * pi; /// Enumerator representing the type of sweep source to create. Used as /// parameter in [Siggen::newSweep]. -#[cfg_attr(feature = "python-bindings", pyclass)] -#[derive(Debug, Clone, Display, EnumMessage)] +#[cfg_attr(feature = "python-bindings", pyclass(eq, eq_int))] +#[derive(Debug, PartialEq, Clone, Display, EnumMessage)] pub enum SweepType { /// Forward only logarithmic sweep, repeats itself #[strum(message = "Forward logarithmic")] diff --git a/src/slm/tw.rs b/src/slm/tw.rs index 2367306..4dc70cd 100644 --- a/src/slm/tw.rs +++ b/src/slm/tw.rs @@ -3,11 +3,7 @@ use crate::config::*; use strum::EnumMessage; use strum_macros::Display; /// Time weighting to use in level detection of Sound Level Meter. -/// -// Do the following when Pyo3 0.22 can finally be used combined with rust-numpy: -// #[cfg_attr(feature = "python-bindings", pyclass(eq))] -// For now: -#[cfg_attr(feature = "python-bindings", pyclass)] +#[cfg_attr(feature = "python-bindings", pyclass(eq))] #[derive(Clone, Copy, Debug, PartialEq, Display)] pub enum TimeWeighting { // I know that the curly braces here are not required and add some @@ -37,10 +33,6 @@ pub enum TimeWeighting { #[cfg(feature = "python-bindings")] #[cfg_attr(feature = "python-bindings", pymethods)] impl TimeWeighting { - // This method is still required in Pyo3 0.21, not anymore in 0.22 - fn __eq__(&self, other: &Self) -> bool { - self == other - } fn __str__(&self) -> String { format!("{self}") }