Compare commits

..

6 Commits

Author SHA1 Message Date
cf672dc9d7 Made it such that the signal generator always starts from the start (pos 0)
All checks were successful
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in -5m57s
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped
2024-12-03 11:24:20 +01:00
4cd390871a fully implemented AvSweepPowerSpectra
All checks were successful
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in -5m53s
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped
2024-12-03 10:47:10 +01:00
6cdc182b57 Added siggen data to measurement metadata + fixed sweeps
All checks were successful
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in -5m51s
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped
2024-11-30 14:25:22 +01:00
bfb23ad698 fixed threshold at 1e-13
All checks were successful
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in -4m59s
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped
2024-11-06 17:08:51 +01:00
a986a6b9cd Added the aps calculator where blocks are neglectd below a certain threshold. Warning, this is sweep specific and is currently the method implemented in lasp_measurement. Fine for muZ, not necessarily for other stuff
All checks were successful
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in -4m53s
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped
2024-11-01 16:16:06 +01:00
9caf5fe387 Added amplitude envelope to sweep signals, with associated methods
All checks were successful
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in -4m47s
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped
2024-11-01 14:17:31 +01:00
11 changed files with 734 additions and 278 deletions

View File

@ -1,145 +0,0 @@
# Installation of LASP - Linux (Ubuntu-based)
## Prerequisites
Run the following on the command line to install all prerequisites on
Debian-based Linux, x86-64:
- `sudo apt install python3-pip libfftw3-3 libopenblas-base libusb-1.0-0 libpulse0`
## Installation from wheel (recommended for non-developers)
Go to: [LASP releases](https://code.ascee.nl/ASCEE/lasp/releases/latest/) and
download the latest `.whl`. Then run:
- `pip install lasp-*-linux_x86_64.whl`
## From source (Ubuntu-based)
### Prerequisites
Run the following one-liner:
- `sudo apt install -y git python3 python3-virtualenv python3-venv libopenblas-dev python3-pip libfftw3-dev libusb-1.0-0-dev libpulse-dev python3-build`
If building RtAudio with the ALSA backend, you will also require the following packages:
- `sudo apt install libclalsadrv-dev`
If building RtAudio with the Jack Audio Connection Kit (JACK) backend, you will also require the following packages:
- `sudo apt install libjack-jackd2-dev`
### Download & build
- `$ git clone --recursive https://code.ascee.nl/ASCEE/lasp.git`
- `$ cd lasp`
- `pip install -e .`
# Building and installation for Raspberry Pi (Raspberry Pi OS)
Run the following on the command line to install all prerequisites on
Raspberry Pi OS:
- `sudo apt install libfftw3-dev libopenblas64-dev libhdf5-dev libclalsadrv-dev`
In a virtualenv: install `build`
- `$ pip install build`
Then run:
- `$ git clone --recursive https://code.ascee.nl/ASCEE/lasp.git`
- `$ cd lasp`
- `$ pyproject-build`
Which will generate a `whl` in the `dist` folder, that is redistributable for Raspberry Pis that run Raspberry Pi OS.
When installing the `whl`, it appears that H5PY takes quite some time to install. To follow this process, run it it verbose mode.
# Installation - (x86_64) Windows (with WinPython), build with MSYS2
## Prerequisites
- Download and install [WinPython](https://winpython.github.io)
## From wheel
- Download latest wheel from [LASP releases](https://code.ascee.nl/ASCEE/lasp/releases/latest/) and
download the latest `.whl`. Then install with `pip`.
## From source
- Download and install [MSYS2](https://msys2.org). Make sure to install the
x86_64 version.
- When unzipping WinPython, make sure to choose a proper and simple path, i.e.
C:\winpython
- Download and install [Git for Windows](https://git-scm.com)
- Open an MSYS2 **MINGW64** terminal, and install some tools we require:
- `$ pacman -S git`
- Create a new virtualenv:
- `$ /c/winpython/<py-distr-dir>/python.exe -m venv venv`
- Add the venv-python to the path (eases a lot of commands)
- `$ export PATH=$PATH:~/venv/Scripts`
- Install `build`:
- `$ pip install build`
- Clone LASP:
- `$ git clone --recurse-submodules https://code.ascee.nl/ascee/lasp && cd lasp`
- If run for the first time, we have to install the libraries we depend on in
MSYS2 (this only has to be done on a fresh MSYS2 installation):
- `$ scripts/install_msys2_builddeps.sh`
- Copy over required DLL's to be included in distribution:
- `scripts/copy_windows_dlls.sh`
- And... build!
- `pyproject-build`
- Lastly: the generated wheel can be installed in the current virtualenv:
- `pip install dist/lasp*.whl`
# Documentation
## Online
[Online LASP documentation](https://lasp.ascee.nl/).
## In directory (Linux/Debian)
`$ sudo apt install doxygen graphviz`
`$ pip install doxypypy`
While still in lasp dir:
`$ doxygen`
This will build the documentation. It can be read by:
`$ <YOUR-BROWSER> doc/html/index.html`
# Usage
- See examples directories for IPython notebooks.
- Please refer to the [documentation](https://lasp.ascee.nl/) for features.
# Development docs
## Bumping version number
When bumping the version number, please update the number in
- `pyproject.toml`
- `CMakeLists.txt`
Then, create a commit with tag `vX.X.X`, and push it.
## Updating to latest version (editable mode)
When updating to the latest version of LASP in editable mode:
```bash
- $ git pull
- $ git submodule update
- $ pip install -e . -v
```

156
README.md
View File

@ -1,11 +1,13 @@
Library for Acoustic Signal Processing
======================================
Welcome to LASP: Library for Acoustic Signal Processing. LASP is a C++ library
with a Python interface which is supposed to acquire and process (multi) sensor data in real time on a PC and output results.
Current features that are implemented:
- Communication with data acquisition (DAQ) devices, of which:
- Internal sound cards via the [RtAudio](http://www.music.mcgill.ca/~gary/rtaudio) backend. Many thanks to Gary P. Scavone et al.
- [Measurement Computing](https://www.mccdaq.com) [DT9838A](https://www.mccdaq.com/Products/Sound-Vibration-DAQ/DT9837) signal analyzer.
@ -29,8 +31,10 @@ Current features that are implemented:
- Spectra data smoothing algorithms
- Sensor calibration for microphones
Future features (wish-list)
- Conventional and delay-and-sum beam-forming algorithms
- Impedance tube measurement processing
@ -38,7 +42,151 @@ For now, the source code is well-documented on [lasp.ascee.nl](https://lasp.asce
additional documentation (the math behind it). This is maintained
in a sister repository [lasp-doc](https://code.ascee.nl/ascee/lasp-doc).
For installation instructions, please visit
[INSTALLATON.md](https://code.ascee.nl/ASCEE/lasp/src/branch/master/INSTALLATION.md).
If you have any question(s), please feel free to contact us:
[email](mailto:info@ascee.nl).
If you have any question(s), please feel free to contact us: [email](info@ascee.nl).
# Installation - Linux (Ubuntu-based)
## Prerequisites
Run the following on the command line to install all prerequisites on
Debian-based Linux, x86-64:
- `sudo apt install python3-pip libfftw3-3 libopenblas-base libusb-1.0-0 libpulse0`
## Installation from wheel (recommended for non-developers)
Go to: [LASP releases](https://code.ascee.nl/ASCEE/lasp/releases/latest/) and
download the latest `.whl`. Then run:
- `pip install lasp-*-linux_x86_64.whl`
## From source (Ubuntu-based)
### Prerequisites
Run the following one-liner:
- `sudo apt install -y git python3 python3-virtualenv python3-venv libopenblas-dev python3-pip libfftw3-dev libusb-1.0-0-dev libpulse-dev python3-build`
If building RtAudio with the ALSA backend, you will also require the following packages:
- `sudo apt install libclalsadrv-dev`
If building RtAudio with the Jack Audio Connection Kit (JACK) backend, you will also require the following packages:
- `sudo apt install libjack-jackd2-dev`
### Download & build
- `$ git clone --recursive https://code.ascee.nl/ASCEE/lasp.git`
- `$ cd lasp`
- `pip install -e .`
# Building and installation for Raspberry Pi (Raspberry Pi OS)
Run the following on the command line to install all prerequisites on
Raspberry Pi OS:
- `sudo apt install libfftw3-dev libopenblas64-dev libhdf5-dev libclalsadrv-dev`
In a virtualenv: install `build`
- `$ pip install build`
Then run:
- `$ git clone --recursive https://code.ascee.nl/ASCEE/lasp.git`
- `$ cd lasp`
- `$ pyproject-build`
Which will generate a `whl` in the `dist` folder, that is redistributable for Raspberry Pis that run Raspberry Pi OS.
When installing the `whl`, it appears that H5PY takes quite some time to install. To follow this process, run it it verbose mode.
# Installation - (x86_64) Windows (with WinPython), build with MSYS2
## Prerequisites
- Download and install [WinPython](https://winpython.github.io)
## From wheel
- Download latest wheel from [LASP releases](https://code.ascee.nl/ASCEE/lasp/releases/latest/) and
download the latest `.whl`. Then install with `pip`.
## From source
- Download and install [MSYS2](https://msys2.org). Make sure to install the
x86_64 version.
- When unzipping WinPython, make sure to choose a proper and simple path, i.e.
C:\winpython
- Download and install [Git for Windows](https://git-scm.com)
- Open an MSYS2 **MINGW64** terminal, and install some tools we require:
- `$ pacman -S git`
- Create a new virtualenv:
- `$ /c/winpython/<py-distr-dir>/python.exe -m venv venv`
- Add the venv-python to the path (eases a lot of commands)
- `$ export PATH=$PATH:~/venv/Scripts`
- Install `build`:
- `$ pip install build`
- Clone LASP:
- `$ git clone --recurse-submodules https://code.ascee.nl/ascee/lasp && cd lasp`
- If run for the first time, we have to install the libraries we depend on in
MSYS2 (this only has to be done on a fresh MSYS2 installation):
- `$ scripts/install_msys2_builddeps.sh`
- Copy over required DLL's to be included in distribution:
- `scripts/copy_windows_dlls.sh`
- And... build!
- `pyproject-build`
- Lastly: the generated wheel can be installed in the current virtualenv:
- `pip install dist/lasp*.whl`
# Documentation
## Online
[Online LASP documentation](https://lasp.ascee.nl/).
## In directory (Linux/Debian)
`$ sudo apt install doxygen graphviz`
`$ pip install doxypypy`
While still in lasp dir:
`$ doxygen`
This will build the documentation. It can be read by:
`$ <YOUR-BROWSER> doc/html/index.html`
# Usage
- See examples directories for IPython notebooks.
- Please refer to the [documentation](https://lasp.ascee.nl/) for features.
# Development docs
## Bumping version number
When bumping the version number, please update the number in
- `pyproject.toml`
- `CMakeLists.txt`
Then, create a commit with tag `vX.X.X`, and push it.
## Updating to latest version (editable mode)
When updating to the latest version of LASP in editable mode:
```bash
- $ git pull
- $ git submodule update
- $ pip install -e . -v
```

View File

@ -10,7 +10,8 @@ PowerSpectra::PowerSpectra(const us nfft, const Window::WindowType w)
: PowerSpectra(Window::create(w, nfft)) {}
PowerSpectra::PowerSpectra(const vd &window)
: nfft(window.size()), _fft(nfft), _window(window) {
: nfft(window.size()), _fft(nfft), _window(window)
{
d win_pow = arma::sum(window % window) / window.size();
@ -21,7 +22,8 @@ PowerSpectra::PowerSpectra(const vd &window)
DEBUGTRACE_PRINT(win_pow);
}
ccube PowerSpectra::compute(const dmat &input) {
ccube PowerSpectra::compute(const dmat &input)
{
/// Run very often. Silence this one.
/* DEBUGTRACE_ENTER; */
@ -34,11 +36,13 @@ ccube PowerSpectra::compute(const dmat &input) {
cmat rfft = _fft.fft(input_tmp);
ccube output(rfft.n_rows, input.n_cols, input.n_cols);
for (us i = 0; i < input.n_cols; i++) {
for (us i = 0; i < input.n_cols; i++)
{
/// This one can be run in parallel without any problem. Note that it is
/// the inner loop that is run in parallel.
#pragma omp parallel for
for (us j = 0; j < input.n_cols; j++) {
for (us j = 0; j < input.n_cols; j++)
{
output.slice(j).col(i) =
_scale_fac * (rfft.col(i) % arma::conj(rfft.col(j)));
@ -52,37 +56,46 @@ ccube PowerSpectra::compute(const dmat &input) {
AvPowerSpectra::AvPowerSpectra(const us nfft, const Window::WindowType w,
const d overlap_percentage,
const d time_constant_times_fs)
: _ps(nfft, w) {
: _ps(nfft, w)
{
DEBUGTRACE_ENTER;
if (overlap_percentage >= 100 || overlap_percentage < 0) {
if (overlap_percentage >= 100 || overlap_percentage < 0)
{
throw rte("Overlap percentage should be >= 0 and < 100");
}
_overlap_keep = (nfft * overlap_percentage) / 100;
DEBUGTRACE_PRINT(_overlap_keep);
if (_overlap_keep >= nfft) {
if (_overlap_keep >= nfft)
{
throw rte("Overlap is too high. Results in no jump. Please "
"choose a smaller overlap percentage or a higher nfft");
}
if (time_constant_times_fs < 0) {
if (time_constant_times_fs < 0)
{
_mode = Mode::Averaging;
} else if (time_constant_times_fs == 0) {
}
else if (time_constant_times_fs == 0)
{
_mode = Mode::Spectrogram;
} else {
}
else
{
_mode = Mode::Leaking;
_alpha = d_exp(-static_cast<d>((nfft - _overlap_keep) / time_constant_times_fs));
DEBUGTRACE_PRINT(_alpha);
}
}
void AvPowerSpectra::reset() {
void AvPowerSpectra::reset()
{
_timeBuf.reset();
_est.reset();
n_averages=0;
_n_averages = 0;
}
ccube AvPowerSpectra::compute(const dmat &timedata) {
ccube AvPowerSpectra::compute(const dmat &timedata)
{
DEBUGTRACE_ENTER;
DEBUGTRACE_PRINT(timedata.n_rows);
@ -91,32 +104,42 @@ ccube AvPowerSpectra::compute(const dmat &timedata) {
_timeBuf.push(timedata);
bool run_once = false;
while (_timeBuf.size() >= _ps.nfft) {
while (_timeBuf.size() >= _ps.nfft)
{
DEBUGTRACE_PRINT((int)_mode);
dmat samples = _timeBuf.pop(_ps.nfft, _overlap_keep);
switch (_mode) {
switch (_mode)
{
case (Mode::Spectrogram):
DEBUGTRACE_PRINT("Spectrogram mode");
_est = _ps.compute(samples);
break;
case (Mode::Averaging):
DEBUGTRACE_PRINT("Averaging mode");
n_averages++;
if (n_averages == 1) {
_n_averages++;
if (_n_averages == 1)
{
_est = _ps.compute(samples);
} else {
_est = (static_cast<d>(n_averages - 1) / n_averages) * _est +
_ps.compute(samples) / n_averages;
}
else
{
_est = (static_cast<d>(_n_averages - 1) / _n_averages) * _est +
_ps.compute(samples) / _n_averages;
}
break;
case (Mode::Leaking):
DEBUGTRACE_PRINT("Leaking mode");
if (arma::size(_est) == arma::size(0, 0, 0)) {
if (arma::size(_est) == arma::size(0, 0, 0))
{
_est = _ps.compute(samples);
} else {
}
else
{
_est = _alpha * _est + (1 - _alpha) * _ps.compute(samples);
}
break;
@ -128,6 +151,149 @@ ccube AvPowerSpectra::compute(const dmat &timedata) {
/// Othewise, we return an empty ccube.
return run_once ? _est : ccube();
}
ccube AvPowerSpectra::get_est() const {
ccube AvPowerSpectra::get_est() const
{
return _est;
}
AvSweepPowerSpectra::AvSweepPowerSpectra(const us nfft, const Window::WindowType w,
const d overlap_percentage,
const d time_constant_times_fs,
const vd *A)
: _nfft(nfft), _ps(nfft, w)
{
DEBUGTRACE_ENTER;
if (overlap_percentage >= 100 || overlap_percentage < 0)
{
throw rte("Overlap percentage should be >= 0 and < 100");
}
if(A == nullptr) {
_A = vd(nfft, arma::fill::ones);
} else {
if(A->n_elem != (nfft / 2 + 1)) {
throw rte("Invalid length given for amplitude coefficients");
}
_A = arma::square(*A);
}
_overlap_keep = (nfft * overlap_percentage) / 100;
DEBUGTRACE_PRINT(_overlap_keep);
if (_overlap_keep >= nfft)
{
throw rte("Overlap is too high. Results in no jump. Please "
"choose a smaller overlap percentage or a higher nfft");
}
if (time_constant_times_fs < 0)
{
_mode = Mode::Averaging;
}
else if (time_constant_times_fs == 0)
{
_mode = Mode::Spectrogram;
}
else
{
_mode = Mode::Leaking;
_alpha = d_exp(-static_cast<d>((nfft - _overlap_keep) / time_constant_times_fs));
DEBUGTRACE_PRINT(_alpha);
}
}
void AvSweepPowerSpectra::reset()
{
_timeBuf.reset();
_est.reset();
_n_averages.reset();
}
ccube AvSweepPowerSpectra::compute(const dmat &timedata)
{
DEBUGTRACE_ENTER;
DEBUGTRACE_PRINT(timedata.n_rows);
DEBUGTRACE_PRINT(_ps.nfft);
_timeBuf.push(timedata);
bool run_once = false;
while (_timeBuf.size() >= _ps.nfft)
{
DEBUGTRACE_PRINT((int)_mode);
dmat samples = _timeBuf.pop(_ps.nfft, _overlap_keep);
us ncols = timedata.n_cols;
switch (_mode)
{
case (Mode::Spectrogram):
DEBUGTRACE_PRINT("Spectrogram mode");
_est = _ps.compute(samples);
break;
case (Mode::Averaging):
{
DEBUGTRACE_PRINT("Averaging mode");
ccube temp = _ps.compute(samples);
us nfreq = arma::size(temp)[0];
if (_est.size() == 0) {
// Initialize empty
_est = ccube(nfreq, ncols, ncols, arma::fill::zeros);
_n_averages = vd(nfreq, arma::fill::zeros);
}
vd corr_pow = arma::real(temp.slice(0).col(0)) / _A;
d threshold = pow(10.0, log10(arma::max(corr_pow)*arma::median(corr_pow))/2.0);
// d threshold = 1e-13;
for (us f=0; f < nfreq; f++)
{
if (real(temp(f, 0, 0)) / _A(f) > threshold)
{
_n_averages[f]++;
if (_n_averages[f] == 1)
{
_est.row(f) = temp.row(f);
}
else
{
_est.row(f) = (static_cast<d>(_n_averages[f] - 1) / _n_averages[f]) * _est.row(f) +
temp.row(f) / _n_averages[f];
}
}
}
}
break;
case (Mode::Leaking):
DEBUGTRACE_PRINT("Leaking mode");
if (arma::size(_est) == arma::size(0, 0, 0))
{
_est = _ps.compute(samples);
}
else
{
_est = _alpha * _est + (1 - _alpha) * _ps.compute(samples);
}
break;
} // end switch mode
run_once = true;
}
/// Return a copy of current estimator in case we have done one update.
/// Othewise, we return an empty ccube.
return run_once ? _est : ccube();
}
ccube AvSweepPowerSpectra::get_est() const
{
return _est;
}
vd AvSweepPowerSpectra::get_A() const
{
return _A;
}

View File

@ -81,7 +81,7 @@ class AvPowerSpectra {
Mode _mode;
d _alpha; // Only valid in case of 'Leaking'
us n_averages = 0; // Only valid in case of 'Averaging'
us _n_averages = 0; // Only valid in case of 'Averaging'
PowerSpectra _ps;
/**
@ -165,3 +165,112 @@ public:
us exactOverlapSamples() const { return _ps.nfft - _overlap_keep; }
};
/** @} */
/**
* @brief Estimate cross-power spectra using Welch' method of spectral
* estimation. The exact amount of overlap in Welch' method is rounded up to a
* certain amount of samples. This class is specifically for sweeps and will ignore
* fft blocks where the auto-power of the "reference" signal is below the average
* when in averaging mode.
*/
class AvSweepPowerSpectra {
enum class Mode {
Averaging = 0, // Averaging all time date
Leaking = 1, // Exponential weighting of an "instantaneous cps"
Spectrogram = 2 // Instantenous spectrum, no averaging
};
Mode _mode;
d _alpha; // Only valid in case of 'Leaking'
us _nfft;
vd _n_averages { 1, arma::fill::zeros}; // Only valid in case of 'Averaging'
vd _A; // squared amplitude envelope
PowerSpectra _ps;
/**
* @brief Current estimate of cross-spectral density
*/
ccube _est;
/**
* @brief Buffer of storage of time data.
*/
TimeBuffer _timeBuf;
/**
* @brief The amount of samples to keep in the overlap
*/
us _overlap_keep;
public:
/**
* @brief Initalize averaged power spectra computer. If a time constant is
* given > 0, it is used in a kind of exponential weighting.
*
* @param nfft The fft length
* @param w The window type.
* @param overlap_percentage A number 0 < overlap_percentage <= 100. It
* determines the amount of overlap used in Welch' method. A typical value is
* 50 %, i.e. 50.
* @param fs_tau Value should either be < 0, indicating that the
* estimate is averages over all time data.
* For a value = 0 the instantaneous power spectrum is returned, which can be
* interpreted as the spectrogram. For a value > 0 a exponential forgetting is
* used, where the value is used as the time constant such that the decay
* follows approximately the trend exp(-n/fs_tau), where n is the
* sample number in the power spectra. To choose 'fast' time weighting, set
* time_constant to the value of fs*0.125, where fs denotes the sampling
* frequency.
**/
AvSweepPowerSpectra(const us nfft = 2048,
const Window::WindowType w = Window::WindowType::Hann,
const d overlap_percentage = 50.,
const d fs_tau = -1,
const vd *A = nullptr); // amplitude envelope
AvSweepPowerSpectra(const AvSweepPowerSpectra &) = default;
AvSweepPowerSpectra &operator=(const AvSweepPowerSpectra &) = default;
AvSweepPowerSpectra (AvSweepPowerSpectra&& ) = default;
/**
* @brief Reset to empty state. Clears the time buffer and sets estimator to
* empty.
*/
void reset();
/**
* @brief Compute an update of the power spectra based on given time data.
* Note that the number of channels is determined from the first time this
* function is called. If a later call has an incompatible number of
* channels, a runtime error is thrown.
*
* @param timedata
*
* @return a copy of the latest estimate of the power spectra. an
* update is only given if the amount of new time data is enough to compute a
* new estimate. if no new estimate is available, it returns an empty ccube.
* Note that the latest available estimate can be obtained using get_est().
* */
ccube compute(const dmat &timedata);
/**
* @brief Returns the latest estimate of cps (cross-power spectra.
*
* @return a copy of the latest estimate of the power spectra. an
* update is only given if the amount of new time data is enough to compute a
* new estimate. If no estimate is available, it returns an empty ccube.
* */
ccube get_est() const;
vd get_A() const;
/**
* @brief The overlap is rounded to a certain amount of time samples. This
* function returns that value.
*
* @return The amount of samples in overlapping.
*/
us exactOverlapSamples() const { return _ps.nfft - _overlap_keep; }
};
/** @} */

View File

@ -38,6 +38,7 @@ protected:
int _interruption_frame_count = 0;
virtual void resetImpl() = 0;
virtual void resetPos() = 0;
virtual vd genSignalUnscaled(const us nframes) = 0;
public:
@ -73,9 +74,9 @@ public:
* @brief Mute the signal. Passes through the DC offset. No lock is hold. If
* it just works one block later, than that is just the case.
*
* @param mute if tre
* @param mute if true
*/
void setMute(bool mute = true) { _muted = mute; _interruption_frame_count=0; }
void setMute(bool mute = true) { _muted = mute; _interruption_frame_count=0; resetPos();}
/**
* @brief Set the level of the signal generator

View File

@ -24,60 +24,85 @@ DEBUGTRACE_VARIABLES;
Noise::Noise(){DEBUGTRACE_ENTER}
vd Noise::genSignalUnscaled(us nframes) {
vd Noise::genSignalUnscaled(us nframes)
{
return arma::randn<vd>(nframes);
}
void Noise::resetImpl() {}
void Noise::resetPos() {}
Sine::Sine(const d freq) : omg(2 * arma::datum::pi * freq) { DEBUGTRACE_ENTER; }
vd Sine::genSignalUnscaled(const us nframes) {
vd Sine::genSignalUnscaled(const us nframes)
{
/* DEBUGTRACE_ENTER; */
slock lck(_mtx);
const d pi = arma::datum::pi;
vd phase_vec =
arma::linspace(phase, phase + omg * (nframes - 1) / _fs, nframes);
phase += omg * nframes / _fs;
while (phase > 2 * arma::datum::pi) {
while (phase > 2 * arma::datum::pi)
{
phase -= 2 * pi;
}
return arma::sin(phase_vec);
}
vd Periodic::genSignalUnscaled(const us nframes) {
vd Periodic::genSignalUnscaled(const us nframes)
{
vd res(nframes);
slock lck(_mtx);
if (_signal.size() == 0) {
if (_signal.size() == 0)
{
throw rte("No signal defined while calling");
}
for (us i = 0; i < nframes; i++) {
res(i) = _signal[_cur_pos];
if (_signal.size() != A_.size())
{
std::cout << "Seq size: " << _signal.size() << ", A size: " << A_.size() << "\n";
throw rte("Sequence and amplitude envelopes have different lengths");
}
for (us i = 0; i < nframes; i++)
{
res(i) = A_[_cur_pos] * _signal[_cur_pos];
_cur_pos++;
_cur_pos %= _signal.size();
}
return res;
}
void Periodic::setA(const vd &A)
{
A_ = A;
}
Sweep::Sweep(const d fl, const d fu, const d Ts, const d Tq, const us flags)
: fl_(fl), fu_(fu), Ts(Ts), Tq(Tq), flags(flags) {
if (fl <= 0 || fu < fl || Ts <= 0) {
: fl_(fl), fu_(fu), Ts(Ts), Tq(Tq), flags(flags)
{
if (fl <= 0 || fu < fl || Ts <= 0)
{
throw rte("Invalid sweep parameters");
}
if ((flags & ForwardSweep) && (flags & BackwardSweep)) {
if ((flags & ForwardSweep) && (flags & BackwardSweep))
{
throw rte(
"Both forward and backward sweep flag set. Please only set either one "
"or none for a continuous sweep");
}
if ((flags & LinearSweep) && (flags & LogSweep)) {
if ((flags & LinearSweep) && (flags & LogSweep))
{
throw rte(
"Both logsweep and linear sweep flag set. Please only set either one.");
}
if (!((flags & LinearSweep) || (flags & LogSweep))) {
if (!((flags & LinearSweep) || (flags & LogSweep)))
{
throw rte("Either LinearSweep or LogSweep should be given as flag");
}
resetImpl();
}
void Sweep::resetImpl() {
void Sweep::resetImpl()
{
DEBUGTRACE_ENTER;
slock lck(_mtx);
@ -94,14 +119,18 @@ void Sweep::resetImpl() {
const us N = Ns + Nq;
_signal = vd(N, arma::fill::zeros);
fn_ = vd(N, arma::fill::zeros);
index = 0;
d fl, fu;
/* Swap fl and fu for a backward sweep */
if (backward_sweep) {
if (backward_sweep)
{
fu = fl_;
fl = fu_;
} else {
}
else
{
/* Case of continuous sweep, or forward sweep */
fl = fl_;
fu = fu_;
@ -110,8 +139,10 @@ void Sweep::resetImpl() {
d phase = 0;
/* Linear sweep */
if (flags & LinearSweep) {
if (forward_sweep || backward_sweep) {
if (flags & LinearSweep)
{
if (forward_sweep || backward_sweep)
{
/* Forward or backward sweep */
/* TRACE(15, "Forward or backward sweep"); */
us K = (us)(Dt * (fl * Ns + 0.5 * (Ns - 1) * (fu - fl)));
@ -120,12 +151,16 @@ void Sweep::resetImpl() {
/* iVARTRACE(15, K); */
/* dVARTRACE(15, eps); */
for (us n = 0; n < Ns; n++) {
for (us n = 0; n < Ns; n++)
{
_signal(n) = d_sin(phase);
d fn = fl + ((d)n) / Ns * (fu + eps - fl);
fn_(n) = fn;
phase += 2 * arma::datum::pi * Dt * fn;
}
} else {
}
else
{
/* Continous sweep */
/* TRACE(15, "continuous sweep"); */
@ -150,18 +185,24 @@ void Sweep::resetImpl() {
/* dVARTRACE(15, eps); */
d phase = 0;
for (us n = 0; n <= Ns; n++) {
for (us n = 0; n < Ns; n++)
{
/* iVARTRACE(17, n); */
if (n < N) {
if (n < N)
{
_signal[n] = d_sin(phase);
}
d fn;
if (n <= Nf) {
if (n <= Nf)
{
fn = fl + ((d)n) / Nf * (fu - fl);
} else {
}
else
{
fn = fu - ((d)n - Nf) / Nb * (fu + eps - fl);
}
fn_(n) = fn;
/* dbgassert(fn >= 0, "BUG"); */
phase += 2 * number_pi * Dt * fn;
@ -169,9 +210,12 @@ void Sweep::resetImpl() {
/* This should be a very small number!! */
/* dVARTRACE(15, phase); */
}
} else if (flags & LogSweep) {
}
else if (flags & LogSweep)
{
DEBUGTRACE_PRINT("Log sweep");
if (forward_sweep || backward_sweep) {
if (forward_sweep || backward_sweep)
{
/* Forward or backward sweep */
DEBUGTRACE_PRINT("Forward or backward sweep");
d k1 = (fu / fl);
@ -180,7 +224,8 @@ void Sweep::resetImpl() {
/* Iterate k to the right solution */
d E;
for (us iter = 0; iter < 10; iter++) {
for (us iter = 0; iter < 10; iter++)
{
E = 1 + K / (Dt * fl) * (d_pow(k, 1.0 / Ns) - 1) - k;
d dEdk = K / (Dt * fl) * d_pow(k, 1.0 / Ns) / (Ns * k) - 1;
k -= E / dEdk;
@ -191,12 +236,16 @@ void Sweep::resetImpl() {
DEBUGTRACE_PRINT(k);
DEBUGTRACE_PRINT(E);
for (us n = 0; n < Ns; n++) {
for (us n = 0; n < Ns; n++)
{
_signal[n] = d_sin(phase);
d fn = fl * d_pow(k, ((d)n) / Ns);
fn_(n) = fn;
phase += 2 * number_pi * Dt * fn;
}
} else {
}
else
{
DEBUGTRACE_PRINT("Continuous sweep");
const us Nf = Ns / 2;
@ -212,7 +261,8 @@ void Sweep::resetImpl() {
/* Newton iterations to converge k to the value such that the sweep is
* continuous */
for (us iter = 0; iter < NITER_NEWTON; iter++) {
for (us iter = 0; iter < NITER_NEWTON; iter++)
{
E = (k - 1) / (d_pow(k, 1.0 / Nf) - 1) +
(k - 1) / (1 - d_pow(k, -1.0 / Nb)) - K / Dt / fl;
DEBUGTRACE_PRINT(E);
@ -236,29 +286,37 @@ void Sweep::resetImpl() {
DEBUGTRACE_PRINT(k);
DEBUGTRACE_PRINT(E);
for (us n = 0; n <= Ns; n++) {
for (us n = 0; n < Ns; n++)
{
/* iVARTRACE(17, n); */
if (n < Ns) {
if (n < Ns)
{
_signal[n] = d_sin(phase);
}
d fn;
if (n <= Nf) {
if (n <= Nf)
{
fn = fl * d_pow(k, ((d)n) / Nf);
} else {
}
else
{
fn = fl * k * d_pow(1 / k, ((d)n - Nf) / Nb);
}
fn_(n) = fn;
/* dbgassert(fn >= 0, "BUG"); */
phase += 2 * number_pi * Dt * fn;
while (phase > 2 * number_pi) phase -= 2 * number_pi;
while (phase > 2 * number_pi)
phase -= 2 * number_pi;
/* dVARTRACE(17, phase); */
}
/* This should be a very small number!! */
DEBUGTRACE_PRINT(phase);
}
} // End of log sweep
else {
else
{
// Either log or linear sweep had to be given as flags.
assert(false);
}

View File

@ -18,6 +18,7 @@ class Noise : public Siggen {
d level_linear;
virtual vd genSignalUnscaled(const us nframes) override;
void resetImpl() override;
void resetPos() override;
public:
/**
@ -39,6 +40,7 @@ class Sine : public Siggen {
protected:
void resetImpl() override final { phase = 0; }
void resetPos() override final { phase = 0; }
virtual vd genSignalUnscaled(const us nframes) override final;
public:
@ -58,6 +60,7 @@ class Sine : public Siggen {
* periodic as the frequency can be any floating point value.
*/
class Periodic: public Siggen {
protected:
vd _signal { 1, arma::fill::zeros};
us _cur_pos = 0;
@ -68,8 +71,16 @@ class Periodic: public Siggen {
* @return As stated above
*/
vd getSequence() const { return _signal; }
vd A_ { 1, arma::fill::ones};
void setA(const vd& A);
void resetPos() override final { _cur_pos = 0; }
virtual vd genSignalUnscaled(const us nframes) override final;
vd getA() const { return A_; }
~Periodic() = default;
};
@ -81,6 +92,7 @@ class Sweep : public Periodic {
d fl_, fu_, Ts, Tq;
us index;
us flags;
vd fn_ { 1, arma::fill::zeros};
void resetImpl() override;
@ -90,6 +102,8 @@ class Sweep : public Periodic {
static constexpr int LinearSweep = 1 << 2;
static constexpr int LogSweep = 1 << 3;
vd getfn() const { return fn_; }
/**
* Create a sweep signal
*

View File

@ -29,27 +29,28 @@ using rte = std::runtime_error;
* @param m The Python module to add classes and methods to
*/
void init_dsp(py::module &m) {
void init_dsp(py::module &m)
{
py::class_<Fft> fft(m, "Fft");
fft.def(py::init<us>());
fft.def("fft", [](Fft &f, dpyarray dat) {
fft.def("fft", [](Fft &f, dpyarray dat)
{
if (dat.ndim() == 1) {
return ColToNpy<c>(f.fft(NpyToCol<d, false>(dat)));
} else if (dat.ndim() == 2) {
return MatToNpy<c>(f.fft(NpyToMat<d, false>(dat)));
} else {
throw rte("Invalid dimensions of array");
}
});
fft.def("ifft", [](Fft &f, cpyarray dat) {
} });
fft.def("ifft", [](Fft &f, cpyarray dat)
{
if (dat.ndim() == 1) {
return ColToNpy<d>(f.ifft(NpyToCol<c, false>(dat)));
} else if (dat.ndim() == 2) {
return MatToNpy<d>(f.ifft(NpyToMat<c, false>(dat)));
} else {
throw rte("Invalid dimensions of array");
}
});
} });
fft.def_static("load_fft_wisdom", &Fft::load_fft_wisdom);
fft.def_static("store_fft_wisdom", &Fft::store_fft_wisdom);
@ -71,41 +72,39 @@ void init_dsp(py::module &m) {
/// SeriesBiquad
py::class_<SeriesBiquad, std::shared_ptr<SeriesBiquad>> sbq(m, "SeriesBiquad",
filter);
sbq.def(py::init([](dpyarray filter) {
return std::make_shared<SeriesBiquad>(NpyToCol<d, false>(filter));
}));
sbq.def("filter", [](SeriesBiquad &s, dpyarray input) {
sbq.def(py::init([](dpyarray filter)
{ return std::make_shared<SeriesBiquad>(NpyToCol<d, false>(filter)); }));
sbq.def("filter", [](SeriesBiquad &s, dpyarray input)
{
vd res = NpyToCol<d, true>(input);
s.filter(res);
return ColToNpy<d>(res);
});
return ColToNpy<d>(res); });
/// BiquadBank
py::class_<BiquadBank, Filter, std::shared_ptr<BiquadBank>> bqb(m,
"BiquadBank");
bqb.def(py::init([](dpyarray coefs) {
return std::make_shared<BiquadBank>(NpyToMat<d, false>(coefs));
}));
bqb.def(py::init([](dpyarray coefs, dpyarray gains) {
bqb.def(py::init([](dpyarray coefs)
{ return std::make_shared<BiquadBank>(NpyToMat<d, false>(coefs)); }));
bqb.def(py::init([](dpyarray coefs, dpyarray gains)
{
vd gains_arma = NpyToMat<d, false>(gains);
return std::make_shared<BiquadBank>(NpyToMat<d, false>(coefs), &gains_arma);
}));
return std::make_shared<BiquadBank>(NpyToMat<d, false>(coefs), &gains_arma); }));
bqb.def("setGains",
[](BiquadBank &b, dpyarray gains) { b.setGains(NpyToCol(gains)); });
bqb.def("filter", [](BiquadBank &b, dpyarray input) {
[](BiquadBank &b, dpyarray gains)
{ b.setGains(NpyToCol(gains)); });
bqb.def("filter", [](BiquadBank &b, dpyarray input)
{
// Yes, a copy here
vd inout = NpyToCol<d, true>(input);
b.filter(inout);
return ColToNpy(inout);
});
return ColToNpy(inout); });
/// PowerSpectra
py::class_<PowerSpectra> ps(m, "PowerSpectra");
ps.def(py::init<const us, const Window::WindowType>());
ps.def("compute", [](PowerSpectra &ps, dpyarray input) {
return CubeToNpy<c>(ps.compute(NpyToMat<d, false>(input)));
});
ps.def("compute", [](PowerSpectra &ps, dpyarray input)
{ return CubeToNpy<c>(ps.compute(NpyToMat<d, false>(input))); });
/// AvPowerSpectra
py::class_<AvPowerSpectra> aps(m, "AvPowerSpectra");
@ -114,7 +113,8 @@ void init_dsp(py::module &m) {
py::arg("windowType") = Window::WindowType::Hann,
py::arg("overlap_percentage") = 50.0, py::arg("time_constant") = -1);
aps.def("compute", [](AvPowerSpectra &aps, dpyarray timedata) {
aps.def("compute", [](AvPowerSpectra &aps, dpyarray timedata)
{
std::optional<ccube> res;
dmat timedata_mat = NpyToMat<d, false>(timedata);
{
@ -122,44 +122,97 @@ void init_dsp(py::module &m) {
res = aps.compute(timedata_mat);
}
return CubeToNpy<c>(res.value_or(ccube(0, 0, 0)));
});
aps.def("get_est", [](const AvPowerSpectra &ps) {
return CubeToNpy<c>(res.value_or(ccube(0, 0, 0))); });
aps.def("get_est", [](const AvPowerSpectra &ps)
{
ccube est = ps.get_est();
return CubeToNpy<c>(est);
});
return CubeToNpy<c>(est); });
/// AvSweepPowerSpectra
py::class_<AvSweepPowerSpectra, std::unique_ptr<AvSweepPowerSpectra>> asps(m, "AvSweepPowerSpectra");
asps.def(py::init([](const us nfft,
const Window::WindowType windowtype,
const d overlap_percentage,
const d time_constant,
const dpyarray A)
{
std::unique_ptr<vd> theA = std::make_unique<vd>(NpyToCol<d, false>(A));
return std::make_unique<AvSweepPowerSpectra>(
nfft,
windowtype,
overlap_percentage,
time_constant,
theA.get());
}
));
asps.def("compute", [](AvSweepPowerSpectra &asps, dpyarray timedata)
{
std::optional<ccube> res;
dmat timedata_mat = NpyToMat<d, false>(timedata);
{
py::gil_scoped_release release;
res = asps.compute(timedata_mat);
}
return CubeToNpy<c>(res.value_or(ccube(0, 0, 0))); });
asps.def("get_est", [](const AvSweepPowerSpectra &sps)
{
ccube est = sps.get_est();
return CubeToNpy<c>(est); });
asps.def("get_A", [](const AvSweepPowerSpectra &sps)
{
vd A = sps.get_A();
return ColToNpy<d>(A); });
py::class_<SLM> slm(m, "cppSLM");
slm.def_static("fromBiquads", [](const d fs, const d Lref, const us ds,
const d tau, dpyarray bandpass) {
return SLM::fromBiquads(fs, Lref, ds, tau, NpyToMat<d, false>(bandpass));
});
const d tau, dpyarray bandpass)
{
return SLM::fromBiquads(fs, Lref, ds, tau, NpyToMat<d, false>(bandpass)); });
slm.def_static("fromBiquads", [](const d fs, const d Lref, const us ds,
const d tau, dpyarray prefilter,
py::array_t<d> bandpass) {
py::array_t<d> bandpass)
{
return SLM::fromBiquads(fs, Lref, ds, tau, NpyToCol<d, false>(prefilter),
NpyToMat<d, false>(bandpass));
});
NpyToMat<d, false>(bandpass)); });
slm.def("run", [](SLM &slm, dpyarray in) {
return MatToNpy<d>(slm.run(NpyToCol<d, false>(in)));
});
slm.def("Pm", [](const SLM &slm) { return ColToNpy<d>(slm.Pm); });
slm.def("Pmax", [](const SLM &slm) { return ColToNpy<d>(slm.Pmax); });
slm.def("Ppeak", [](const SLM &slm) { return ColToNpy<d>(slm.Ppeak); });
slm.def("run", [](SLM &slm, dpyarray in)
{
return MatToNpy<d>(slm.run(NpyToCol<d, false>(in))); });
slm.def("Pm", [](const SLM &slm)
{
return ColToNpy<d>(slm.Pm); });
slm.def("Pmax", [](const SLM &slm)
{
return ColToNpy<d>(slm.Pmax); });
slm.def("Ppeak", [](const SLM &slm)
{
return ColToNpy<d>(slm.Ppeak); });
slm.def("Leq", [](const SLM &slm) { return ColToNpy<d>(slm.Leq()); });
slm.def("Lmax", [](const SLM &slm) { return ColToNpy<d>(slm.Lmax()); });
slm.def("Lpeak", [](const SLM &slm) { return ColToNpy<d>(slm.Lpeak()); });
slm.def("Leq", [](const SLM &slm)
{
return ColToNpy<d>(slm.Leq()); });
slm.def("Lmax", [](const SLM &slm)
{
return ColToNpy<d>(slm.Lmax()); });
slm.def("Lpeak", [](const SLM &slm)
{
return ColToNpy<d>(slm.Lpeak()); });
slm.def_static("suggestedDownSamplingFac", &SLM::suggestedDownSamplingFac);
// Frequency smoother
m.def("freqSmooth", [](dpyarray freq, dpyarray X, unsigned w) {
m.def("freqSmooth", [](dpyarray freq, dpyarray X, unsigned w)
{
vd freqa = NpyToCol<d, false>(freq);
vd Xa = NpyToCol<d, false>(X);
return ColToNpy(freqSmooth(freqa, Xa, w));
});
return ColToNpy(freqSmooth(freqa, Xa, w)); });
}
/** @} */

View File

@ -43,11 +43,22 @@ void init_siggen(py::module &m) {
py::class_<Periodic, std::shared_ptr<Periodic>> periodic(m, "Periodic",
siggen);
periodic.def("setA",
[](Periodic &p, const dpyarray A) {
p.setA(NpyToCol<d, false>(A));
});
periodic.def("getSequence",
[](const Sweep &s) { return ColToNpy<d>(s.getSequence()); });
periodic.def("getA",
[](const Sweep &s) { return ColToNpy<d>(s.getA()); });
py::class_<Sweep, std::shared_ptr<Sweep>> sweep(m, "Sweep", periodic);
sweep.def(py::init<const d, const d, const d, const d, const us>());
sweep.def("getfn",
[](const Sweep &s) { return ColToNpy<d>(s.getfn()); });
sweep.def_readonly_static("ForwardSweep", &Sweep::ForwardSweep);
sweep.def_readonly_static("BackwardSweep", &Sweep::BackwardSweep);
sweep.def_readonly_static("LinearSweep", &Sweep::LinearSweep);

View File

@ -10,11 +10,11 @@ from .lasp_cpp import *
# from .lasp_imptube import * # TwoMicImpedanceTube
from .lasp_measurement import * # Measurement, scaleBlockSens
from .lasp_octavefilter import * # OverallFilterBank, SosOctaveFilterBank, SosThirdOctaveFilterBank
from .lasp_octavefilter import *
from .lasp_slm import * # SLM, Dummy
from .lasp_record import * # RecordStatus, Recording
from .lasp_daqconfigs import * # DaqConfigurations
from .lasp_measurementset import * # MeasurementSet
from .lasp_daqconfigs import *
from .lasp_measurementset import *
# from .lasp_siggen import * # SignalType, NoiseType, SiggenMessage, SiggenData, Siggen
# from .lasp_weighcal import * # WeighCal

View File

@ -15,10 +15,13 @@ from scipy.io import wavfile
import os, time, wave, logging
from .lasp_common import SIQtys, Qty, getFreq
from .lasp_version import LASP_VERSION_MAJOR, LASP_VERSION_MINOR
from .lasp_cpp import Window, DaqChannel, AvPowerSpectra
from .lasp_cpp import Window, DaqChannel, AvPowerSpectra, AvSweepPowerSpectra
from typing import List
from functools import lru_cache
from .lasp_config import ones
from scipy.interpolate import CubicSpline
"""!
Author: J.A. de Jong - ASCEE
@ -306,6 +309,16 @@ class Measurement:
except KeyError:
self._type_int = 0
try:
dat = {}
for key in f['siggen']['Data'].attrs:
dat[key] = f['siggen']['Data'].attrs[key]
self._siggen = {'Type': f['siggen'].attrs["Type"], 'Data': dat}
except KeyError:
self._siggen = {'Type': 'Muted', 'Data': {}}
# Due to a previous bug, the channel names were not stored
# consistently, i.e. as 'channel_names' and later camelcase.
try:
@ -571,12 +584,29 @@ class Measurement:
"""
self.setAttribute("type_int", type_.value)
def setSiggen(self, siggen_: dict):
"""
Set the signal generator data
"""
with self.file("r+") as f:
siggen_group = f.create_group("siggen", track_order=True)
siggen_group.attrs['Type'] = siggen_['Type']
data_group = siggen_group.create_group("Data", track_order=True)
for key in siggen_['Data'].keys():
data_group.attrs[key] = siggen_['Data'][key]
def measurementType(self):
"""
Returns type of measurement
"""
return MeasurementType(self._type_int)
def signalGenerator(self):
"""
Returns signal generator data
"""
return self._siggen
@property
def name(self):
"""Returns filename base without extension."""
@ -814,9 +844,16 @@ class Measurement:
channels = list(range(self.nchannels))
nchannels = len(channels)
aps = AvPowerSpectra(nfft, window, overlap)
freq = getFreq(self.samplerate, nfft)
if self._siggen['Type'] == 'Sweep':
iA = CubicSpline(self._siggen['Data']['fA'], self._siggen['Data']['A'])
aps = AvSweepPowerSpectra(nfft, window, overlap, -1, iA(freq))
else:
aps = AvPowerSpectra(nfft, window, overlap)
for data in self.iterData(channels, **kwargs):
CS = aps.compute(data)
@ -1005,7 +1042,8 @@ class Measurement:
# Convert range to [-1, 1]
# TODO: this is wrong for float data where full scale > 1
sensone = np.ones_like(self.sensitivity)
sensone = np.ones(data.shape[1])
data = scaleBlockSens(data, sensone)
if dtype == "int16" or dtype == "int32":
@ -1156,8 +1194,11 @@ class Measurement:
if data.ndim != 2:
data = data[:, np.newaxis]
if not (isinstance(sensitivity, np.ndarray) and sensitivity.ndim >= 1):
sensitivity = np.asarray(sensitivity)[np.newaxis]
try:
len(sensitivity)
except:
raise ValueError("Sensitivity should be given as array-like data type")
sensitivity = np.asarray(sensitivity)
nchannels = data.shape[1]
if nchannels != sensitivity.shape[0]: