Added suggested SLM down sampling factor, improved quite a lot of documentation. Measurement object can now work with old, as well as new measurement files.

This commit is contained in:
Anne de Jong 2022-10-01 19:59:35 +02:00
parent 59085b091f
commit bb26fc6bcc
15 changed files with 214 additions and 102 deletions

View File

@ -11,7 +11,7 @@ __version__ = lasp_cpp.__version__
# from .lasp_imptube import * # TwoMicImpedanceTube # from .lasp_imptube import * # TwoMicImpedanceTube
from .lasp_measurement import * # Measurement, scaleBlockSens from .lasp_measurement import * # Measurement, scaleBlockSens
from .lasp_octavefilter import * from .lasp_octavefilter import *
# from .lasp_slm import * # SLM, Dummy from .lasp_slm import * # SLM, Dummy
from .lasp_record import * # RecordStatus, Recording from .lasp_record import * # RecordStatus, Recording
from .lasp_daqconfigs import * from .lasp_daqconfigs import *
# from .lasp_siggen import * # SignalType, NoiseType, SiggenMessage, SiggenData, Siggen # from .lasp_siggen import * # SignalType, NoiseType, SiggenMessage, SiggenData, Siggen

View File

@ -200,6 +200,19 @@ public:
* output. * output.
*/ */
double digitalHighPassCutOn = -1; double digitalHighPassCutOn = -1;
/**
* @brief Compare two channels to eachother. They are equal when the name,
* sensitivity and quantity are the same.
*
* @param other The DaqChannel to compare with
*
* @return true if equal
*/
bool operator==(const DaqChannel& other) const {
return other.name == name && other.sensitivity == sensitivity &&
other.qty == qty;
}
}; };
/** /**

View File

@ -7,6 +7,10 @@
#include <optional> #include <optional>
/** \defgroup dsp Digital Signal Processing utilities /** \defgroup dsp Digital Signal Processing utilities
* These are classes and functions used for processing raw signal data, to
* obtain statistics.
*
*
* @{ * @{
*/ */

View File

@ -2,7 +2,7 @@
#include "lasp_filter.h" #include "lasp_filter.h"
/** /**
* \ingroup dsp * \addtogroup dsp
* @{ * @{
*/ */
@ -53,7 +53,8 @@ public:
/** /**
* @brief Initialize biquadbank. * @brief Initialize biquadbank.
* *
* @param filters Filters for each filter in the bank * @param filters Filters for each filter in the bank. First axis isis the
* coefficient index, second axis is the filter index.
* @param gains Gain values. Given as pointer, if not given (nulltpr), gains * @param gains Gain values. Given as pointer, if not given (nulltpr), gains
* are initialized with unity gain. * are initialized with unity gain.
*/ */

View File

@ -9,7 +9,7 @@
using std::cerr; using std::cerr;
using std::endl; using std::endl;
using std::runtime_error; using rte = std::runtime_error;
using std::unique_ptr; using std::unique_ptr;
SLM::SLM(const d fs, const d Lref, const us downsampling_fac, const d tau, SLM::SLM(const d fs, const d Lref, const us downsampling_fac, const d tau,
@ -40,13 +40,13 @@ SLM::SLM(const d fs, const d Lref, const us downsampling_fac, const d tau,
getPool(); getPool();
if (Lref <= 0) { if (Lref <= 0) {
throw runtime_error("Invalid reference level"); throw rte("Invalid reference level");
} }
if (tau <= 0) { if (tau <= 0) {
throw runtime_error("Invalid time constant for Single pole lowpass filter"); throw rte("Invalid time constant for Single pole lowpass filter");
} }
if (fs <= 0) { if (fs <= 0) {
throw runtime_error("Invalid sampling frequency"); throw rte("Invalid sampling frequency");
} }
} }
SLM::~SLM() {} SLM::~SLM() {}
@ -66,6 +66,21 @@ std::vector<unique_ptr<Filter>> createBandPass(const dmat &coefs) {
} }
return bf; return bf;
} }
us SLM::suggestedDownSamplingFac(const d fs,const d tau) {
if(tau<0) throw rte("Invalid time weightin time constant");
if(fs<=0) throw rte("Invalid sampling frequency");
// A reasonable 'framerate' for the sound level meter, based on the
// filtering time constant.
if (tau > 0) {
d fs_slm = 10 / tau;
if(fs_slm < 30) {
fs_slm = 30;
}
return std::max((us) 1, static_cast<us>(fs / fs_slm));
} else {
return 1;
}
}
SLM SLM::fromBiquads(const d fs, const d Lref, const us downsampling_fac, SLM SLM::fromBiquads(const d fs, const d Lref, const us downsampling_fac,
const d tau, const vd &pre_filter_coefs, const d tau, const vd &pre_filter_coefs,

View File

@ -98,7 +98,8 @@ public:
* @param downsampling_fac Every 1/downsampling_fac value is returned from * @param downsampling_fac Every 1/downsampling_fac value is returned from
* compute() * compute()
* @param tau Time consant of level meter * @param tau Time consant of level meter
* @param bandpass_coefs Biquad filter coeffiecients for bandpass filter * @param bandpass_coefs Biquad filter coefficients for bandpass filter. First axis isis the coefficient index, second axis is the filter index.
* *
* @return Sound Level Meter object * @return Sound Level Meter object
*/ */
@ -149,6 +150,20 @@ public:
*/ */
vd Lmax() const { return 10 * arma::log10(Pmax / Lrefsq); }; vd Lmax() const { return 10 * arma::log10(Pmax / Lrefsq); };
/**
* @brief Comput a 'suggested' downsampling factor, i.e. a lower frame rate
* at which sound level meter values are returned from the computation. This
* is possible since the signal power is low-pas filtered with a single pole
* low pass filter. It can remove computational burden, especially for
* plotting, to have a value > 10.
*
* @param fs Sampling frequency of signal [Hz]
* @param tw Time weighting of SLM low pass filter
*
* @return Suggested downsampling factor, no unit. [-]
*/
static us suggestedDownSamplingFac(const d fs,const d tw);
private: private:
vd run_single(vd input, const us filter_no); vd run_single(vd input, const us filter_no);
}; };

View File

@ -460,7 +460,15 @@ class OctaveBankDesigner(FilterBankDesigner):
class ThirdOctaveBankDesigner(FilterBankDesigner): class ThirdOctaveBankDesigner(FilterBankDesigner):
def __init__(self, fs): def __init__(self, fs: float):
"""
Initialize ThirdOctaveBankDesigner, a filter bank designer for
one-third octave bands
Args:
fs: Sampling frequency in [Hz]
"""
super().__init__(fs) super().__init__(fs)
self.xs = list(range(-16, 14)) self.xs = list(range(-16, 14))
# Text corresponding to the nominal frequency # Text corresponding to the nominal frequency

View File

@ -84,7 +84,6 @@ class Qty:
@unique @unique
class SIQtys(Enum): class SIQtys(Enum):
N = Qty(name='Number', N = Qty(name='Number',
@ -113,26 +112,20 @@ class SIQtys(Enum):
cpp_enum = DaqChannel.Qty.Voltage cpp_enum = DaqChannel.Qty.Voltage
) )
@staticmethod
def fillComboBox(cb):
"""
Fill to a combobox
Args:
cb: QComboBox to fill
"""
cb.clear()
for ty in SIQtys:
cb.addItem(f'{ty.value.unit_name}')
cb.setCurrentIndex(0)
@staticmethod @staticmethod
def default(): def default():
return SIQtys.N.value return SIQtys.N.value
@staticmethod @staticmethod
def getCurrent(cb): def fromCppEnum(enum):
return list(SIQtys)[cb.currentIndex()] """
Convert enumeration index from - say - a measurement file back into
physical quantity information.
"""
for qty in SIQtys:
if qty.value.cpp_enum.value == enum:
return qty.value
raise RuntimeError(f'Qty corresponding to enum {enum} not found')
@dataclass @dataclass

View File

@ -16,9 +16,13 @@ is assumed to be acoustic data.
'sensitivity': (Optionally) the stored sensitivity of the record channels. 'sensitivity': (Optionally) the stored sensitivity of the record channels.
This can be a single value, or a list of sensitivities for This can be a single value, or a list of sensitivities for
each channel. Both representations are allowed. each channel. Both representations are allowed.
For measurement files of LASP < v1.0
'qtys' : (Optionally): list of quantities that is recorded for each channel', 'qtys' : (Optionally): list of quantities that is recorded for each channel',
if this array is not found. Quantities are defaulted to 'Number / Full scale' if this array is not found. Quantities are defaulted to 'Number / Full scale'
For measurement files of LASP >= 1.0
- Datasets: - Datasets:
'audio': 3-dimensional array of blocks of audio data. The first axis is the 'audio': 3-dimensional array of blocks of audio data. The first axis is the
@ -46,7 +50,7 @@ from .lasp_config import LASP_NUMPY_FLOAT_TYPE
from scipy.io import wavfile from scipy.io import wavfile
import os, time, wave, logging import os, time, wave, logging
from .lasp_common import SIQtys, Qty, getFreq from .lasp_common import SIQtys, Qty, getFreq
from .lasp_cpp import Window, DaqChannel from .lasp_cpp import Window, DaqChannel, LASP_VERSION_MAJOR
def getSampWidth(dtype): def getSampWidth(dtype):
@ -208,6 +212,13 @@ class Measurement:
self.N = (self.nblocks * self.blocksize) self.N = (self.nblocks * self.blocksize)
self.T = self.N / self.samplerate self.T = self.N / self.samplerate
try:
self.version_major = f.attrs['LASP_VERSION_MAJOR']
self.version_minor = f.attrs['LASP_VERSION_MINOR']
except KeyError:
self.version_major = 0
self.version_minor = 1
# Due to a previous bug, the channel names were not stored # Due to a previous bug, the channel names were not stored
# consistently, i.e. as 'channel_names' and later camelcase. # consistently, i.e. as 'channel_names' and later camelcase.
try: try:
@ -239,6 +250,7 @@ class Measurement:
self._time = f.attrs['time'] self._time = f.attrs['time']
self._qtys = None
try: try:
qtys_json = f.attrs['qtys'] qtys_json = f.attrs['qtys']
# Load quantity data # Load quantity data
@ -246,9 +258,20 @@ class Measurement:
except KeyError: except KeyError:
# If quantity data is not available, this is an 'old' # If quantity data is not available, this is an 'old'
# measurement file. # measurement file.
logging.debug(f'Physical quantity data not available in measurement file. Assuming {SIQtys.default}') pass
self._qtys = [SIQtys.default() for i in range(self.nchannels)]
try:
qtys_enum_idx = f.attrs['qtys_enum_idx']
self._qtys = [SIQtys.fromCppEnum(idx) for idx in qtys_enum_idx]
except KeyError:
pass
if self._qtys is None:
self._qtys = [SIQtys.default() for i in range(self.nchannels)]
logging.debug(f'Physical quantity data not available in measurement file. Assuming {SIQtys.default}')
logging.debug(f'Physical quantity data not available in measurement file. Assuming {SIQtys.default}')
def setAttribute(self, atrname, value): def setAttribute(self, atrname, value):
""" """
Set an attribute in the measurement file, and keep a local copy in Set an attribute in the measurement file, and keep a local copy in
@ -276,14 +299,15 @@ class Measurement:
@property @property
def channelConfig(self): def channelConfig(self):
chcfg = [DaqChannel(channel_enabled=True, chcfg = []
channel_name=chname, for chname, sens, qty in zip(self.channelNames, self.sensitivity,
sensitivity=sens,) self.qtys):
for chname, sens in zip( ch = DaqChannel()
self.channelNames, ch.enabled = True
self.sensitivity)] ch.name = chname
for i, qty in enumerate(self.qtys): ch.sensitivity = sens
chcfg[i].qty = qty ch.qty = qty.cpp_enum
chcfg.append(ch)
return chcfg return chcfg
@channelConfig.setter @channelConfig.setter
@ -292,9 +316,9 @@ class Measurement:
sens = [] sens = []
qtys = [] qtys = []
for ch in chcfg: for ch in chcfg:
chname.append(ch.channel_name) chname.append(ch.name)
sens.append(ch.sensitivity) sens.append(ch.sensitivity)
qtys.append(ch.qty) qtys.append(SIQtys.fromCppEnum(ch.qty))
self.channelNames = chname self.channelNames = chname
self.sensitivity = sens self.sensitivity = sens

View File

@ -9,7 +9,8 @@ import os
import time import time
import h5py import h5py
import numpy as np import numpy as np
from .lasp_cpp import InDataHandler, StreamMgr from .lasp_cpp import (InDataHandler, StreamMgr, LASP_VERSION_MAJOR,
LASP_VERSION_MINOR)
from .lasp_atomic import Atomic from .lasp_atomic import Atomic
@ -119,12 +120,16 @@ class Recording:
f = self.f f = self.f
f.attrs['LASP_VERSION_MAJOR'] = LASP_VERSION_MAJOR
f.attrs['LASP_VERSION_MINOR'] = LASP_VERSION_MINOR
# Set the bunch of attributes # Set the bunch of attributes
f.attrs['samplerate'] = daq.samplerate() f.attrs['samplerate'] = daq.samplerate()
f.attrs['nchannels'] = daq.neninchannels() f.attrs['nchannels'] = daq.neninchannels()
f.attrs['blocksize'] = blocksize f.attrs['blocksize'] = blocksize
f.attrs['sensitivity'] = [ch.sensitivity for ch in in_ch] f.attrs['sensitivity'] = [ch.sensitivity for ch in in_ch]
f.attrs['channelNames'] = [ch.name for ch in in_ch] f.attrs['channelNames'] = [ch.name for ch in in_ch]
# f.attrs['
# Add the start delay here, as firstFrames() is called right after the # Add the start delay here, as firstFrames() is called right after the
# constructor is called. # constructor is called.

View File

@ -4,7 +4,7 @@
Sound level meter implementation Sound level meter implementation
@author: J.A. de Jong - ASCEE @author: J.A. de Jong - ASCEE
""" """
from .lasp_cpp import SLM as cppSLM from .lasp_cpp import cppSLM
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
@ -33,8 +33,8 @@ class SLM:
def __init__(self, def __init__(self,
fs, fs,
fbdesigner=None, fbdesigner=None,
tw=TimeWeighting.fast, tw: TimeWeighting =TimeWeighting.fast,
fw=FreqWeighting.A, fw: FreqWeighting =FreqWeighting.A,
xmin = None, xmin = None,
xmax = None, xmax = None,
include_overall=True, include_overall=True,
@ -50,7 +50,7 @@ class SLM:
fs: Sampling frequency [Hz] fs: Sampling frequency [Hz]
tw: Time Weighting to apply tw: Time Weighting to apply
fw: Frequency weighting to apply fw: Frequency weighting to apply
xmin: Filter designator of lowest band xmin: Filter designator of lowest band.
xmax: Filter designator of highest band xmax: Filter designator of highest band
include_overall: If true, a non-functioning filter is added which include_overall: If true, a non-functioning filter is added which
is used to compute the overall level. is used to compute the overall level.
@ -96,36 +96,42 @@ 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((nfilters, sos_firstx.size), dtype=float, order='C') sos = np.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:]):
sos[i+1, :] = fbdesigner.createSOSFilter(x).flatten() sos[:, i+1] = fbdesigner.createSOSFilter(x).flatten()
self.nom_txt.append(fbdesigner.nominal_txt(x)) self.nom_txt.append(fbdesigner.nominal_txt(x))
if include_overall: if include_overall:
# Create a unit impulse response filter, every third index equals # Create a unit impulse response filter, every third index equals
# 1, so b0 = 1 and a0 is 1 (by definition) # 1, so b0 = 1 and a0 is 1 (by definition)
# a0 = 1, b0 = 1, rest is zero # a0 = 1, b0 = 1, rest is zero
sos[-1,:] = sos_overall sos[:,-1] = sos_overall
self.nom_txt.append('overall') self.nom_txt.append('overall')
else: else:
# No filterbank, means we do only compute the overall values. This # No filterbank, means we do only compute the overall values. This
# means that in case of include_overall, it creates two overall # means that in case of include_overall, it creates two overall
# channels. That would be confusing, so we do not allow it. # channels. That would be confusing, so we do not allow it.
sos = sos_overall[np.newaxis,:] sos = sos_overall[:,np.newaxis]
self.nom_txt.append('overall') self.nom_txt.append('overall')
self.slm = cppSLM(fs, prefilter, sos, # Downsampling factor, determine from single pole low pass filter time
fs, tw[0], level_ref_value) # constant, such that aliasing is ~ allowed at 20 dB lower value
# and
dsfac = cppSLM.suggestedDownSamplingFac(fs, tw[0])
dsfac = self.slm.downsampling_fac if prefilter is not None:
if dsfac > 0: self.slm = cppSLM.fromBiquads(fs, level_ref_value, dsfac,
# Not unfiltered data tw[0],
self.fs_slm = fs / self.slm.downsampling_fac prefilter, sos)
else: else:
self.fs_slm = fs self.slm = cppSLM.fromBiquads(fs, level_ref_value, dsfac,
tw[0],
sos)
self.fs_slm = fs / dsfac
# Initialize counter to 0 # Initialize counter to 0
self.N = 0 self.N = 0
@ -189,7 +195,6 @@ class SLM:
output['mid'] = self.fbdesigner.fm(list(self.xs)) output['mid'] = self.fbdesigner.fm(list(self.xs))
logging.debug(list(self.xs)) logging.debug(list(self.xs))
logging.debug(output['mid']) logging.debug(output['mid'])
# Indiced at 0, as pyxSLM always returns 2D arrays.
if self.include_overall and self.fbdesigner is not None: if self.include_overall and self.fbdesigner is not None:
output['overall'] = dat[-1,0] output['overall'] = dat[-1,0]

View File

@ -69,6 +69,8 @@ void init_daqconfiguration(py::module &m) {
.value("UserDefined", DaqChannel::Qty::UserDefined); .value("UserDefined", DaqChannel::Qty::UserDefined);
daqchannel.def_readwrite("qty", &DaqChannel::qty); daqchannel.def_readwrite("qty", &DaqChannel::qty);
daqchannel.def("__eq__", [](const DaqChannel& a, const DaqChannel& b) { return a==b;});
/// DaqConfiguration /// DaqConfiguration
daqconfig.def(py::init<>()); daqconfig.def(py::init<>());
daqconfig.def(py::init<const DeviceInfo &>()); daqconfig.def(py::init<const DeviceInfo &>());

View File

@ -69,15 +69,16 @@ void init_dsp(py::module &m) {
/// AvPowerSpectra /// AvPowerSpectra
py::class_<AvPowerSpectra> aps(m, "AvPowerSpectra"); py::class_<AvPowerSpectra> aps(m, "AvPowerSpectra");
aps.def(py::init<const us, const Window::WindowType, const d, const int>(), aps.def(py::init<const us, const Window::WindowType, const d, const int>(),
py::arg("nfft"), py::arg("WindowType"), py::arg("overlap_percentage"), py::arg("nfft") = 2048, py::arg("windowType") =Window::WindowType::Hann, py::arg("overlap_percentage") = 50.0,
py::arg("time_constant")); py::arg("time_constant") = -1);
aps.def("compute", [](AvPowerSpectra &aps, const dmat &timedata) { aps.def("compute", [](AvPowerSpectra &aps, const dmat &timedata) {
std::optional<arma::cx_cube> res = aps.compute(timedata); std::optional<arma::cx_cube> res = aps.compute(timedata);
return res.value_or(arma::cx_cube(0,0,0)); return res.value_or(arma::cx_cube(0,0,0));
} }
); );
py::class_<SLM> slm(m, "SLM"); py::class_<SLM> slm(m, "cppSLM");
slm.def_static( slm.def_static(
"fromBiquads", "fromBiquads",
@ -94,5 +95,6 @@ void init_dsp(py::module &m) {
slm.def("Lpeak", &SLM::Lpeak); slm.def("Lpeak", &SLM::Lpeak);
slm.def("Leq", &SLM::Leq); slm.def("Leq", &SLM::Leq);
slm.def("Lmax", &SLM::Lmax); slm.def("Lmax", &SLM::Lmax);
slm.def_static("suggestedDownSamplingFac", &SLM::suggestedDownSamplingFac);
} }
/** @} */ /** @} */

View File

@ -2,6 +2,7 @@
#include <pybind11/numpy.h> #include <pybind11/numpy.h>
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
#include <pybind11/stl.h> #include <pybind11/stl.h>
#include <functional>
#include <stdint.h> #include <stdint.h>
using std::cerr; using std::cerr;
@ -31,4 +32,11 @@ void init_streammgr(py::module &m) {
smgr.def("isStreamRunningOK", &StreamMgr::isStreamRunningOK); smgr.def("isStreamRunningOK", &StreamMgr::isStreamRunningOK);
smgr.def("isStreamRunning", &StreamMgr::isStreamRunning); smgr.def("isStreamRunning", &StreamMgr::isStreamRunning);
smgr.def("getDaq", &StreamMgr::getDaq, py::return_value_policy::reference); smgr.def("getDaq", &StreamMgr::getDaq, py::return_value_policy::reference);
smgr.def("rescanDAQDevices", [](StreamMgr& smgr, bool background) {
// A pure C++ callback is the second argument to rescanDAQDevices, which
// cannot be wrapped to Pybind11. Only the one without callback is
// forwarded here to Python code.
smgr.rescanDAQDevices(background);
},
py::arg("background")=false);
} }

View File

@ -1,6 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
import numpy as np import numpy as np
from lasp import SLM from lasp import cppSLM
from lasp.filter import SPLFilterDesigner from lasp.filter import SPLFilterDesigner
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@ -11,7 +11,7 @@ def test_cppslm1():
fs = 48000 fs = 48000
omg = 2*np.pi*1000 omg = 2*np.pi*1000
slm = SLM.fromBiquads(fs, 2e-5, 1, 0.125, [1.,0,0,1,0,0]) slm = cppSLM.fromBiquads(fs, 2e-5, 1, 0.125, [1.,0,0,1,0,0])
t = np.linspace(0, 10, 10*fs, endpoint=False) t = np.linspace(0, 10, 10*fs, endpoint=False)
@ -40,7 +40,7 @@ def test_cppslm2():
omg = 2*np.pi*1000 omg = 2*np.pi*1000
filt = SPLFilterDesigner(fs).A_Sos_design() filt = SPLFilterDesigner(fs).A_Sos_design()
slm = SLM.fromBiquads(fs, 2e-5, 0, 0.125, filt.flatten(), [1.,0,0,1,0,0]) slm = cppSLM.fromBiquads(fs, 2e-5, 0, 0.125, filt.flatten(), [1.,0,0,1,0,0])
t = np.linspace(0, 10, 10*fs, endpoint=False) t = np.linspace(0, 10, 10*fs, endpoint=False)
@ -60,16 +60,33 @@ def test_cppslm2():
# (Fast, Slow etc) # (Fast, Slow etc)
assert np.isclose(out[-1,0], level, atol=1e-2) assert np.isclose(out[-1,0], level, atol=1e-2)
def test_cppslm3():
fs = 48000
omg = 2*np.pi*1000
filt = SPLFilterDesigner(fs).A_Sos_design()
slm = cppSLM.fromBiquads(fs, 2e-5, 0, 0.125, filt.flatten(), [1.,0,0,1,0,0])
t = np.linspace(0, 10, 10*fs, endpoint=False)
in_ = 10*np.sin(omg*t) * np.sqrt(2)+np.random.randn()
# Compute overall RMS
rms = np.sqrt(np.sum(in_**2)/in_.size)
# Compute overall level
level = 20*np.log10(rms/2e-5)
# Output of SLM
out = slm.run(in_)
Lpeak = 20*np.log10(np.max(np.abs(in_)/2e-5))
Lpeak
slm.Lpeak()
assert np.isclose(out[-1,0], slm.Leq()[0][0], atol=1e-2)
assert np.isclose(Lpeak, slm.Lpeak()[0][0], atol=1e0)
if __name__ == '__main__': if __name__ == '__main__':
test_cppslm1() test_cppslm1()
test_cppslm2() test_cppslm2()
test_cppslm3()
# plt.plot(t,out[:,0])
# plt.close('all')
# from scipy.signal import sosfreqz
# omg, H = sosfreqz(filt)
# freq = omg / (2*np.pi)*fs
# plt.figure()
# plt.semilogx(freq[1:],20*np.log10(np.abs(H[1:])))