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
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
submodules: true

View File

@ -1,17 +1,27 @@
cmake_minimum_required (VERSION 3.16)
project(LASP LANGUAGES C CXX VERSION 1.1)
cmake_minimum_required(VERSION 3.16)
project(LASP LANGUAGES C CXX VERSION 1.6.3)
set(CMAKE_CXX_STANDARD 17)
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 /
# Windows.
set(DEFAULT_DOUBLE_PRECISION ON)
if(WIN32)
set(DEFAULT_RTAUDIO OFF)
set(DEFAULT_PORTAUDIO ON)
set(DEFAULT_ULDAQ OFF)
elseif(${RPI})
set(DEFAULT_RTAUDIO OFF)
set(DEFAULT_PORTAUDIO ON)
set(DEFAULT_ULDAQ OFF)
set(DEFAULT_DOUBLE_PRECISION OFF)
else()
set(DEFAULT_RTAUDIO OFF)
set(DEFAULT_PORTAUDIO ON)
@ -19,6 +29,7 @@ else()
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_PORTAUDIO "Compile with PortAudio Daq backend" ${DEFAULT_PORTAUDIO})
if(LASP_HAS_PORTAUDIO AND LASP_HAS_RTAUDIO)
@ -34,7 +45,7 @@ option(LASP_BUILD_CPP_TESTS "Build CPP test code" OFF)
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CCACHE_PROGRAM}")
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CCACHE_PROGRAM}")
endif()
# To allow linking to static libs from other directories
@ -80,7 +91,7 @@ endif()
# Tune for current machine
if(LASP_BUILD_TUNED)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(FATAL_ERROR
message(FATAL_ERROR
"Cannot build optimized and tuned code when debug is switched on")
endif()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native -mtune=native")
@ -105,13 +116,17 @@ else()
endif()
# ###################################### 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")
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")
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 ")
# ############################# End compilation flags
@ -121,7 +136,7 @@ include(OSSpecific)
include(rtaudio)
include(portaudio)
include(uldaq)
#
#
add_definitions(-Dgsl_CONFIG_DEFAULTS_VERSION=1)
add_subdirectory(cpp_src)
if(LASP_BUILD_CPP_TESTS)

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).
# Installation - Linux (Ubuntu-based)
## Prerequisites
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`
## Installation from wheel (recommended for non-developers)
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`
- `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
@ -144,3 +168,25 @@ This will build the documentation. It can be read by:
- 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

@ -107,37 +107,12 @@ void StreamMgr::rescanDAQDevices(bool background,
_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) {
DEBUGTRACE_ENTER;
assert(!_inputStream && !_outputStream);
Lck lck(_mtx);
// Alsa spits out annoying messages that are not useful
{
MuteErrHandler guard;
_devices = DeviceInfo::getDeviceInfo();
}

View File

@ -16,6 +16,33 @@ using std::endl;
using std::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) {
DEBUGTRACE_ENTER;
if (e != paNoError) {
@ -44,6 +71,7 @@ class OurPaDeviceInfo : public DeviceInfo {
void fillPortAudioDeviceInfo(DeviceInfoList &devinfolist) {
DEBUGTRACE_ENTER;
bool shouldPaTerminate = false;
MuteErrHandler guard;
try {
PaError err = Pa_Initialize();
/// 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) {
DEBUGTRACE_ENTER;
assert(_stream);
MuteErrHandler guard;
if (Pa_IsStreamActive(_stream)) {
throw rte("Stream is already running");

View File

@ -11,7 +11,7 @@ using std::cerr;
using std::endl;
// 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) {
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 a1 = 9240. / 18608.;
d a2 = 1430. / 18608.;
return a0 - a1 * d_cos((2 * number_pi/N) * lin0N) +
a2 * d_cos((4 * number_pi / N)* lin0N );
return a0 - a1 * arma::cos((2 * number_pi/N) * lin0N) +
a2 * arma::cos((4 * number_pi / N)* lin0N );
}
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) {
#if LASP_DOUBLE_PRECISION == 1
m.attr("LASP_DOUBLE_PRECISION") = true;
#else
m.attr("LASP_DOUBLE_PRECISION") = false;
#endif
init_dsp(m);
init_deviceinfo(m);
init_daqconfiguration(m);
@ -51,6 +57,5 @@ PYBIND11_MODULE(lasp_cpp, m) {
init_streammgr(m);
init_datahandler(m);
init_siggen(m);
}
/** @} */
/** @} */

View File

@ -5,7 +5,7 @@ requires-python = ">=3.10"
description = "Library for Acoustic Signal Processing"
license = { "file" = "LICENSE" }
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"]
@ -23,7 +23,7 @@ classifiers = [
urls = { "Documentation" = "https://lasp.ascee.nl" }
dependencies = [
"scipy==1.12",
"scipy>=1.13.1",
"matplotlib>=3.7.2",
"appdirs",
"dataclasses_json",

View File

@ -11,7 +11,9 @@ __all__ = ['freqResponse', 'bandpass_fir_design', 'lowpass_fir_design',
'arbitrary_fir_design']
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.):
@ -44,7 +46,7 @@ def bandpass_fir_design(L, fs, fl, fu, window=hann):
Omg2 = 2*np.pi*fu/fs
Omg1 = 2*np.pi*fl/fs
fir = np.empty(L, dtype=float)
fir = empty(L, dtype=float)
# First Create ideal band-pass filter
fir[L//2] = (Omg2-Omg1)/np.pi
@ -64,7 +66,7 @@ def lowpass_fir_design(L, fs, fc, window=hann):
" than upper cut-off"
Omgc = 2*np.pi*fc/fs
fir = np.empty(L, dtype=float)
fir = empty(L, dtype=float)
# First Create ideal band-pass filter
fir[L//2] = Omgc/np.pi

View File

@ -6,8 +6,14 @@ Author: J.A. de Jong - ASCEE
Description: LASP configuration
"""
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):

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
# -*- coding: utf-8 -*-
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
@ -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
MEXT = 'h5'
DOTMEXT = f'.{MEXT}'
MEXT = "h5"
DOTMEXT = f".{MEXT}"
@unique
@ -223,7 +224,7 @@ class IterData(IterRawData):
def __init__(self, fa, channels, sensitivity, **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
def __next__(self):
@ -235,7 +236,8 @@ class Measurement:
"""Provides access to measurement data stored in the h5 measurement file
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()
def __init__(self, fn):
@ -271,36 +273,36 @@ class Measurement:
self.version_major = f.attrs["LASP_VERSION_MAJOR"]
self.version_minor = f.attrs["LASP_VERSION_MINOR"]
except KeyError:
# No version information stored
# No version information stored
self.version_major = 0
self.version_minor = 1
try:
# 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
create_new_uuid = False
except KeyError:
create_new_uuid = True
try:
# UUID of the reference measurement. Should be stored as
# UUID of the reference measurement. Should be stored as
# a lists of tuples, where each tuple is a combination of (<MeasurementType.value>, <uuid_string>, <last_filename>).
# 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.
# 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
self._refMeas = {}
for (key, val, name) in refMeas_list:
for key, val, name in refMeas_list:
self._refMeas[MeasurementType(int(key))] = (val, name)
except KeyError:
self._refMeas = {}
try:
self._type_int = f.attrs['type_int']
self._type_int = f.attrs["type_int"]
except KeyError:
self._type_int = 0
@ -329,10 +331,10 @@ class Measurement:
try:
sens = f.attrs["sensitivity"]
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:
self._sens = np.ones(self.nchannels)
self._sens = ones(self.nchannels)
# The time is cached AND ALWAYS ASSUMED TO BE AN IMMUTABLE OBJECT.
# It is also cached. Changing the measurement timestamp should not
@ -367,7 +369,9 @@ class Measurement:
self.genNewUUID()
else:
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
Measurement.uuid_s[self._UUID] = self
@ -379,11 +383,11 @@ class Measurement:
Args:
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.
if ext != DOTMEXT:
newname = newname + DOTMEXT
# Folder, Base filename + extension
folder, _ = os.path.split(self.fn)
@ -396,7 +400,7 @@ class Measurement:
"""
Create new UUID for measurement and store in file.
"""
self.setAttribute('UUID', str(uuid.uuid1()))
self.setAttribute("UUID", str(uuid.uuid1()))
@property
def UUID(self):
@ -404,59 +408,73 @@ class Measurement:
Universally unique identifier
"""
return self._UUID
def getRefMeas(self, mtype: MeasurementType):
"""
Return corresponding reference measurement, if configured and can be found. If the reference
Return corresponding reference measurement, if configured and can be found. If the reference
measurement is currently not open, it tries to open it by traversing other measurement
files in the current directory. Throws a runtime error in case the reference measurement cannot be found.
Throws a ValueError when the reference measurement is not configured.
"""
# See if we can find the UUID for the required measurement type
try:
required_uuid, possible_name = self._refMeas[mtype]
except KeyError:
raise ValueError(f"No reference measurement configured for '{self.name}'")
m = None
# Try to find it in the dictionary of of open measurements
if required_uuid in Measurement.uuid_s.keys():
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
if m is None:
try:
m = Measurement(possible_name)
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:
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
if m is None:
try:
folder, _ = os.path.split(self.fn)
m = Measurement.fromFolderWithUUID(required_uuid, folder, skip=[self.name])
logging.info('Found reference measurement in folder with correct UUID. Updating name of reference measurement')
m = Measurement.fromFolderWithUUID(
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
# can be opened just by its name.
self.setRefMeas(m)
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).
if m is not None:
if m.measurementType() != 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!
return m
else:
# 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):
"""
@ -477,9 +495,12 @@ class Measurement:
with self.file("r+") as f:
# Update attribute in file. Stored as a flat list of string tuples:
# [(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)
f.attrs['refMeas'] = reflist
f.attrs["refMeas"] = reflist
def setRefMeas(self, m: Measurement):
"""
@ -489,32 +510,36 @@ class Measurement:
"""
mtype = m.measurementType()
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.__storeReafMeas()
@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
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.
"""
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.
if len(list(filter(lambda a: a in fn, skip))) > 0:
continue
try:
m = Measurement(fn)
if m.UUID == uuid_str:
if m.UUID == uuid_str:
# Update 'last_fn' attribute in dict of stored reference measurements
return m
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):
"""
@ -534,17 +559,17 @@ class Measurement:
"""
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
elif type_.value == self._type_int == 0:
return True
return False
def setType(self, type_: MeasurementType):
"""
Set the measurement type to given type
"""
self.setAttribute('type_int', type_.value)
self.setAttribute("type_int", type_.value)
def measurementType(self):
"""
@ -589,7 +614,7 @@ class Measurement:
@channelConfig.setter
def channelConfig(self, chcfg: List[DaqChannel]):
"""
Set new channel configuration from list of DaqChannel objects.
Set new channel configuration from list of DaqChannel objects.
Use cases:
- Update channel types, sensitivities etc.
@ -885,7 +910,7 @@ class Measurement:
"""
if isinstance(sens, float):
# Put all sensitivities equal
sens = sens * np.ones(self.nchannels)
sens = sens * ones(self.nchannels)
elif isinstance(sens, list):
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.
"""
# 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:
theuuid = f.attrs['UUID']
theuuid = f.attrs["UUID"]
except KeyError:
# 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
@ -1009,7 +1034,7 @@ class Measurement:
if theuuid in Measurement.uuid_s.keys():
return Measurement.uuid_s[theuuid]
return Measurement(fn)
@staticmethod
@ -1090,8 +1115,8 @@ class Measurement:
sensitivity,
mfn,
timestamp=None,
qtys: List[SIQtys]=None,
channelNames: List[str]=None,
qtys: List[SIQtys] = None,
channelNames: List[str] = None,
force=False,
) -> Measurement:
"""
@ -1161,7 +1186,7 @@ class Measurement:
hf.attrs["nchannels"] = nchannels
# 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
if channelNames is not None:
@ -1199,7 +1224,7 @@ class Measurement:
nchannels = 1
nframes = len(data)
data = data[:, np.newaxis]
sensitivity = np.ones(nchannels)
sensitivity = ones(nchannels)
with h5.File(newfn, "w") as hf:
hf.attrs["samplerate"] = samplerate
@ -1217,4 +1242,3 @@ class Measurement:
ad[0] = data
return Measurement(newfn)

View File

@ -14,17 +14,16 @@ class MeasurementSet(list):
"""
Group of measurements that have some correspondence to one another. Class
is used to operate on multiple measurements at once.
"""
def __init__(self, mlist: List[Measurement] = []):
"""
Initialize a measurement set
Args:
Arg:
mlist: Measurement list
"""
if any([not isinstance(i, Measurement) for i in mlist]):
raise TypeError("Object in list should be of Measurement type")
@ -33,11 +32,17 @@ class MeasurementSet(list):
super().__init__(mlist)
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
for m in self:
if m.measurementType() == mtype:
@ -49,25 +54,32 @@ class MeasurementSet(list):
return mnewest
def getReferenceMeasurements(self, mtype: MeasurementType):
"""Get all available reference measurements of a certain type in the
current set.
"""Get ALL ref. measurements of a certain type, in the current set.
Args:
Arg:
mtype (MeasurementType): The type of which to list
Returns:
a new measurement set including all measurements of a certain type
Return:
A new measurement set, including all measurements of a certain type
"""
return [m for m in self if m.measurementType() == mtype]
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 = {}
for m in self:
mtype = m.measurementType()
if mtype == MeasurementType.NotSpecific:
continue
if not mtype in newest:
if mtype not in newest:
newest[mtype] = m
else:
if m.time > newest[mtype].time:
@ -75,8 +87,17 @@ class MeasurementSet(list):
return newest
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()
newest = self.getNewestReferenceMeasurements()
newest_older_than = {}
@ -87,25 +108,29 @@ class MeasurementSet(list):
def measTimeSame(self):
"""
Returns True if all measurements have the same measurement
time (recorded time)
Returns True if all measurements have the same measurement length and
sample rate
"""
if len(self) > 0:
first = self[0].N
return all([first == meas.N for meas in self])
firstN = self[0].N # samples
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:
return False
def measSimilar(self):
"""
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:
True if measChannelsSame() and measTimeSame() else False
- True if measChannelsSame() and measTimeSame()
- False otherwise
"""
return self.measTimeSame() and self.measChannelsSame()
def measChannelsSame(self):
@ -115,6 +140,7 @@ class MeasurementSet(list):
a set of measurements, simultaneously. If the channel data is the same
(name, sensitivity, ...) it returns True.
"""
if len(self) > 0:
first = self[0].channelConfig
return all([first == meas.channelConfig for meas in self])

View File

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

View File

@ -6,6 +6,7 @@ Description:
Reverberation time estimation tool using least squares
"""
from .lasp_common import getTime
from .lasp_config import ones
import numpy as np
@ -56,7 +57,7 @@ class ReverbTime:
x = self._t[istart:istop][:, np.newaxis]
# 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(points.shape)

View File

@ -5,6 +5,7 @@ Sound level meter implementation
@author: J.A. de Jong - ASCEE
"""
from .lasp_cpp import cppSLM
from .lasp_config import empty
import numpy as np
from .lasp_common import (TimeWeighting, FreqWeighting, P_REF)
from .filter import SPLFilterDesigner
@ -101,7 +102,7 @@ class SLM:
assert fbdesigner.fs == fs
sos_firstx = fbdesigner.createSOSFilter(self.xs[0]).flatten()
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
for i, x in enumerate(self.xs[1:]):

View File

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

View File

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

View File

@ -31,7 +31,7 @@ def test_backward_fft():
nfft = 2048
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)
Sigi = np.random.randn(nfft//2+1)

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