Merge remote-tracking branch 'refs/remotes/origin/develop' into develop
All checks were successful
Building, testing and releasing LASP if it has a tag / Build-Test-Ubuntu (push) Successful in -59s
Building, testing and releasing LASP if it has a tag / Release-Ubuntu (push) Has been skipped

This commit is contained in:
Thijs Hekman 2024-06-26 15:06:09 +02:00
commit 7cd3dcffa8
20 changed files with 280 additions and 321 deletions

View File

@ -12,7 +12,7 @@ jobs:
- lasp_dist:/dist - lasp_dist:/dist
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
submodules: true submodules: true

View File

@ -1,17 +1,27 @@
cmake_minimum_required (VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(LASP LANGUAGES C CXX VERSION 1.1) project(LASP LANGUAGES C CXX VERSION 1.6.3)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED) set(CMAKE_CXX_STANDARD_REQUIRED)
option(LASP_DOUBLE_PRECISION "Compile as double precision floating point" ON) if(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "aarch64")
set(RPI TRUE)
else()
set(RPI FALSE)
endif()
# Setting defaults for PortAudio and RtAudio backend, depending on Linux / # Setting defaults for PortAudio and RtAudio backend, depending on Linux /
# Windows. # Windows.
set(DEFAULT_DOUBLE_PRECISION ON)
if(WIN32) if(WIN32)
set(DEFAULT_RTAUDIO OFF) set(DEFAULT_RTAUDIO OFF)
set(DEFAULT_PORTAUDIO ON) set(DEFAULT_PORTAUDIO ON)
set(DEFAULT_ULDAQ OFF) set(DEFAULT_ULDAQ OFF)
elseif(${RPI})
set(DEFAULT_RTAUDIO OFF)
set(DEFAULT_PORTAUDIO ON)
set(DEFAULT_ULDAQ OFF)
set(DEFAULT_DOUBLE_PRECISION OFF)
else() else()
set(DEFAULT_RTAUDIO OFF) set(DEFAULT_RTAUDIO OFF)
set(DEFAULT_PORTAUDIO ON) set(DEFAULT_PORTAUDIO ON)
@ -19,6 +29,7 @@ else()
endif() endif()
option(LASP_DOUBLE_PRECISION "Compile as double precision floating point" ${DEFAULT_DOUBLE_PRECISION})
option(LASP_HAS_RTAUDIO "Compile with RtAudio Daq backend" ${DEFAULT_RTAUDIO}) option(LASP_HAS_RTAUDIO "Compile with RtAudio Daq backend" ${DEFAULT_RTAUDIO})
option(LASP_HAS_PORTAUDIO "Compile with PortAudio Daq backend" ${DEFAULT_PORTAUDIO}) option(LASP_HAS_PORTAUDIO "Compile with PortAudio Daq backend" ${DEFAULT_PORTAUDIO})
if(LASP_HAS_PORTAUDIO AND LASP_HAS_RTAUDIO) if(LASP_HAS_PORTAUDIO AND LASP_HAS_RTAUDIO)
@ -105,13 +116,17 @@ else()
endif() endif()
# ###################################### Compilation flags # ###################################### Compilation flags
set(CMAKE_C_FLAGS_RELEASE "-O3 -flto -mfpmath=sse -march=x86-64 -mtune=native \ set(CMAKE_C_FLAGS_RELEASE "-O3 -flto -mtune=native \
-fdata-sections -ffunction-sections -fomit-frame-pointer -finline-functions") -fdata-sections -ffunction-sections -fomit-frame-pointer -finline-functions")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-type-limits -Werror=return-type") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-type-limits -Werror=return-type")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -flto -mfpmath=sse -march=x86-64 -mtune=native \ set(CMAKE_CXX_FLAGS_RELEASE "-O3 -flto -mtune=native \
-fdata-sections -ffunction-sections -fomit-frame-pointer -finline-functions") -fdata-sections -ffunction-sections -fomit-frame-pointer -finline-functions")
if(NOT ${RPI})
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -mfpmath=sse -march=x86-64")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -mfpmath=sse -march=x86-64")
endif()
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g -Wall ") set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g -Wall ")
# ############################# End compilation flags # ############################# End compilation flags

View File

@ -44,15 +44,17 @@ in a sister repository [lasp-doc](https://code.ascee.nl/ascee/lasp-doc).
If you have any question(s), please feel free to contact us: [email](info@ascee.nl). If you have any question(s), please feel free to contact us: [email](info@ascee.nl).
# Installation - Linux (Ubuntu-based) # Installation - Linux (Ubuntu-based)
## Prerequisites ## Prerequisites
Run the following on the command line to install all prerequisites on Run the following on the command line to install all prerequisites on
Debian-based Linux: Debian-based Linux, x86-64:
- `sudo apt install python3-pip libfftw3-3 libopenblas-base libusb-1.0-0 libpulse0` - `sudo apt install python3-pip libfftw3-3 libopenblas-base libusb-1.0-0 libpulse0`
## Installation from wheel (recommended for non-developers) ## Installation from wheel (recommended for non-developers)
Go to: [LASP releases](https://code.ascee.nl/ASCEE/lasp/releases/latest/) and Go to: [LASP releases](https://code.ascee.nl/ASCEE/lasp/releases/latest/) and
@ -82,6 +84,28 @@ If building RtAudio with the Jack Audio Connection Kit (JACK) backend, you will
- `$ cd lasp` - `$ cd lasp`
- `pip install -e .` - `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 # Installation - (x86_64) Windows (with WinPython), build with MSYS2
## Prerequisites ## Prerequisites
@ -144,3 +168,25 @@ This will build the documentation. It can be read by:
- See examples directories for IPython notebooks. - See examples directories for IPython notebooks.
- Please refer to the [documentation](https://lasp.ascee.nl/) for features. - 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

@ -107,37 +107,12 @@ void StreamMgr::rescanDAQDevices(bool background,
_pool.push_task(&StreamMgr::rescanDAQDevices_impl, this, callback); _pool.push_task(&StreamMgr::rescanDAQDevices_impl, this, callback);
} }
} }
#if LASP_HAS_PORTAUDIO && LASP_HAS_PA_ALSA
#include <alsa/asoundlib.h>
void empty_handler(const char *file, int line, const char *function, int err,
const char *fmt, ...) {}
// Temporarily set the ALSA eror handler to something that does nothing, to
// prevent ALSA from spitting out all kinds of misconfiguration errors.
class MuteErrHandler {
private:
snd_lib_error_handler_t _default_handler;
public:
explicit MuteErrHandler() {
_default_handler = snd_lib_error;
snd_lib_error_set_handler(empty_handler);
}
~MuteErrHandler() { snd_lib_error_set_handler(_default_handler); }
};
#else
// Does nothin in case of no ALSA
class MuteErrHandler {};
#endif
void StreamMgr::rescanDAQDevices_impl(std::function<void()> callback) { void StreamMgr::rescanDAQDevices_impl(std::function<void()> callback) {
DEBUGTRACE_ENTER; DEBUGTRACE_ENTER;
assert(!_inputStream && !_outputStream); assert(!_inputStream && !_outputStream);
Lck lck(_mtx); Lck lck(_mtx);
// Alsa spits out annoying messages that are not useful // Alsa spits out annoying messages that are not useful
{ {
MuteErrHandler guard;
_devices = DeviceInfo::getDeviceInfo(); _devices = DeviceInfo::getDeviceInfo();
} }

View File

@ -16,6 +16,33 @@ using std::endl;
using std::string; using std::string;
using std::to_string; using std::to_string;
#if LASP_HAS_PA_ALSA
#include <alsa/asoundlib.h>
void empty_handler(const char *file, int line, const char *function, int err,
const char *fmt, ...) {
// cerr << "Test empty error handler...\n";
}
// Temporarily set the ALSA eror handler to something that does nothing, to
// prevent ALSA from spitting out all kinds of misconfiguration errors.
class MuteErrHandler {
private:
snd_lib_error_handler_t _default_handler;
public:
explicit MuteErrHandler() {
_default_handler = snd_lib_error;
snd_lib_error_set_handler(empty_handler);
}
~MuteErrHandler() { snd_lib_error_set_handler(_default_handler); }
};
#else
// Does nothin in case of no ALSA
class MuteErrHandler {};
#endif
inline void throwIfError(PaError e) { inline void throwIfError(PaError e) {
DEBUGTRACE_ENTER; DEBUGTRACE_ENTER;
if (e != paNoError) { if (e != paNoError) {
@ -44,6 +71,7 @@ class OurPaDeviceInfo : public DeviceInfo {
void fillPortAudioDeviceInfo(DeviceInfoList &devinfolist) { void fillPortAudioDeviceInfo(DeviceInfoList &devinfolist) {
DEBUGTRACE_ENTER; DEBUGTRACE_ENTER;
bool shouldPaTerminate = false; bool shouldPaTerminate = false;
MuteErrHandler guard;
try { try {
PaError err = Pa_Initialize(); PaError err = Pa_Initialize();
/// PortAudio says that Pa_Terminate() should not be called whenever there /// PortAudio says that Pa_Terminate() should not be called whenever there
@ -348,6 +376,7 @@ PortAudioDaq::PortAudioDaq(const OurPaDeviceInfo &devinfo_gen,
void PortAudioDaq::start(InDaqCallback inCallback, OutDaqCallback outCallback) { void PortAudioDaq::start(InDaqCallback inCallback, OutDaqCallback outCallback) {
DEBUGTRACE_ENTER; DEBUGTRACE_ENTER;
assert(_stream); assert(_stream);
MuteErrHandler guard;
if (Pa_IsStreamActive(_stream)) { if (Pa_IsStreamActive(_stream)) {
throw rte("Stream is already running"); throw rte("Stream is already running");

View File

@ -11,7 +11,7 @@ using std::cerr;
using std::endl; using std::endl;
// Safe some typing. Linspace form 0 up to (and NOT including N). // Safe some typing. Linspace form 0 up to (and NOT including N).
#define lin0N arma::linspace(0, N - 1, N) #define lin0N arma::linspace<vd>(0, N - 1, N)
vd Window::hann(const us N) { vd Window::hann(const us N) {
return arma::pow(arma::sin((arma::datum::pi/N) * lin0N), 2); return arma::pow(arma::sin((arma::datum::pi/N) * lin0N), 2);
@ -24,8 +24,8 @@ vd Window::blackman(const us N) {
d a0 = 7938. / 18608.; d a0 = 7938. / 18608.;
d a1 = 9240. / 18608.; d a1 = 9240. / 18608.;
d a2 = 1430. / 18608.; d a2 = 1430. / 18608.;
return a0 - a1 * d_cos((2 * number_pi/N) * lin0N) + return a0 - a1 * arma::cos((2 * number_pi/N) * lin0N) +
a2 * d_cos((4 * number_pi / N)* lin0N ); a2 * arma::cos((4 * number_pi / N)* lin0N );
} }
vd Window::rectangular(const us N) { return arma::ones(N); } vd Window::rectangular(const us N) { return arma::ones(N); }

View File

@ -44,6 +44,12 @@ void init_siggen(py::module &m);
PYBIND11_MODULE(lasp_cpp, m) { PYBIND11_MODULE(lasp_cpp, m) {
#if LASP_DOUBLE_PRECISION == 1
m.attr("LASP_DOUBLE_PRECISION") = true;
#else
m.attr("LASP_DOUBLE_PRECISION") = false;
#endif
init_dsp(m); init_dsp(m);
init_deviceinfo(m); init_deviceinfo(m);
init_daqconfiguration(m); init_daqconfiguration(m);
@ -51,6 +57,5 @@ PYBIND11_MODULE(lasp_cpp, m) {
init_streammgr(m); init_streammgr(m);
init_datahandler(m); init_datahandler(m);
init_siggen(m); init_siggen(m);
} }
/** @} */ /** @} */

View File

@ -5,7 +5,7 @@ requires-python = ">=3.10"
description = "Library for Acoustic Signal Processing" description = "Library for Acoustic Signal Processing"
license = { "file" = "LICENSE" } license = { "file" = "LICENSE" }
authors = [{ "name" = "J.A. de Jong", "email" = "j.a.dejong@ascee.nl" }] authors = [{ "name" = "J.A. de Jong", "email" = "j.a.dejong@ascee.nl" }]
version = "1.6.1" version = "1.6.6"
keywords = ["DSP", "DAQ", "Signal processing"] keywords = ["DSP", "DAQ", "Signal processing"]
@ -23,7 +23,7 @@ classifiers = [
urls = { "Documentation" = "https://lasp.ascee.nl" } urls = { "Documentation" = "https://lasp.ascee.nl" }
dependencies = [ dependencies = [
"scipy==1.12", "scipy>=1.13.1",
"matplotlib>=3.7.2", "matplotlib>=3.7.2",
"appdirs", "appdirs",
"dataclasses_json", "dataclasses_json",

View File

@ -11,7 +11,9 @@ __all__ = ['freqResponse', 'bandpass_fir_design', 'lowpass_fir_design',
'arbitrary_fir_design'] 'arbitrary_fir_design']
import numpy as np import numpy as np
from scipy.signal import freqz, hann, firwin2 from scipy.signal import freqz, firwin2
from scipy.signal.windows import hann
from ..lasp_config import empty
def freqResponse(fs, freq, coefs_b, coefs_a=1.): def freqResponse(fs, freq, coefs_b, coefs_a=1.):
@ -44,7 +46,7 @@ def bandpass_fir_design(L, fs, fl, fu, window=hann):
Omg2 = 2*np.pi*fu/fs Omg2 = 2*np.pi*fu/fs
Omg1 = 2*np.pi*fl/fs Omg1 = 2*np.pi*fl/fs
fir = np.empty(L, dtype=float) fir = empty(L, dtype=float)
# First Create ideal band-pass filter # First Create ideal band-pass filter
fir[L//2] = (Omg2-Omg1)/np.pi fir[L//2] = (Omg2-Omg1)/np.pi
@ -64,7 +66,7 @@ def lowpass_fir_design(L, fs, fc, window=hann):
" than upper cut-off" " than upper cut-off"
Omgc = 2*np.pi*fc/fs Omgc = 2*np.pi*fc/fs
fir = np.empty(L, dtype=float) fir = empty(L, dtype=float)
# First Create ideal band-pass filter # First Create ideal band-pass filter
fir[L//2] = Omgc/np.pi fir[L//2] = Omgc/np.pi

View File

@ -6,8 +6,14 @@ Author: J.A. de Jong - ASCEE
Description: LASP configuration Description: LASP configuration
""" """
import numpy as np import numpy as np
from .lasp_cpp import LASP_DOUBLE_PRECISION
LASP_NUMPY_FLOAT_TYPE = np.float64 if LASP_DOUBLE_PRECISION:
LASP_NUMPY_FLOAT_TYPE = np.float64
LASP_NUMPY_COMPLEX_TYPE = np.complex128
else:
LASP_NUMPY_FLOAT_TYPE = np.float32
LASP_NUMPY_COMPLEX_TYPE = np.float64
def zeros(shape): def zeros(shape):

View File

@ -1,173 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""!
Author: J.A. de Jong - ASCEE
Description: Two-microphone impedance tube methods
"""
__all__ = ['TwoMicImpedanceTube']
# from lrftubes import Air
from .lasp_measurement import Measurement
from numpy import pi, sqrt, exp
import numpy as np
from scipy.interpolate import UnivariateSpline
# from lrftubes import PrsDuct
from functools import lru_cache
class TwoMicImpedanceTube:
def __init__(self, mnormal: Measurement,
mswitched: Measurement,
s: float,
d1: float,
d2: float,
fl: float = None,
fu: float = None,
periodic_method=False,
# mat= Air(),
D_imptube = 50e-3,
thermoviscous = True,
**kwargs):
"""
Initialize two-microphone impedance tube methods
Args:
mnormal: Measurement in normal configuration
mswitched: Measurement in normal configuration
s: Microphone distance
fl: Lower evaluation frequency
fu: Lower evaluation frequency
kwargs: tuple with extra arguments, of which:
N: Period length of periodic excitation *obligatory*
chan0: Measurement channel index of mic 0
chan1: Measurement channel index of mic 1
"""
self.mnormal = mnormal
self.mswitched = mswitched
self.mat = mat
self.s = s
self.d1 = d1
self.d2 = d2
self.fl = fl
if fl is None:
ksmin = 0.1*pi
kmin = ksmin/s
self.fl = kmin*mat.c0/2/pi
self.fu = fu
if fu is None:
ksmax = 0.8*pi
kmax = ksmax/s
self.fu = kmax*mat.c0/2/pi
self.thermoviscous = thermoviscous
self.D_imptube = D_imptube
self.periodic_method = periodic_method
self.channels = [kwargs.pop('chan0', 0), kwargs.pop('chan1', 1)]
# Compute calibration correction
if periodic_method:
self.N = kwargs.pop('N')
freq, C1 = mnormal.periodicCPS(self.N, channels=self.channels)
freq, C2 = mswitched.periodicCPS(self.N, channels=self.channels)
else:
self.nfft = kwargs.pop('nfft', 16000)
freq, C1 = mnormal.CPS(nfft=self.nfft, channels=self.channels)
freq, C2 = mswitched.CPS(nfft=self.nfft, channels=self.channels)
# Store this, as it is often used for just a single sample.
self.C1 = C1
self.freq = freq
self.il = np.where(self.freq<= self.fl)[0][-1]
self.ul = np.where(self.freq > self.fu)[0][0]
# Calibration correction factor
# self.K = 0*self.freq + 1.0
K = sqrt(C2[:,0,1]*C1[:,0,0]/(C2[:,1,1]*C1[:,1,0]))
# self.K = UnivariateSpline(self.freq, K.real)(self.freq) +\
# 1j*UnivariateSpline(self.freq, K.imag)(self.freq)
self.K = K
def cut_to_limits(self, ar):
return ar[self.il:self.ul]
def getFreq(self):
"""
Array of frequencies, cut to limits of validity
"""
return self.cut_to_limits(self.freq)
@lru_cache
def G_AB(self, meas):
if meas is self.mnormal:
C = self.C1
freq = self.freq
else:
if self.periodic_method:
freq, C = meas.periodicCPS(self.N, self.channels)
else:
freq, C = meas.CPS(nfft=self.nfft, channels=self.channels)
# Microphone transfer function
G_AB = self.K*C[:,1,0]/C[:,0,0]
return self.getFreq(), self.cut_to_limits(G_AB)
def k(self, freq):
"""
Wave number, or thermoviscous wave number
"""
if self.thermoviscous:
D = self.D_imptube
S = pi/4*D**2
d = PrsDuct(0, S=S, rh=D/4, cs='circ')
d.mat = self.mat
omg = 2*pi*freq
G, Z = d.GammaZc(omg)
return G
else:
return 2*pi*freq/self.mat.c0
def R(self, meas):
freq, G_AB = self.G_AB(meas)
s = self.s
k = self.k(freq)
d1 = self.d1
RpA = (G_AB - exp(-1j*k*s))/(exp(1j*k*s)-G_AB)
R = RpA*exp(2*1j*k*(s+d1))
return freq, R
def alpha(self, meas):
"""
Acoustic absorption coefficient
"""
freq, R = self.R(meas)
return freq, 1 - np.abs(R)**2
def z(self, meas):
"""
Acoustic impedance at the position of the sample, in front of the sample
"""
freq, R = self.R(meas)
return freq, self.mat.z0*(1+R)/(1-R)
def zs(self, meas):
"""
Sample impedance jump, assuming a cavity behind the sample with
thickness d2
"""
freq, R = self.R(meas)
z0 = self.mat.z0
k = 2*pi*freq/self.mat.c0
d2 = self.d2
zs = 2*z0*(1-R*exp(2*1j*k*d2))/((R-1)*(exp(2*d2*k*1j)-1))
return freq, zs

View File

@ -1,6 +1,24 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
from contextlib import contextmanager
from weakref import WeakValueDictionary
import h5py as h5
import uuid
import pathlib
import glob
import itertools
import numpy as np
from enum import Enum, unique
from .lasp_config import LASP_NUMPY_FLOAT_TYPE
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 typing import List
from functools import lru_cache
from .lasp_config import ones
"""! """!
Author: J.A. de Jong - ASCEE Author: J.A. de Jong - ASCEE
@ -46,28 +64,11 @@ The video dataset can possibly be not present in the data.
""" """
__all__ = ["Measurement", "scaleBlockSens", "MeasurementType"]
from contextlib import contextmanager
from weakref import WeakValueDictionary
import h5py as h5
import uuid
import pathlib
import glob
import itertools
import numpy as np
from enum import Enum, unique
from .lasp_config import LASP_NUMPY_FLOAT_TYPE
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 typing import List
from functools import lru_cache
__all__ = ["Measurement", "scaleBlockSens", "MeasurementType"]
# Measurement file extension # Measurement file extension
MEXT = 'h5' MEXT = "h5"
DOTMEXT = f'.{MEXT}' DOTMEXT = f".{MEXT}"
@unique @unique
@ -223,7 +224,7 @@ class IterData(IterRawData):
def __init__(self, fa, channels, sensitivity, **kwargs): def __init__(self, fa, channels, sensitivity, **kwargs):
super().__init__(fa, channels, **kwargs) super().__init__(fa, channels, **kwargs)
self.sens = np.asarray(sensitivity)[self.channels] self.sens = np.asarray(sensitivity, dtype=LASP_NUMPY_FLOAT_TYPE)[self.channels]
assert self.sens.ndim == 1 assert self.sens.ndim == 1
def __next__(self): def __next__(self):
@ -235,7 +236,8 @@ class Measurement:
"""Provides access to measurement data stored in the h5 measurement file """Provides access to measurement data stored in the h5 measurement file
format.""" format."""
# Store a dict of open measurements, with uuid string as a key. We store them as a weak ref. # Store a dict of open measurements, with uuid string as a key. We store
# them as a weak ref.
uuid_s = WeakValueDictionary() uuid_s = WeakValueDictionary()
def __init__(self, fn): def __init__(self, fn):
@ -277,7 +279,7 @@ class Measurement:
try: try:
# Try to catch UUID (Universally Unique IDentifier) # Try to catch UUID (Universally Unique IDentifier)
self._UUID = f.attrs['UUID'] self._UUID = f.attrs["UUID"]
# Flag indicating we have to add a new UUID # Flag indicating we have to add a new UUID
create_new_uuid = False create_new_uuid = False
except KeyError: except KeyError:
@ -290,17 +292,17 @@ class Measurement:
# The last filename is a filename that *probably* is the reference measurement with # The last filename is a filename that *probably* is the reference measurement with
# given UUID. If it is not, we will search for it in the same directory as `this` measurement. # given UUID. If it is not, we will search for it in the same directory as `this` measurement.
# If we cannot find it there, we will give up, and remove the field corresponding to this reference measurement type. # If we cannot find it there, we will give up, and remove the field corresponding to this reference measurement type.
refMeas_list = f.attrs['refMeas'] refMeas_list = f.attrs["refMeas"]
# Build a tuple string from it # Build a tuple string from it
self._refMeas = {} self._refMeas = {}
for (key, val, name) in refMeas_list: for key, val, name in refMeas_list:
self._refMeas[MeasurementType(int(key))] = (val, name) self._refMeas[MeasurementType(int(key))] = (val, name)
except KeyError: except KeyError:
self._refMeas = {} self._refMeas = {}
try: try:
self._type_int = f.attrs['type_int'] self._type_int = f.attrs["type_int"]
except KeyError: except KeyError:
self._type_int = 0 self._type_int = 0
@ -329,10 +331,10 @@ class Measurement:
try: try:
sens = f.attrs["sensitivity"] sens = f.attrs["sensitivity"]
self._sens = ( self._sens = (
sens * np.ones(self.nchannels) if isinstance(sens, float) else sens sens * ones(self.nchannels) if isinstance(sens, float) else sens
) )
except KeyError: except KeyError:
self._sens = np.ones(self.nchannels) self._sens = ones(self.nchannels)
# The time is cached AND ALWAYS ASSUMED TO BE AN IMMUTABLE OBJECT. # The time is cached AND ALWAYS ASSUMED TO BE AN IMMUTABLE OBJECT.
# It is also cached. Changing the measurement timestamp should not # It is also cached. Changing the measurement timestamp should not
@ -367,7 +369,9 @@ class Measurement:
self.genNewUUID() self.genNewUUID()
else: else:
if self.UUID in Measurement.uuid_s.keys(): if self.UUID in Measurement.uuid_s.keys():
raise RuntimeError(f"Measurement '{self.name}' is already opened. Cannot open measurement twice. Note: this error can happen when measurements are manually copied.") raise RuntimeError(
f"Measurement '{self.name}' is already opened. Cannot open measurement twice. Note: this error can happen when measurements are manually copied."
)
# Store weak reference to 'self' in list of UUID's. They are removed when no file is open anymore # Store weak reference to 'self' in list of UUID's. They are removed when no file is open anymore
Measurement.uuid_s[self._UUID] = self Measurement.uuid_s[self._UUID] = self
@ -379,7 +383,7 @@ class Measurement:
Args: Args:
newname: New name, with or without extension newname: New name, with or without extension
""" """
_ , ext = os.path.splitext(newname) _, ext = os.path.splitext(newname)
# Add proper extension if new name is given without extension. # Add proper extension if new name is given without extension.
if ext != DOTMEXT: if ext != DOTMEXT:
newname = newname + DOTMEXT newname = newname + DOTMEXT
@ -396,7 +400,7 @@ class Measurement:
""" """
Create new UUID for measurement and store in file. Create new UUID for measurement and store in file.
""" """
self.setAttribute('UUID', str(uuid.uuid1())) self.setAttribute("UUID", str(uuid.uuid1()))
@property @property
def UUID(self): def UUID(self):
@ -423,40 +427,54 @@ class Measurement:
# Try to find it in the dictionary of of open measurements # Try to find it in the dictionary of of open measurements
if required_uuid in Measurement.uuid_s.keys(): if required_uuid in Measurement.uuid_s.keys():
m = Measurement.uuid_s[required_uuid] m = Measurement.uuid_s[required_uuid]
logging.debug(f'Returned reference measurement {m.name} from list of open measurements') logging.debug(
f"Returned reference measurement {m.name} from list of open measurements"
)
# Not found in list of openend measurements. See if we can open it using its last stored file name we know of # Not found in list of openend measurements. See if we can open it using its last stored file name we know of
if m is None: if m is None:
try: try:
m = Measurement(possible_name) m = Measurement(possible_name)
if m.UUID == required_uuid: if m.UUID == required_uuid:
logging.info(f'Opened reference measurement {m.name} by name') logging.info(f"Opened reference measurement {m.name} by name")
except Exception as e: except Exception as e:
logging.error(f'Could not find reference measurement using file name: {possible_name}') logging.error(
f"Could not find reference measurement using file name: {possible_name}"
)
# Last resort, see if we can find the right measurement in the same folder # Last resort, see if we can find the right measurement in the same folder
if m is None: if m is None:
try: try:
folder, _ = os.path.split(self.fn) folder, _ = os.path.split(self.fn)
m = Measurement.fromFolderWithUUID(required_uuid, folder, skip=[self.name]) m = Measurement.fromFolderWithUUID(
logging.info('Found reference measurement in folder with correct UUID. Updating name of reference measurement') required_uuid, folder, skip=[self.name]
)
logging.info(
"Found reference measurement in folder with correct UUID. Updating name of reference measurement"
)
# Update the measurement file name in the list, such that next time it # Update the measurement file name in the list, such that next time it
# can be opened just by its name. # can be opened just by its name.
self.setRefMeas(m) self.setRefMeas(m)
except: except:
logging.error("Could not find the reference measurement. Is it deleted?") logging.error(
"Could not find the reference measurement. Is it deleted?"
)
# Well, we found it. Now make sure the reference measurement actually has the right type (User could have marked it as a NotSpecific for example in the mean time). # Well, we found it. Now make sure the reference measurement actually has the right type (User could have marked it as a NotSpecific for example in the mean time).
if m is not None: if m is not None:
if m.measurementType() != mtype: if m.measurementType() != mtype:
m.removeRefMeas(mtype) m.removeRefMeas(mtype)
raise RuntimeError(f"Reference measurement for {self.name} is not a proper reference (anymore).") raise RuntimeError(
f"Reference measurement for {self.name} is not a proper reference (anymore)."
)
# Whow, we passed all security checks, here we go! # Whow, we passed all security checks, here we go!
return m return m
else: else:
# Nope, not there. # Nope, not there.
raise RuntimeError(f"Could not find the reference measurement for '{self.name}'. Is it deleted?") raise RuntimeError(
f"Could not find the reference measurement for '{self.name}'. Is it deleted?"
)
def removeRefMeas(self, mtype: MeasurementType): def removeRefMeas(self, mtype: MeasurementType):
""" """
@ -477,9 +495,12 @@ class Measurement:
with self.file("r+") as f: with self.file("r+") as f:
# Update attribute in file. Stored as a flat list of string tuples: # Update attribute in file. Stored as a flat list of string tuples:
# [(ref_value1, uuid_1, name_1), (ref_value2, uuid_2, name_2), ...] # [(ref_value1, uuid_1, name_1), (ref_value2, uuid_2, name_2), ...]
reflist = list((str(key.value), val1, val2) for key, (val1, val2) in self._refMeas.items()) reflist = list(
(str(key.value), val1, val2)
for key, (val1, val2) in self._refMeas.items()
)
# print(reflist) # print(reflist)
f.attrs['refMeas'] = reflist f.attrs["refMeas"] = reflist
def setRefMeas(self, m: Measurement): def setRefMeas(self, m: Measurement):
""" """
@ -489,19 +510,21 @@ class Measurement:
""" """
mtype = m.measurementType() mtype = m.measurementType()
if mtype == MeasurementType.NotSpecific: if mtype == MeasurementType.NotSpecific:
raise ValueError('Measurement to be set as reference is not a reference measurement') raise ValueError(
"Measurement to be set as reference is not a reference measurement"
)
self._refMeas[mtype] = (m.UUID, m.name) self._refMeas[mtype] = (m.UUID, m.name)
self.__storeReafMeas() self.__storeReafMeas()
@staticmethod @staticmethod
def fromFolderWithUUID(uuid_str: str, folder: str='', skip=[]): def fromFolderWithUUID(uuid_str: str, folder: str = "", skip=[]):
""" """
Returns Measurement object from a given UUID string. It first tries to find whether there Returns Measurement object from a given UUID string. It first tries to find whether there
is an uuid in the static list of weak references. If not, it will try to open files in is an uuid in the static list of weak references. If not, it will try to open files in
the current file path. the current file path.
""" """
for fn in glob.glob(str(pathlib.Path(folder)) + f'/*{DOTMEXT}'): for fn in glob.glob(str(pathlib.Path(folder)) + f"/*{DOTMEXT}"):
# Do not try to open this file in case it is in the 'skip' list. # Do not try to open this file in case it is in the 'skip' list.
if len(list(filter(lambda a: a in fn, skip))) > 0: if len(list(filter(lambda a: a in fn, skip))) > 0:
continue continue
@ -512,9 +535,11 @@ class Measurement:
# Update 'last_fn' attribute in dict of stored reference measurements # Update 'last_fn' attribute in dict of stored reference measurements
return m return m
except Exception as e: except Exception as e:
logging.error(f'Possible measurement file {fn} returned error {e} when opening.') logging.error(
f"Possible measurement file {fn} returned error {e} when opening."
)
raise RuntimeError(f'Measurement with UUID {uuid_str} could not be found.') raise RuntimeError(f"Measurement with UUID {uuid_str} could not be found.")
def setAttribute(self, attrname: str, value): def setAttribute(self, attrname: str, value):
""" """
@ -534,7 +559,7 @@ class Measurement:
""" """
Returns True when a measurement is flagged as being of a certaint "MeasurementType" Returns True when a measurement is flagged as being of a certaint "MeasurementType"
""" """
if (type_.value & self._type_int): if type_.value & self._type_int:
return True return True
elif type_.value == self._type_int == 0: elif type_.value == self._type_int == 0:
return True return True
@ -544,7 +569,7 @@ class Measurement:
""" """
Set the measurement type to given type Set the measurement type to given type
""" """
self.setAttribute('type_int', type_.value) self.setAttribute("type_int", type_.value)
def measurementType(self): def measurementType(self):
""" """
@ -885,7 +910,7 @@ class Measurement:
""" """
if isinstance(sens, float): if isinstance(sens, float):
# Put all sensitivities equal # Put all sensitivities equal
sens = sens * np.ones(self.nchannels) sens = sens * ones(self.nchannels)
elif isinstance(sens, list): elif isinstance(sens, list):
sens = np.asarray(sens) sens = np.asarray(sens)
@ -998,9 +1023,9 @@ class Measurement:
happen that a Measurement object is created twice for the same backing file, which we do not allow. happen that a Measurement object is created twice for the same backing file, which we do not allow.
""" """
# See if the base part of the filename is referring to a file that is already open # See if the base part of the filename is referring to a file that is already open
with h5.File(fn, 'r') as f: with h5.File(fn, "r") as f:
try: try:
theuuid = f.attrs['UUID'] theuuid = f.attrs["UUID"]
except KeyError: except KeyError:
# No UUID stored in measurement. This is an old measurement that did not have UUID's # No UUID stored in measurement. This is an old measurement that did not have UUID's
# We create a new UUID here such that the file is opened from the filesystem # We create a new UUID here such that the file is opened from the filesystem
@ -1090,8 +1115,8 @@ class Measurement:
sensitivity, sensitivity,
mfn, mfn,
timestamp=None, timestamp=None,
qtys: List[SIQtys]=None, qtys: List[SIQtys] = None,
channelNames: List[str]=None, channelNames: List[str] = None,
force=False, force=False,
) -> Measurement: ) -> Measurement:
""" """
@ -1161,7 +1186,7 @@ class Measurement:
hf.attrs["nchannels"] = nchannels hf.attrs["nchannels"] = nchannels
# Add physical quantity indices # Add physical quantity indices
hf.attrs['qtys_enum_idx'] = [qty.toInt() for qty in qtys] hf.attrs["qtys_enum_idx"] = [qty.toInt() for qty in qtys]
# Add channel names in case given # Add channel names in case given
if channelNames is not None: if channelNames is not None:
@ -1199,7 +1224,7 @@ class Measurement:
nchannels = 1 nchannels = 1
nframes = len(data) nframes = len(data)
data = data[:, np.newaxis] data = data[:, np.newaxis]
sensitivity = np.ones(nchannels) sensitivity = ones(nchannels)
with h5.File(newfn, "w") as hf: with h5.File(newfn, "w") as hf:
hf.attrs["samplerate"] = samplerate hf.attrs["samplerate"] = samplerate
@ -1217,4 +1242,3 @@ class Measurement:
ad[0] = data ad[0] = data
return Measurement(newfn) return Measurement(newfn)

View File

@ -14,17 +14,16 @@ class MeasurementSet(list):
""" """
Group of measurements that have some correspondence to one another. Class Group of measurements that have some correspondence to one another. Class
is used to operate on multiple measurements at once. is used to operate on multiple measurements at once.
""" """
def __init__(self, mlist: List[Measurement] = []): def __init__(self, mlist: List[Measurement] = []):
""" """
Initialize a measurement set Initialize a measurement set
Args: Arg:
mlist: Measurement list mlist: Measurement list
""" """
if any([not isinstance(i, Measurement) for i in mlist]): if any([not isinstance(i, Measurement) for i in mlist]):
raise TypeError("Object in list should be of Measurement type") raise TypeError("Object in list should be of Measurement type")
@ -33,11 +32,17 @@ class MeasurementSet(list):
super().__init__(mlist) super().__init__(mlist)
def getNewestReferenceMeasurement(self, mtype: MeasurementType): def getNewestReferenceMeasurement(self, mtype: MeasurementType):
"""Return the newest (in time) measurement in the current list of a certain type. Returns None in case no measurement could be found.
Args:
mtype (MeasurementType): The type required.
""" """
Get the NEWEST ref. measurement of a current type, in the current set.
Arg:
mtype (MeasurementType): The type required.
Return:
- The newest (in time) measurement in the current list of a certain type.
- None, in case no measurement could be found.
"""
mnewest = None mnewest = None
for m in self: for m in self:
if m.measurementType() == mtype: if m.measurementType() == mtype:
@ -49,25 +54,32 @@ class MeasurementSet(list):
return mnewest return mnewest
def getReferenceMeasurements(self, mtype: MeasurementType): def getReferenceMeasurements(self, mtype: MeasurementType):
"""Get all available reference measurements of a certain type in the """Get ALL ref. measurements of a certain type, in the current set.
current set.
Args: Arg:
mtype (MeasurementType): The type of which to list mtype (MeasurementType): The type of which to list
Returns: Return:
a new measurement set including all measurements of a certain type A new measurement set, including all measurements of a certain type
""" """
return [m for m in self if m.measurementType() == mtype] return [m for m in self if m.measurementType() == mtype]
def getNewestReferenceMeasurements(self): def getNewestReferenceMeasurements(self):
"""Returns a dictionary with newest measurement of each type that is not specific returns None in case no measurement is found.""" """
Get the NEWEST ref. measurement of all types, in the current set.
Return:
- A dictionary with the newest measurement of each type that is not specific
- None, in case no measurement is found.
"""
newest = {} newest = {}
for m in self: for m in self:
mtype = m.measurementType() mtype = m.measurementType()
if mtype == MeasurementType.NotSpecific: if mtype == MeasurementType.NotSpecific:
continue continue
if not mtype in newest: if mtype not in newest:
newest[mtype] = m newest[mtype] = m
else: else:
if m.time > newest[mtype].time: if m.time > newest[mtype].time:
@ -75,8 +87,17 @@ class MeasurementSet(list):
return newest return newest
def newestReferenceOlderThan(self, secs): def newestReferenceOlderThan(self, secs):
"""Returns a dictionary of references with the newest reference, that is still """
older than `secs` seconds.""" Get a dictionary of reference measurements which are older than a
specified threshold. Only one of each type is returned.
Args:
- secs: time threshold, in seconds
Return:
- a dictionary of references with the newest reference, that is still
older than `secs` seconds
"""
curtime = time.time() curtime = time.time()
newest = self.getNewestReferenceMeasurements() newest = self.getNewestReferenceMeasurements()
newest_older_than = {} newest_older_than = {}
@ -87,25 +108,29 @@ class MeasurementSet(list):
def measTimeSame(self): def measTimeSame(self):
""" """
Returns True if all measurements have the same measurement Returns True if all measurements have the same measurement length and
time (recorded time) sample rate
""" """
if len(self) > 0: if len(self) > 0:
first = self[0].N firstN = self[0].N # samples
return all([first == meas.N for meas in self]) firstFS = self[0].samplerate # sample rate
sameN = all([firstN == meas.N for meas in self])
sameFS = all([firstFS == meas.samplerate for meas in self])
return sameN and sameFS
else: else:
return False return False
def measSimilar(self): def measSimilar(self):
""" """
Similar means: channel metadata is the same, and the measurement time Similar means: channel metadata is the same, and the measurement time
is the same. It means that the recorded data is, of course, different. is the same.
Returns: Returns:
True if measChannelsSame() and measTimeSame() else False - True if measChannelsSame() and measTimeSame()
- False otherwise
""" """
return self.measTimeSame() and self.measChannelsSame() return self.measTimeSame() and self.measChannelsSame()
def measChannelsSame(self): def measChannelsSame(self):
@ -115,6 +140,7 @@ class MeasurementSet(list):
a set of measurements, simultaneously. If the channel data is the same a set of measurements, simultaneously. If the channel data is the same
(name, sensitivity, ...) it returns True. (name, sensitivity, ...) it returns True.
""" """
if len(self) > 0: if len(self) > 0:
first = self[0].channelConfig first = self[0].channelConfig
return all([first == meas.channelConfig for meas in self]) return all([first == meas.channelConfig for meas in self])

View File

@ -77,6 +77,7 @@ class FirFilterBank:
self.fs = fs self.fs = fs
self.xs = list(range(xmin, xmax + 1)) self.xs = list(range(xmin, xmax + 1))
raise RuntimeError('Not working code anymore')
maxdecimation = self.designer.firDecimation(self.xs[0]) maxdecimation = self.designer.firDecimation(self.xs[0])
self.decimators = [] self.decimators = []
@ -245,7 +246,7 @@ class SosFilterBank:
for i, x in enumerate(self.xs): for i, x in enumerate(self.xs):
channel = self.designer.createSOSFilter(x) channel = self.designer.createSOSFilter(x)
if sos is None: if sos is None:
sos = np.empty((channel.size, len(self.xs))) sos = empty((channel.size, len(self.xs)))
sos[:, i] = channel.flatten() sos[:, i] = channel.flatten()
self._fb = BiquadBank(sos) self._fb = BiquadBank(sos)

View File

@ -6,6 +6,7 @@ Description:
Reverberation time estimation tool using least squares Reverberation time estimation tool using least squares
""" """
from .lasp_common import getTime from .lasp_common import getTime
from .lasp_config import ones
import numpy as np import numpy as np
@ -56,7 +57,7 @@ class ReverbTime:
x = self._t[istart:istop][:, np.newaxis] x = self._t[istart:istop][:, np.newaxis]
# Solve the least-squares problem, by creating a matrix of # Solve the least-squares problem, by creating a matrix of
A = np.hstack([x, np.ones(x.shape)]) A = np.hstack([x, ones(x.shape)])
# print(A.shape) # print(A.shape)
# print(points.shape) # print(points.shape)

View File

@ -5,6 +5,7 @@ Sound level meter implementation
@author: J.A. de Jong - ASCEE @author: J.A. de Jong - ASCEE
""" """
from .lasp_cpp import cppSLM from .lasp_cpp import cppSLM
from .lasp_config import empty
import numpy as np import numpy as np
from .lasp_common import (TimeWeighting, FreqWeighting, P_REF) from .lasp_common import (TimeWeighting, FreqWeighting, P_REF)
from .filter import SPLFilterDesigner from .filter import SPLFilterDesigner
@ -101,7 +102,7 @@ class SLM:
assert fbdesigner.fs == fs assert fbdesigner.fs == fs
sos_firstx = fbdesigner.createSOSFilter(self.xs[0]).flatten() sos_firstx = fbdesigner.createSOSFilter(self.xs[0]).flatten()
self.nom_txt.append(fbdesigner.nominal_txt(self.xs[0])) self.nom_txt.append(fbdesigner.nominal_txt(self.xs[0]))
sos = np.empty((sos_firstx.size, nfilters), dtype=float, order='C') sos = empty((sos_firstx.size, nfilters), dtype=float, order='C')
sos[:, 0] = sos_firstx sos[:, 0] = sos_firstx
for i, x in enumerate(self.xs[1:]): for i, x in enumerate(self.xs[1:]):

View File

@ -47,7 +47,7 @@ class WeighCal:
P = 2048 # Filter length (number of taps) P = 2048 # Filter length (number of taps)
self._firs = np.empty((P, self.nchannels)) self._firs = empty((P, self.nchannels))
self._fbs = [] self._fbs = []
for chan in range(self.nchannels): for chan in range(self.nchannels):
fir = arbitrary_fir_design(fs, P, freq_design, fir = arbitrary_fir_design(fs, P, freq_design,

View File

@ -21,6 +21,7 @@ import copy
import numpy as np import numpy as np
from numpy import log2, pi, sin from numpy import log2, pi, sin
from ..lasp_cpp import freqSmooth from ..lasp_cpp import freqSmooth
from ..lasp_config import zeros
@unique @unique

View File

@ -31,7 +31,7 @@ def test_backward_fft():
nfft = 2048 nfft = 2048
freq = getFreq(nfft, nfft) freq = getFreq(nfft, nfft)
# Sig = np.zeros(nfft//2+1, dtype=complex) # Sig = zeros(nfft//2+1, dtype=complex)
Sigr = np.random.randn(nfft//2+1) Sigr = np.random.randn(nfft//2+1)
Sigi = np.random.randn(nfft//2+1) Sigi = np.random.randn(nfft//2+1)

@ -1 +1 @@
Subproject commit daaf637f6f9fce670031221abfd7dfde92e5cce3 Subproject commit 18a606e1f928852bfc29639d9539ae74d37b5dee