acoular 25.4__py3-none-any.whl → 25.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acoular/__init__.py +2 -4
- acoular/aiaa/aiaa.py +4 -2
- acoular/base.py +14 -33
- acoular/calib.py +2 -2
- acoular/configuration.py +1 -1
- acoular/demo/__init__.py +6 -1
- acoular/demo/acoular_demo.py +34 -10
- acoular/deprecation.py +10 -1
- acoular/environments.py +5 -4
- acoular/fastFuncs.py +16 -10
- acoular/fbeamform.py +10 -107
- acoular/fprocess.py +3 -32
- acoular/grids.py +144 -37
- acoular/h5cache.py +5 -3
- acoular/h5files.py +10 -0
- acoular/internal.py +4 -0
- acoular/microphones.py +19 -2
- acoular/process.py +3 -45
- acoular/sdinput.py +12 -4
- acoular/signals.py +2 -2
- acoular/sources.py +27 -24
- acoular/spectra.py +3 -2
- acoular/tbeamform.py +3 -0
- acoular/tools/helpers.py +27 -0
- acoular/tprocess.py +1249 -468
- acoular/traitsviews.py +1 -3
- acoular/version.py +4 -3
- {acoular-25.4.dist-info → acoular-25.7.dist-info}/METADATA +3 -3
- acoular-25.7.dist-info/RECORD +56 -0
- acoular-25.4.dist-info/RECORD +0 -56
- {acoular-25.4.dist-info → acoular-25.7.dist-info}/WHEEL +0 -0
- {acoular-25.4.dist-info → acoular-25.7.dist-info}/licenses/AUTHORS.rst +0 -0
- {acoular-25.4.dist-info → acoular-25.7.dist-info}/licenses/LICENSE +0 -0
acoular/tprocess.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# ------------------------------------------------------------------------------
|
|
2
2
|
# Copyright (c) Acoular Development Team.
|
|
3
3
|
# ------------------------------------------------------------------------------
|
|
4
|
-
"""
|
|
4
|
+
"""
|
|
5
|
+
Implement blockwise processing in the time domain.
|
|
5
6
|
|
|
6
7
|
.. autosummary::
|
|
7
8
|
:toctree: generated/
|
|
@@ -27,7 +28,6 @@
|
|
|
27
28
|
WriteWAV
|
|
28
29
|
WriteH5
|
|
29
30
|
TimeConvolve
|
|
30
|
-
MaskedTimeInOut
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
# imports from other packages
|
|
@@ -38,6 +38,7 @@ from os import path
|
|
|
38
38
|
from warnings import warn
|
|
39
39
|
|
|
40
40
|
import numba as nb
|
|
41
|
+
import numpy as np
|
|
41
42
|
from numpy import (
|
|
42
43
|
append,
|
|
43
44
|
arange,
|
|
@@ -58,7 +59,6 @@ from numpy import (
|
|
|
58
59
|
float64,
|
|
59
60
|
identity,
|
|
60
61
|
inf,
|
|
61
|
-
int16,
|
|
62
62
|
interp,
|
|
63
63
|
linspace,
|
|
64
64
|
mean,
|
|
@@ -88,6 +88,7 @@ from traits.api import (
|
|
|
88
88
|
Constant,
|
|
89
89
|
Delegate,
|
|
90
90
|
Dict,
|
|
91
|
+
Either,
|
|
91
92
|
Enum,
|
|
92
93
|
File,
|
|
93
94
|
Float,
|
|
@@ -111,55 +112,70 @@ from .environments import cartToCyl, cylToCart
|
|
|
111
112
|
from .h5files import _get_h5file_class
|
|
112
113
|
from .internal import digest, ldigest
|
|
113
114
|
from .microphones import MicGeom
|
|
115
|
+
from .process import Cache
|
|
114
116
|
from .tools.utils import find_basename
|
|
115
117
|
|
|
116
118
|
|
|
117
|
-
@deprecated_alias(
|
|
119
|
+
@deprecated_alias(
|
|
120
|
+
{'numchannels_total': 'num_channels_total', 'numsamples_total': 'num_samples_total'}, removal_version='25.10'
|
|
121
|
+
)
|
|
118
122
|
class MaskedTimeOut(TimeOut):
|
|
119
|
-
"""
|
|
123
|
+
"""
|
|
124
|
+
A signal processing block that allows for the selection of specific channels and time samples.
|
|
125
|
+
|
|
126
|
+
The :class:`MaskedTimeOut` class is designed to filter data from a given
|
|
127
|
+
:class:`~acoular.sources.SamplesGenerator` (or a derived object) by defining valid time samples
|
|
128
|
+
and excluding specific channels. It acts as an intermediary between the data source and
|
|
129
|
+
subsequent processing steps, ensuring that only the selected portion of the data is passed
|
|
130
|
+
along.
|
|
120
131
|
|
|
121
|
-
This class
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
It gets samples from :attr:`~acoular.base.TimeOut.source`
|
|
125
|
-
and generates output via the generator :meth:`result`.
|
|
132
|
+
This class is useful for selecting specific portions of data for analysis. The processed data is
|
|
133
|
+
accessed through the generator method :meth:`result`, which returns data in block-wise fashion
|
|
134
|
+
for efficient streaming.
|
|
126
135
|
"""
|
|
127
136
|
|
|
128
|
-
|
|
137
|
+
#: The input data source. It must be an instance of a
|
|
138
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
139
|
+
#: This object provides the raw time-domain signals that will be filtered based on the
|
|
140
|
+
#: :attr:`start`, :attr:`stop`, and :attr:`invalid_channels` attributes.
|
|
129
141
|
source = Instance(SamplesGenerator)
|
|
130
142
|
|
|
131
|
-
|
|
143
|
+
#: The index of the first valid sample. Default is ``0``.
|
|
132
144
|
start = CInt(0, desc='start of valid samples')
|
|
133
145
|
|
|
134
|
-
|
|
146
|
+
#: The index of the last valid sample (exclusive).
|
|
147
|
+
#: If set to :obj:`None`, the selection continues until the end of the available data.
|
|
135
148
|
stop = Union(None, CInt, desc='stop of valid samples')
|
|
136
149
|
|
|
137
|
-
|
|
150
|
+
#: List of channel indices to be excluded from processing.
|
|
138
151
|
invalid_channels = List(int, desc='list of invalid channels')
|
|
139
152
|
|
|
140
|
-
|
|
153
|
+
#: A mask or index array representing valid channels. (automatically updated)
|
|
141
154
|
channels = Property(depends_on=['invalid_channels', 'source.num_channels'], desc='channel mask')
|
|
142
155
|
|
|
143
|
-
|
|
156
|
+
#: Total number of input channels, including invalid channels, as given by
|
|
157
|
+
#: :attr:`~acoular.base.TimeOut.source`. (read-only).
|
|
144
158
|
num_channels_total = Delegate('source', 'num_channels')
|
|
145
159
|
|
|
146
|
-
|
|
160
|
+
#: Total number of input channels, including invalid channels. (read-only).
|
|
147
161
|
num_samples_total = Delegate('source', 'num_samples')
|
|
148
162
|
|
|
149
|
-
|
|
163
|
+
#: Number of valid input channels after excluding :attr:`invalid_channels`. (read-only)
|
|
150
164
|
num_channels = Property(
|
|
151
165
|
depends_on=['invalid_channels', 'source.num_channels'], desc='number of valid input channels'
|
|
152
166
|
)
|
|
153
167
|
|
|
154
|
-
|
|
168
|
+
#: Number of valid time-domain samples, based on :attr:`start` and :attr:`stop` indices.
|
|
169
|
+
#: (read-only)
|
|
155
170
|
num_samples = Property(
|
|
156
171
|
depends_on=['start', 'stop', 'source.num_samples'], desc='number of valid samples per channel'
|
|
157
172
|
)
|
|
158
173
|
|
|
159
|
-
|
|
174
|
+
#: The name of the cache file (without extension). It serves as an internal reference for data
|
|
175
|
+
#: caching and tracking processed files. (automatically generated)
|
|
160
176
|
basename = Property(depends_on=['source.digest'], desc='basename for cache file')
|
|
161
177
|
|
|
162
|
-
|
|
178
|
+
#: A unique identifier for the object, based on its properties. (read-only)
|
|
163
179
|
digest = Property(depends_on=['source.digest', 'start', 'stop', 'invalid_channels'])
|
|
164
180
|
|
|
165
181
|
@cached_property
|
|
@@ -171,7 +187,7 @@ class MaskedTimeOut(TimeOut):
|
|
|
171
187
|
warn(
|
|
172
188
|
(
|
|
173
189
|
f'The basename attribute of a {self.__class__.__name__} object is deprecated'
|
|
174
|
-
' and will be removed in
|
|
190
|
+
' and will be removed in Acoular 26.01!'
|
|
175
191
|
),
|
|
176
192
|
DeprecationWarning,
|
|
177
193
|
stacklevel=2,
|
|
@@ -197,19 +213,32 @@ class MaskedTimeOut(TimeOut):
|
|
|
197
213
|
return sli[1] - sli[0]
|
|
198
214
|
|
|
199
215
|
def result(self, num):
|
|
200
|
-
"""
|
|
216
|
+
"""
|
|
217
|
+
Generate blocks of processed data, selecting only valid samples and channels.
|
|
218
|
+
|
|
219
|
+
This method fetches data from the :attr:`source` object, applies the defined :attr:`start`
|
|
220
|
+
and :attr:`stop` constraints on time samples, and filters out :attr:`invalid_channels`. The
|
|
221
|
+
data is then yielded in block-wise fashion to facilitate efficient streaming.
|
|
201
222
|
|
|
202
223
|
Parameters
|
|
203
224
|
----------
|
|
204
|
-
num :
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
The last block may
|
|
212
|
-
|
|
225
|
+
num : :obj:`int`
|
|
226
|
+
Number of samples per block.
|
|
227
|
+
|
|
228
|
+
Yields
|
|
229
|
+
------
|
|
230
|
+
:class:`numpy.ndarray`
|
|
231
|
+
An array of shape (``num``, :attr:`MaskedTimeOut.num_channels`), contatining blocks of
|
|
232
|
+
a filtered time-domain signal. The last block may contain fewer samples if the total
|
|
233
|
+
number of samples is not a multiple of ``num``. `MaskedTimeOut.num_channels` is not
|
|
234
|
+
inherited directly and may be smaller than the :attr:`source`'s number of channels.
|
|
235
|
+
|
|
236
|
+
Raises
|
|
237
|
+
------
|
|
238
|
+
:obj:`OSError`
|
|
239
|
+
If no valid samples are available within the defined :attr:`start` and :attr:`stop`
|
|
240
|
+
range. This can occur if :attr:`start` is greater than or equal to :attr:`stop` or if
|
|
241
|
+
the :attr:`source` is not containing any valid samples in the given range.
|
|
213
242
|
"""
|
|
214
243
|
sli = slice(self.start, self.stop).indices(self.num_samples_total)
|
|
215
244
|
start = sli[0]
|
|
@@ -259,20 +288,35 @@ class MaskedTimeOut(TimeOut):
|
|
|
259
288
|
|
|
260
289
|
|
|
261
290
|
class ChannelMixer(TimeOut):
|
|
262
|
-
"""Class for directly mixing the channels of a multi-channel source.
|
|
263
|
-
Outputs a single channel.
|
|
264
291
|
"""
|
|
292
|
+
A signal processing block that mixes multiple input channels into a single output channel.
|
|
265
293
|
|
|
266
|
-
|
|
294
|
+
The :class:`ChannelMixer` class takes a multi-channel signal from a
|
|
295
|
+
:class:`~acoular.sources.SamplesGenerator` (or a derived object) and applies an optional set of
|
|
296
|
+
amplitude weights to each channel. The resulting weighted sum is then output as a single-channel
|
|
297
|
+
signal.
|
|
298
|
+
|
|
299
|
+
This class is particularly useful for cases where a combined signal representation is needed,
|
|
300
|
+
such as beamforming, array signal processing, or for reducing the dimensionality of
|
|
301
|
+
multi-channel time signal data.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
#: The input data source. It must be an instance of a
|
|
305
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
306
|
+
#: It provides the multi-channel time-domain signals that will be mixed.
|
|
267
307
|
source = Instance(SamplesGenerator)
|
|
268
308
|
|
|
269
|
-
|
|
309
|
+
#: An array of amplitude weight factors applied to each input channel before summation.
|
|
310
|
+
#: If not explicitly set, all channels are weighted equally (delault is ``1``).
|
|
311
|
+
#: The shape of :attr:`weights` must match the :attr:`number of input channels<num_channels>`.
|
|
312
|
+
#: If an incompatible shape is provided, a :obj:`ValueError` will be raised.
|
|
270
313
|
weights = CArray(desc='channel weights')
|
|
271
314
|
|
|
272
|
-
|
|
315
|
+
#: The number of output channels, which is always ``1`` for this class since it produces a
|
|
316
|
+
#: single mixed output. (read-only)
|
|
273
317
|
num_channels = Constant(1)
|
|
274
318
|
|
|
275
|
-
|
|
319
|
+
#: A unique identifier for the object, based on its properties. (read-only)
|
|
276
320
|
digest = Property(depends_on=['source.digest', 'weights'])
|
|
277
321
|
|
|
278
322
|
@cached_property
|
|
@@ -280,19 +324,31 @@ class ChannelMixer(TimeOut):
|
|
|
280
324
|
return digest(self)
|
|
281
325
|
|
|
282
326
|
def result(self, num):
|
|
283
|
-
"""
|
|
327
|
+
"""
|
|
328
|
+
Generate the mixed output signal in blocks.
|
|
329
|
+
|
|
330
|
+
This method retrieves data from the :attr:`source` object, applies the specified amplitude
|
|
331
|
+
:attr:`weights` to each channel, and sums them to produce a single-channel output. The data
|
|
332
|
+
is processed and yielded in block-wise fashion for efficient memory handling.
|
|
284
333
|
|
|
285
334
|
Parameters
|
|
286
335
|
----------
|
|
287
|
-
num :
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
The last block may
|
|
295
|
-
|
|
336
|
+
num : :obj:`int`
|
|
337
|
+
Number of samples per block.
|
|
338
|
+
|
|
339
|
+
Yields
|
|
340
|
+
------
|
|
341
|
+
:class:`numpy.ndarray`
|
|
342
|
+
An array of shape ``(num, 1)`` containing blocks a of single-channel mixed signal.
|
|
343
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
344
|
+
a multiple of ``num``.
|
|
345
|
+
|
|
346
|
+
Raises
|
|
347
|
+
------
|
|
348
|
+
:obj:`ValueError`
|
|
349
|
+
If the :attr:`weights` array is provided but its shape does not match the expected shape
|
|
350
|
+
(:attr:`num_channels`,) or (``1``,), a :obj:`ValueError` is raised indicating that the
|
|
351
|
+
weights cannot be broadcasted properly.
|
|
296
352
|
"""
|
|
297
353
|
if self.weights.size:
|
|
298
354
|
if self.weights.shape in {(self.source.num_channels,), (1,)}:
|
|
@@ -308,69 +364,79 @@ class ChannelMixer(TimeOut):
|
|
|
308
364
|
|
|
309
365
|
|
|
310
366
|
class Trigger(TimeOut): # pragma: no cover
|
|
311
|
-
"""
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
The
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
367
|
+
"""
|
|
368
|
+
A signal processing class for detecting and analyzing trigger signals in time-series data.
|
|
369
|
+
|
|
370
|
+
The :class:`Trigger` class identifies trigger events in a single-channel signal provided by a
|
|
371
|
+
:class:`~acoular.base.SamplesGenerator` source. The detection process involves:
|
|
372
|
+
|
|
373
|
+
1. Identifying peaks that exceed a specified positive or negative threshold.
|
|
374
|
+
2. Estimating the approximate duration of one revolution based on the largest
|
|
375
|
+
sample distance between consecutive peaks.
|
|
376
|
+
3. Dividing the estimated revolution duration into segments called "hunks,"
|
|
377
|
+
allowing only one peak per hunk.
|
|
378
|
+
4. Selecting the most appropriate peak per hunk based on a chosen criterion
|
|
379
|
+
(e.g., first occurrence or extremum value).
|
|
380
|
+
5. Validating the consistency of the detected peaks by ensuring the revolutions
|
|
381
|
+
have a stable duration with minimal variation.
|
|
382
|
+
|
|
383
|
+
This class is typically used for rotational speed analysis, where trigger events
|
|
384
|
+
correspond to periodic markers in a signal (e.g., TDC signals in engine diagnostics).
|
|
322
385
|
"""
|
|
323
386
|
|
|
324
|
-
|
|
387
|
+
#: The input data source. It must be an instance of a
|
|
388
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
389
|
+
#: The signal must be single-channel.
|
|
325
390
|
source = Instance(SamplesGenerator)
|
|
326
391
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
392
|
+
#: The threshold value for detecting trigger peaks. The meaning of this threshold depends
|
|
393
|
+
#: on the trigger type (:attr;`trigger_type`). The sign is relevant:
|
|
394
|
+
#:
|
|
395
|
+
#: - A positive threshold detects peaks above this value.
|
|
396
|
+
#: - A negative threshold detects peaks below this value.
|
|
397
|
+
#:
|
|
398
|
+
#: If :obj:`None`, an estimated threshold is used, calculated as 75% of the extreme deviation
|
|
399
|
+
#: from the mean signal value. Default is :obj:`None`.
|
|
400
|
+
#:
|
|
401
|
+
#: E.g: If the mean value is :math:`0` and there are positive extrema at :math:`400` and
|
|
402
|
+
#: negative extrema at :math:`-800`. Then the estimated threshold would be
|
|
403
|
+
#: :math:`0.75 \cdot (-800) = -600`.
|
|
336
404
|
threshold = Union(None, Float)
|
|
337
405
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
# abs(durationEachRev - meanDuration) > 0.02 * meanDuration
|
|
406
|
+
#: The maximum allowable variation in duration between two trigger instances. If any revolution
|
|
407
|
+
#: exceeds this variation threshold, a warning is issued. Default is ``0.02``.
|
|
341
408
|
max_variation_of_duration = Float(0.02)
|
|
342
409
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
# Default is to 0.1.
|
|
410
|
+
#: Defines the length of "hunks" as a fraction of the estimated duration between two trigger
|
|
411
|
+
#: instances. If multiple peaks occur within a hunk, only one is retained based on
|
|
412
|
+
#: :attr:`multiple_peaks_in_hunk`. Default is ``0.1``.
|
|
347
413
|
hunk_length = Float(0.1)
|
|
348
414
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
#
|
|
359
|
-
# Default is 'dirac'.
|
|
415
|
+
#: Specifies the type of trigger detection:
|
|
416
|
+
#:
|
|
417
|
+
#: - ``'dirac'``: A single impulse is considered a trigger. The sign of :attr:`threshold`
|
|
418
|
+
#: determines whether positive or negative peaks are detected.
|
|
419
|
+
#: - ``'rect'``: A repeating rectangular waveform is assumed. Only every second edge is
|
|
420
|
+
#: considered a trigger. The sign of :attr:`threshold` determines whether rising (``+``) or
|
|
421
|
+
#: falling (``-``) edges are used.
|
|
422
|
+
#:
|
|
423
|
+
#: Default is ``'dirac'``.
|
|
360
424
|
trigger_type = Enum('dirac', 'rect')
|
|
361
425
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
426
|
+
#: Defines the criterion for selecting a peak when multiple occur within a hunk (see
|
|
427
|
+
#: :attr:`hunk_length`):
|
|
428
|
+
#:
|
|
429
|
+
#: - ``'extremum'``: Selects the most extreme peak.
|
|
430
|
+
#: - ``'first'``: Selects the first peak encountered.
|
|
431
|
+
#:
|
|
432
|
+
#: Default is ``'extremum'``.
|
|
365
433
|
multiple_peaks_in_hunk = Enum('extremum', 'first')
|
|
366
434
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
#
|
|
373
|
-
# 3.: -minimum of number of samples between adjacent trigger samples
|
|
435
|
+
#: A tuple containing:
|
|
436
|
+
#:
|
|
437
|
+
#: - A :class:`numpy.ndarray` of sample indices corresponding to detected trigger events.
|
|
438
|
+
#: - The maximum number of samples between consecutive trigger peaks.
|
|
439
|
+
#: - The minimum number of samples between consecutive trigger peaks.
|
|
374
440
|
trigger_data = Property(
|
|
375
441
|
depends_on=[
|
|
376
442
|
'source.digest',
|
|
@@ -382,7 +448,7 @@ class Trigger(TimeOut): # pragma: no cover
|
|
|
382
448
|
],
|
|
383
449
|
)
|
|
384
450
|
|
|
385
|
-
|
|
451
|
+
#: A unique identifier for the trigger, based on its properties. (read-only)
|
|
386
452
|
digest = Property(
|
|
387
453
|
depends_on=[
|
|
388
454
|
'source.digest',
|
|
@@ -500,23 +566,52 @@ class Trigger(TimeOut): # pragma: no cover
|
|
|
500
566
|
return 0
|
|
501
567
|
|
|
502
568
|
def result(self, num):
|
|
569
|
+
"""
|
|
570
|
+
Generate signal data from the source without modification.
|
|
571
|
+
|
|
572
|
+
This method acts as a pass-through, providing data blocks directly from the :attr:`source`
|
|
573
|
+
generator. It is included for interface consistency but does not apply trigger-based
|
|
574
|
+
transformations to the data.
|
|
575
|
+
|
|
576
|
+
Parameters
|
|
577
|
+
----------
|
|
578
|
+
num : :obj:`int`
|
|
579
|
+
Number of samples per block.
|
|
580
|
+
|
|
581
|
+
Yields
|
|
582
|
+
------
|
|
583
|
+
:class:`numpy.ndarray`
|
|
584
|
+
An array containing ``num`` samples from the source signal.
|
|
585
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
586
|
+
a multiple of ``num``.
|
|
587
|
+
|
|
588
|
+
Warnings
|
|
589
|
+
--------
|
|
590
|
+
This method is not implemented for trigger-based transformations.
|
|
591
|
+
A warning is issued, indicating that data is passed unprocessed.
|
|
592
|
+
"""
|
|
503
593
|
msg = 'result method not implemented yet! Data from source will be passed without transformation.'
|
|
504
594
|
warn(msg, Warning, stacklevel=2)
|
|
505
595
|
yield from self.source.result(num)
|
|
506
596
|
|
|
507
597
|
|
|
508
598
|
class AngleTracker(MaskedTimeOut):
|
|
509
|
-
"""
|
|
510
|
-
|
|
599
|
+
"""
|
|
600
|
+
Compute the rotational angle and RPM per sample from a trigger signal in the time domain.
|
|
511
601
|
|
|
512
|
-
|
|
513
|
-
:
|
|
602
|
+
This class retrieves samples from the specified :attr:`trigger` signal and interpolates angular
|
|
603
|
+
position and rotational speed. The results are stored in the properties :attr:`angle` and
|
|
604
|
+
:attr:`rpm`.
|
|
605
|
+
|
|
606
|
+
The algorithm assumes a periodic trigger signal marking rotational events (e.g., a tachometer
|
|
607
|
+
pulse or an encoder signal) and interpolates the angle and RPM using cubic splines. It is
|
|
608
|
+
capable of handling different rotational directions and numbers of triggers per revolution.
|
|
514
609
|
"""
|
|
515
610
|
|
|
516
|
-
|
|
611
|
+
#: Trigger data source, expected to be an instance of :class:`Trigger`.
|
|
517
612
|
trigger = Instance(Trigger)
|
|
518
613
|
|
|
519
|
-
|
|
614
|
+
#: A unique identifier for the tracker, based on its properties. (read-only)
|
|
520
615
|
digest = Property(
|
|
521
616
|
depends_on=[
|
|
522
617
|
'source.digest',
|
|
@@ -528,28 +623,35 @@ class AngleTracker(MaskedTimeOut):
|
|
|
528
623
|
],
|
|
529
624
|
)
|
|
530
625
|
|
|
531
|
-
|
|
532
|
-
|
|
626
|
+
#: Number of trigger signals per revolution. This allows tracking scenarios where multiple
|
|
627
|
+
#: trigger pulses occur per rotation. Default is ``1``, meaning a single trigger per revolution.
|
|
533
628
|
trigger_per_revo = Int(1, desc='trigger signals per revolution')
|
|
534
629
|
|
|
535
|
-
|
|
536
|
-
|
|
630
|
+
#: Rotation direction flag:
|
|
631
|
+
#:
|
|
632
|
+
#: - ``1``: counter-clockwise rotation.
|
|
633
|
+
#: - ``-1``: clockwise rotation.
|
|
634
|
+
#:
|
|
635
|
+
#: Default is ``-1``.
|
|
537
636
|
rot_direction = Int(-1, desc='mathematical direction of rotation')
|
|
538
637
|
|
|
539
|
-
|
|
540
|
-
# defaults to 4.
|
|
638
|
+
#: Number of points used for spline interpolation. Default is ``4``.
|
|
541
639
|
interp_points = Int(4, desc='Points of interpolation used for spline')
|
|
542
640
|
|
|
543
|
-
|
|
641
|
+
#: Initial rotation angle (in radians) corresponding to the first trigger event. This allows
|
|
642
|
+
#: defining a custom starting reference angle. Default is ``0``.
|
|
544
643
|
start_angle = Float(0, desc='rotation angle for trigger position')
|
|
545
644
|
|
|
546
|
-
|
|
645
|
+
#: Revolutions per minute (RPM) computed for each sample.
|
|
646
|
+
#: It is based on the trigger data. (read-only)
|
|
547
647
|
rpm = Property(depends_on=['digest'], desc='revolutions per minute for each sample')
|
|
548
648
|
|
|
549
|
-
|
|
649
|
+
#: Average revolutions per minute over the entire dataset.
|
|
650
|
+
#: It is computed based on the trigger intervals. (read-only)
|
|
550
651
|
average_rpm = Property(depends_on=['digest'], desc='average revolutions per minute')
|
|
551
652
|
|
|
552
|
-
|
|
653
|
+
#: Computed rotation angle (in radians) for each sample.
|
|
654
|
+
#: It is interpolated from the trigger data. (read-only)
|
|
553
655
|
angle = Property(depends_on=['digest'], desc='rotation angle for each sample')
|
|
554
656
|
|
|
555
657
|
# Internal flag to determine whether rpm and angle calculation has been processed,
|
|
@@ -572,12 +674,12 @@ class AngleTracker(MaskedTimeOut):
|
|
|
572
674
|
return (abs(peakarray - value)).argmin()
|
|
573
675
|
|
|
574
676
|
def _to_rpm_and_angle(self):
|
|
575
|
-
|
|
576
|
-
Calculates angles in radians for one or more instants in time.
|
|
677
|
+
# Internal helper function.
|
|
678
|
+
# Calculates angles in radians for one or more instants in time.
|
|
679
|
+
|
|
680
|
+
# Current version supports only trigger and sources with the same samplefreq.
|
|
681
|
+
# This behaviour may change in future releases.
|
|
577
682
|
|
|
578
|
-
Current version supports only trigger and sources with the same samplefreq.
|
|
579
|
-
This behaviour may change in future releases
|
|
580
|
-
"""
|
|
581
683
|
# init
|
|
582
684
|
ind = 0
|
|
583
685
|
# trigger data
|
|
@@ -646,14 +748,6 @@ class AngleTracker(MaskedTimeOut):
|
|
|
646
748
|
# calc average rpm from trigger data
|
|
647
749
|
@cached_property
|
|
648
750
|
def _get_average_rpm(self):
|
|
649
|
-
"""Returns average revolutions per minute (rpm) over the source samples.
|
|
650
|
-
|
|
651
|
-
Returns
|
|
652
|
-
-------
|
|
653
|
-
rpm : float
|
|
654
|
-
rpm in 1/min.
|
|
655
|
-
|
|
656
|
-
"""
|
|
657
751
|
# trigger indices data
|
|
658
752
|
peakloc = self.trigger.trigger_data[0]
|
|
659
753
|
# calculation of average rpm in 1/min
|
|
@@ -661,18 +755,33 @@ class AngleTracker(MaskedTimeOut):
|
|
|
661
755
|
|
|
662
756
|
|
|
663
757
|
class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
664
|
-
"""
|
|
665
|
-
|
|
666
|
-
|
|
758
|
+
"""
|
|
759
|
+
Base class for spatial interpolation of microphone data.
|
|
760
|
+
|
|
761
|
+
This class retrieves samples from a specified source and performs spatial interpolation to
|
|
762
|
+
generate output at virtual microphone positions. The interpolation is executed using various
|
|
763
|
+
methods such as linear, spline, radial basis function (RBF), and inverse distance weighting
|
|
764
|
+
(IDW).
|
|
765
|
+
|
|
766
|
+
See Also
|
|
767
|
+
--------
|
|
768
|
+
:class:`SpatialInterpolatorRotation` : Spatial interpolation class for rotating sound sources.
|
|
769
|
+
:class:`SpatialInterpolatorConstantRotation` :
|
|
770
|
+
Performs spatial linear interpolation for sources undergoing constant rotation.
|
|
667
771
|
"""
|
|
668
772
|
|
|
669
|
-
|
|
773
|
+
#: The input data source. It must be an instance of a
|
|
774
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
775
|
+
#: It provides the time-domain pressure samples from microphones.
|
|
670
776
|
source = Instance(SamplesGenerator)
|
|
671
777
|
|
|
672
|
-
|
|
778
|
+
#: The physical microphone geometry. An instance of :class:`~acoular.microphones.MicGeom` that
|
|
779
|
+
#: defines the positions of the real microphones used for measurement.
|
|
673
780
|
mics = Instance(MicGeom(), desc='microphone geometry')
|
|
674
781
|
|
|
675
|
-
|
|
782
|
+
#: The virtual microphone geometry. This property defines the positions
|
|
783
|
+
#: of virtual microphones where interpolated pressure values are computed.
|
|
784
|
+
#: Default is the physical microphone geometry (:attr:`mics`).
|
|
676
785
|
mics_virtual = Property(desc='microphone geometry')
|
|
677
786
|
|
|
678
787
|
_mics_virtual = Instance(MicGeom, desc='internal microphone geometry;internal usage, read only')
|
|
@@ -685,11 +794,18 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
685
794
|
def _set_mics_virtual(self, mics_virtual):
|
|
686
795
|
self._mics_virtual = mics_virtual
|
|
687
796
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
797
|
+
#: Interpolation method used for spatial data estimation.
|
|
798
|
+
#:
|
|
799
|
+
#: Options:
|
|
800
|
+
#:
|
|
801
|
+
#: - ``'linear'``: Uses NumPy linear interpolation.
|
|
802
|
+
#: - ``'spline'``: Uses SciPy's CubicSpline interpolator
|
|
803
|
+
#: - ``'rbf-multiquadric'``: Radial basis function (RBF) interpolation with a multiquadric
|
|
804
|
+
#: kernel.
|
|
805
|
+
#: - ``'rbf-cubic'``: RBF interpolation with a cubic kernel.
|
|
806
|
+
#: - ``'IDW'``: Inverse distance weighting interpolation.
|
|
807
|
+
#: - ``'custom'``: Allows user-defined interpolation methods.
|
|
808
|
+
#: - ``'sinc'``: Uses sinc-based interpolation for signal reconstruction.
|
|
693
809
|
method = Enum(
|
|
694
810
|
'linear',
|
|
695
811
|
'spline',
|
|
@@ -701,30 +817,54 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
701
817
|
desc='method for interpolation used',
|
|
702
818
|
)
|
|
703
819
|
|
|
704
|
-
|
|
820
|
+
#: Defines the spatial dimensionality of the microphone array.
|
|
821
|
+
#:
|
|
822
|
+
#: Possible values:
|
|
823
|
+
#:
|
|
824
|
+
#: - ``'1D'``: Linear microphone arrays.
|
|
825
|
+
#: - ``'2D'``: Planar microphone arrays.
|
|
826
|
+
#: - ``'ring'``: Circular arrays where rotation needs to be considered.
|
|
827
|
+
#: - ``'3D'``: Three-dimensional microphone distributions.
|
|
828
|
+
#: - ``'custom'``: User-defined microphone arrangements.
|
|
705
829
|
array_dimension = Enum('1D', '2D', 'ring', '3D', 'custom', desc='spatial dimensionality of the array geometry')
|
|
706
830
|
|
|
707
|
-
|
|
831
|
+
#: Sampling frequency of the output signal, inherited from the :attr:`source`. This defines the
|
|
832
|
+
#: rate at which microphone pressure samples are acquired and processed.
|
|
708
833
|
sample_freq = Delegate('source', 'sample_freq')
|
|
709
834
|
|
|
710
|
-
|
|
835
|
+
#: Number of channels in the output data. This corresponds to the number of virtual microphone
|
|
836
|
+
#: positions where interpolated pressure values are computed. The value is ´determined based on
|
|
837
|
+
#: the :attr:`mics_virtual` geometry.
|
|
711
838
|
num_channels = Property()
|
|
712
839
|
|
|
713
|
-
|
|
840
|
+
#: Number of time-domain samples in the output signal, inherited from the :attr:`source`.
|
|
714
841
|
num_samples = Delegate('source', 'num_samples')
|
|
715
842
|
|
|
716
|
-
|
|
843
|
+
#: Whether to interpolate a virtual microphone at the origin. If set to ``True``, an additional
|
|
844
|
+
#: virtual microphone position at the coordinate origin :math:`(0,0,0)` will be interpolated.
|
|
717
845
|
interp_at_zero = Bool(False)
|
|
718
846
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
847
|
+
#: Transformation matrix for coordinate system alignment.
|
|
848
|
+
#:
|
|
849
|
+
#: This 3x3 orthogonal matrix is used to align the microphone coordinates such that rotations
|
|
850
|
+
#: occur around the z-axis. If the original coordinates do not conform to the expected alignment
|
|
851
|
+
#: (where the x-axis transitions into the y-axis upon rotation), applying this matrix modifies
|
|
852
|
+
#: the coordinates accordingly. The transformation is defined as
|
|
853
|
+
#:
|
|
854
|
+
#: .. math::
|
|
855
|
+
#: \begin{bmatrix}x'\\y'\\z'\end{bmatrix} = Q \cdot \begin{bmatrix}x\\y\\z\end{bmatrix}
|
|
856
|
+
#:
|
|
857
|
+
#: where :math:`Q` is the transformation matrix and :math:`(x', y', z')` are the modified
|
|
858
|
+
#: coordinates. If no transformation is needed, :math:`Q` defaults to the identity matrix.
|
|
724
859
|
Q = CArray(dtype=float64, shape=(3, 3), value=identity(3))
|
|
725
860
|
|
|
861
|
+
#: Number of neighboring microphones used in IDW interpolation. This parameter determines how
|
|
862
|
+
#: many physical microphones contribute to the weighted sum in inverse distance weighting (IDW)
|
|
863
|
+
#: interpolation.
|
|
726
864
|
num_IDW = Int(3, desc='number of neighboring microphones, DEFAULT=3') # noqa: N815
|
|
727
865
|
|
|
866
|
+
#: Weighting exponent for IDW interpolation. This parameter controls the influence of distance
|
|
867
|
+
#: in inverse distance weighting (IDW). A higher value gives more weight to closer microphones.
|
|
728
868
|
p_weight = Float(
|
|
729
869
|
2,
|
|
730
870
|
desc='used in interpolation for virtual microphone, weighting power exponent for IDW',
|
|
@@ -735,7 +875,7 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
735
875
|
depends_on=['mics.digest', 'mics_virtual.digest', 'method', 'array_dimension', 'interp_at_zero'],
|
|
736
876
|
)
|
|
737
877
|
|
|
738
|
-
|
|
878
|
+
#: Unique identifier for the current configuration of the interpolator. (read-only)
|
|
739
879
|
digest = Property(
|
|
740
880
|
depends_on=[
|
|
741
881
|
'mics.digest',
|
|
@@ -760,51 +900,70 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
760
900
|
return self._virtNewCoord_func(self.mics.mpos, self.mics_virtual.mpos, self.method, self.array_dimension)
|
|
761
901
|
|
|
762
902
|
def sinc_mic(self, r):
|
|
763
|
-
"""
|
|
764
|
-
|
|
903
|
+
"""
|
|
904
|
+
Compute a modified sinc function for use in Radial Basis Function (RBF) approximation.
|
|
765
905
|
|
|
766
|
-
|
|
767
|
-
|
|
906
|
+
This function is used as a kernel in sinc-based interpolation methods, where the sinc
|
|
907
|
+
function serves as a basis function for reconstructing signals based on spatially
|
|
908
|
+
distributed microphone data. The function is scaled according to the number of virtual
|
|
909
|
+
microphone positions, ensuring accurate signal approximation.
|
|
768
910
|
|
|
769
911
|
Parameters
|
|
770
912
|
----------
|
|
771
|
-
|
|
772
|
-
The
|
|
773
|
-
|
|
774
|
-
The mic positions of the virtual mics
|
|
775
|
-
method : string
|
|
776
|
-
The Interpolation method to use
|
|
777
|
-
array_dimension : string
|
|
778
|
-
The Array Dimensions in cylinder coordinates
|
|
913
|
+
r : :obj:`float` or :obj:`list` of :obj:`floats<float>`
|
|
914
|
+
The radial distance(s) at which to evaluate the sinc function, typically representing
|
|
915
|
+
the spatial separation between real and virtual microphone positions.
|
|
779
916
|
|
|
780
917
|
Returns
|
|
781
918
|
-------
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
If the Array is 1D the list items are:
|
|
785
|
-
1. item : float64[nMicsInSpecificSubarray]
|
|
786
|
-
Ordered positions of the real mics on the new 1d axis,
|
|
787
|
-
to be used as inputs for numpys interp.
|
|
788
|
-
2. item : int64[nMicsInArray]
|
|
789
|
-
Indices identifying how the measured pressures must be evaluated, s.t. the
|
|
790
|
-
entries of the previous item (see last line) correspond to their initial
|
|
791
|
-
pressure values.
|
|
792
|
-
If the Array is 2D or 3d the list items are:
|
|
793
|
-
1. item : Delaunay mesh object
|
|
794
|
-
Delaunay mesh (see scipy.spatial.Delaunay) for the specific Array
|
|
795
|
-
2. item : int64[nMicsInArray]
|
|
796
|
-
same as 1d case, BUT with the difference, that here the rotational periodicity
|
|
797
|
-
is handled, when constructing the mesh. Therefore, the mesh could have more
|
|
798
|
-
vertices than the actual Array mics.
|
|
799
|
-
|
|
800
|
-
virtNewCoord : float64[3, nVirtualMics]
|
|
801
|
-
Projection of each virtual mic onto its new coordinates. The columns of virtNewCoord
|
|
802
|
-
correspond to [phi, rho, z].
|
|
803
|
-
|
|
804
|
-
newCoord : float64[3, nMics]
|
|
805
|
-
Projection of each mic onto its new coordinates. The columns of newCoordinates
|
|
806
|
-
correspond to [phi, rho, z].
|
|
919
|
+
:class:`numpy.ndarray`
|
|
920
|
+
Evaluated sinc function values at the given radial distances.
|
|
807
921
|
"""
|
|
922
|
+
return sinc((r * self.mics_virtual.mpos.shape[1]) / (pi))
|
|
923
|
+
|
|
924
|
+
def _virtNewCoord_func(self, mpos, mpos_virt, method, array_dimension): # noqa N802
|
|
925
|
+
# Core functionality for getting the interpolation.
|
|
926
|
+
#
|
|
927
|
+
# Parameters
|
|
928
|
+
# ----------
|
|
929
|
+
# mpos : float[3, nPhysicalMics]
|
|
930
|
+
# The mic positions of the physical (really existing) mics
|
|
931
|
+
# mpos_virt : float[3, nVirtualMics]
|
|
932
|
+
# The mic positions of the virtual mics
|
|
933
|
+
# method : string
|
|
934
|
+
# The Interpolation method to use
|
|
935
|
+
# array_dimension : string
|
|
936
|
+
# The Array Dimensions in cylinder coordinates
|
|
937
|
+
#
|
|
938
|
+
# Returns
|
|
939
|
+
# -------
|
|
940
|
+
# mesh : List[]
|
|
941
|
+
# The items of these lists depend on the reduced interpolation dimension of each
|
|
942
|
+
# subarray.
|
|
943
|
+
# If the Array is 1D the list items are:
|
|
944
|
+
# 1. item : float64[nMicsInSpecificSubarray]
|
|
945
|
+
# Ordered positions of the real mics on the new 1d axis,
|
|
946
|
+
# to be used as inputs for numpys interp.
|
|
947
|
+
# 2. item : int64[nMicsInArray]
|
|
948
|
+
# Indices identifying how the measured pressures must be evaluated, s.t. the
|
|
949
|
+
# entries of the previous item (see last line) correspond to their initial
|
|
950
|
+
# pressure values.
|
|
951
|
+
# If the Array is 2D or 3d the list items are:
|
|
952
|
+
# 1. item : Delaunay mesh object
|
|
953
|
+
# Delaunay mesh (see scipy.spatial.Delaunay) for the specific Array
|
|
954
|
+
# 2. item : int64[nMicsInArray]
|
|
955
|
+
# same as 1d case, BUT with the difference, that here the rotational periodicity
|
|
956
|
+
# is handled, when constructing the mesh. Therefore, the mesh could have more
|
|
957
|
+
# vertices than the actual Array mics.
|
|
958
|
+
#
|
|
959
|
+
# virtNewCoord : float64[3, nVirtualMics]
|
|
960
|
+
# Projection of each virtual mic onto its new coordinates. The columns of virtNewCoord
|
|
961
|
+
# correspond to [phi, rho, z].
|
|
962
|
+
#
|
|
963
|
+
# newCoord : float64[3, nMics]
|
|
964
|
+
# Projection of each mic onto its new coordinates. The columns of newCoordinates
|
|
965
|
+
# correspond to [phi, rho, z].
|
|
966
|
+
|
|
808
967
|
# init positions of virtual mics in cyl coordinates
|
|
809
968
|
nVirtMics = mpos_virt.shape[1]
|
|
810
969
|
virtNewCoord = zeros((3, nVirtMics))
|
|
@@ -909,25 +1068,23 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
909
1068
|
return mesh, virtNewCoord, newCoord
|
|
910
1069
|
|
|
911
1070
|
def _result_core_func(self, p, phi_delay=None, period=None, Q=Q, interp_at_zero=False): # noqa: N803, ARG002 (see #226)
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
Parameters
|
|
915
|
-
----------
|
|
916
|
-
p : float[num, nMicsReal]
|
|
917
|
-
|
|
918
|
-
phi_delay : empty list (default) or float[num]
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
period : None (default) or float
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
Returns
|
|
926
|
-
-------
|
|
927
|
-
pInterp : float[num, nMicsVirtual]
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
"""
|
|
1071
|
+
# Performs the actual Interpolation.
|
|
1072
|
+
#
|
|
1073
|
+
# Parameters
|
|
1074
|
+
# ----------
|
|
1075
|
+
# p : float[num, nMicsReal]
|
|
1076
|
+
# The pressure field of the yielded sample at real mics.
|
|
1077
|
+
# phi_delay : empty list (default) or float[num]
|
|
1078
|
+
# If passed (rotational case), this list contains the angular delay
|
|
1079
|
+
# of each sample in rad.
|
|
1080
|
+
# period : None (default) or float
|
|
1081
|
+
# If periodicity can be assumed (rotational case)
|
|
1082
|
+
# this parameter contains the periodicity length
|
|
1083
|
+
#
|
|
1084
|
+
# Returns
|
|
1085
|
+
# -------
|
|
1086
|
+
# pInterp : float[num, nMicsVirtual]
|
|
1087
|
+
# The interpolated time data at the virtual mics
|
|
931
1088
|
if phi_delay is None:
|
|
932
1089
|
phi_delay = []
|
|
933
1090
|
# number of time samples
|
|
@@ -1150,21 +1307,50 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
1150
1307
|
return pInterp
|
|
1151
1308
|
|
|
1152
1309
|
def result(self, num):
|
|
1310
|
+
"""
|
|
1311
|
+
Generate interpolated microphone data over time.
|
|
1312
|
+
|
|
1313
|
+
This method retrieves pressure samples from the physical microphones and applies spatial
|
|
1314
|
+
interpolation to estimate the pressure at virtual microphone locations.
|
|
1315
|
+
The interpolation method is determined by :attr:`method`.
|
|
1316
|
+
|
|
1317
|
+
Parameters
|
|
1318
|
+
----------
|
|
1319
|
+
num : :obj:`int`
|
|
1320
|
+
Number of samples per block.
|
|
1321
|
+
|
|
1322
|
+
Yields
|
|
1323
|
+
------
|
|
1324
|
+
:class:`numpy.ndarray`
|
|
1325
|
+
An array of shape (``num``, `n`), where `n` is the number of virtual microphones,
|
|
1326
|
+
containing interpolated pressure values for the virtual microphones at each time step.
|
|
1327
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
1328
|
+
a multiple of ``num``.
|
|
1329
|
+
"""
|
|
1153
1330
|
msg = 'result method not implemented yet! Data from source will be passed without transformation.'
|
|
1154
1331
|
warn(msg, Warning, stacklevel=2)
|
|
1155
1332
|
yield from self.source.result(num)
|
|
1156
1333
|
|
|
1157
1334
|
|
|
1158
1335
|
class SpatialInterpolatorRotation(SpatialInterpolator): # pragma: no cover
|
|
1159
|
-
"""
|
|
1160
|
-
|
|
1336
|
+
"""
|
|
1337
|
+
Spatial interpolation class for rotating sound sources.
|
|
1338
|
+
|
|
1339
|
+
This class extends :attr:`SpatialInterpolator` to handle sources that undergo rotational
|
|
1340
|
+
movement. It retrieves samples from the :attr:`source` attribute and angle data from the
|
|
1341
|
+
:attr:`AngleTracker` instance (:attr:`angle_source`). Using these inputs, it computes
|
|
1342
|
+
interpolated outputs through the :meth:`result` generator method.
|
|
1161
1343
|
|
|
1344
|
+
See Also
|
|
1345
|
+
--------
|
|
1346
|
+
:class:`SpatialInterpolator`: Base class for spatial interpolation of microphone data.
|
|
1162
1347
|
"""
|
|
1163
1348
|
|
|
1164
|
-
|
|
1349
|
+
#: Provides real-time tracking of the source's rotation angles,
|
|
1350
|
+
#: instance of :attr:`AngleTracker`.
|
|
1165
1351
|
angle_source = Instance(AngleTracker)
|
|
1166
1352
|
|
|
1167
|
-
|
|
1353
|
+
#: Unique identifier for the current configuration of the interpolator. (read-only)
|
|
1168
1354
|
digest = Property(
|
|
1169
1355
|
depends_on=[
|
|
1170
1356
|
'source.digest',
|
|
@@ -1183,19 +1369,26 @@ class SpatialInterpolatorRotation(SpatialInterpolator): # pragma: no cover
|
|
|
1183
1369
|
return digest(self)
|
|
1184
1370
|
|
|
1185
1371
|
def result(self, num=128):
|
|
1186
|
-
"""
|
|
1372
|
+
"""
|
|
1373
|
+
Generate interpolated output samples in block-wise fashion.
|
|
1374
|
+
|
|
1375
|
+
This method acts as a generator, yielding time-domain time signal samples that have been
|
|
1376
|
+
spatially interpolated based on rotational movement.
|
|
1187
1377
|
|
|
1188
1378
|
Parameters
|
|
1189
1379
|
----------
|
|
1190
|
-
num :
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1380
|
+
num : :obj:`int`, optional
|
|
1381
|
+
Number of samples per block. Default is ``128``.
|
|
1382
|
+
|
|
1383
|
+
Yields
|
|
1384
|
+
------
|
|
1385
|
+
:class:`numpy.ndarray`
|
|
1386
|
+
Interpolated time signal samples in blocks of shape
|
|
1387
|
+
(``num``, :attr:`~SpatialInterpolator.num_channels`), where
|
|
1388
|
+
:attr:`~SpatialInterpolator.num_channels` is inherited from the
|
|
1389
|
+
:class:`SpatialInterpolator` base class.
|
|
1390
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
1391
|
+
a multiple of ``num``.
|
|
1199
1392
|
"""
|
|
1200
1393
|
# period for rotation
|
|
1201
1394
|
period = 2 * pi
|
|
@@ -1211,16 +1404,25 @@ class SpatialInterpolatorRotation(SpatialInterpolator): # pragma: no cover
|
|
|
1211
1404
|
|
|
1212
1405
|
|
|
1213
1406
|
class SpatialInterpolatorConstantRotation(SpatialInterpolator): # pragma: no cover
|
|
1214
|
-
"""Spatial linear Interpolation for constantly rotating sources.
|
|
1215
|
-
Gets samples from :attr:`source` and generates output via the
|
|
1216
|
-
generator :meth:`result`.
|
|
1217
1407
|
"""
|
|
1408
|
+
Performs spatial linear interpolation for sources undergoing constant rotation.
|
|
1218
1409
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1410
|
+
This class interpolates signals from a rotating sound source based on a constant rotational
|
|
1411
|
+
speed. It retrieves samples from the :attr:`source` and applies interpolation before
|
|
1412
|
+
generating output through the :meth:`result` generator.
|
|
1413
|
+
|
|
1414
|
+
See Also
|
|
1415
|
+
--------
|
|
1416
|
+
:class:`SpatialInterpolator` : Base class for spatial interpolation of microphone data.
|
|
1417
|
+
:class:`SpatialInterpolatorRotation` : Spatial interpolation class for rotating sound sources.
|
|
1418
|
+
"""
|
|
1419
|
+
|
|
1420
|
+
#: Rotational speed of the source in revolutions per second (rps). A positive value indicates
|
|
1421
|
+
#: counterclockwise rotation around the positive z-axis, meaning motion from the x-axis toward
|
|
1422
|
+
#: the y-axis.
|
|
1221
1423
|
rotational_speed = Float(0.0)
|
|
1222
1424
|
|
|
1223
|
-
|
|
1425
|
+
#: Unique identifier for the current configuration of the interpolator. (read-only)
|
|
1224
1426
|
digest = Property(
|
|
1225
1427
|
depends_on=[
|
|
1226
1428
|
'source.digest',
|
|
@@ -1239,19 +1441,28 @@ class SpatialInterpolatorConstantRotation(SpatialInterpolator): # pragma: no co
|
|
|
1239
1441
|
return digest(self)
|
|
1240
1442
|
|
|
1241
1443
|
def result(self, num=1):
|
|
1242
|
-
"""
|
|
1444
|
+
"""
|
|
1445
|
+
Generate interpolated time signal data in blocks of size ``num``.
|
|
1446
|
+
|
|
1447
|
+
This generator method continuously processes incoming time signal data while applying
|
|
1448
|
+
rotational interpolation. The phase delay is computed based on the rotational speed and
|
|
1449
|
+
applied to the signal.
|
|
1243
1450
|
|
|
1244
1451
|
Parameters
|
|
1245
1452
|
----------
|
|
1246
|
-
num :
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1453
|
+
num : :obj:`int`, optional
|
|
1454
|
+
Number of samples per block.
|
|
1455
|
+
Default is ``1``.
|
|
1456
|
+
|
|
1457
|
+
Yields
|
|
1458
|
+
------
|
|
1459
|
+
:class:`numpy.ndarray`
|
|
1460
|
+
An array containing the interpolated time signal samples in blocks of shape
|
|
1461
|
+
(``num``, :attr:`~SpatialInterpolator.num_channels`), where
|
|
1462
|
+
:attr:`~SpatialInterpolator.num_channels` is inherited from the
|
|
1463
|
+
:class:`SpatialInterpolator` base class.
|
|
1464
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
1465
|
+
a multiple of ``num``.
|
|
1255
1466
|
"""
|
|
1256
1467
|
omega = 2 * pi * self.rotational_speed
|
|
1257
1468
|
period = 2 * pi
|
|
@@ -1265,32 +1476,44 @@ class SpatialInterpolatorConstantRotation(SpatialInterpolator): # pragma: no co
|
|
|
1265
1476
|
|
|
1266
1477
|
|
|
1267
1478
|
class Mixer(TimeOut):
|
|
1268
|
-
"""
|
|
1479
|
+
"""
|
|
1480
|
+
Mix signals from multiple sources into a single output.
|
|
1481
|
+
|
|
1482
|
+
This class takes a :attr:`primary time signal source<source>` and a list of
|
|
1483
|
+
:attr:`additional sources<sources>` with the same sampling rates and channel counts across all
|
|
1484
|
+
:attr:`primary time signal source<source>`, and outputs a mixed signal.
|
|
1485
|
+
The mixing process is performed block-wise using a generator.
|
|
1486
|
+
|
|
1487
|
+
If one of the :attr:`additional sources<sources>` holds a shorter signal than the other
|
|
1488
|
+
sources the :meth:`result` method will stop yielding mixed time signal at that point.
|
|
1489
|
+
"""
|
|
1269
1490
|
|
|
1270
|
-
|
|
1491
|
+
#: The primary time signal source. It must be an instance of a
|
|
1492
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
1271
1493
|
source = Instance(SamplesGenerator)
|
|
1272
1494
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1495
|
+
#: A list of additional time signal sources to be mixed with the primary source, each must be an
|
|
1496
|
+
#: instance of :class:`~acoular.base.SamplesGenerator`.
|
|
1275
1497
|
sources = List(Instance(SamplesGenerator, ()))
|
|
1276
1498
|
|
|
1277
|
-
|
|
1499
|
+
#: The sampling frequency of the primary time signal, delegated from :attr:`source`.
|
|
1278
1500
|
sample_freq = Delegate('source')
|
|
1279
1501
|
|
|
1280
|
-
|
|
1502
|
+
#: The number of channels in the output, delegated from :attr:`source`.
|
|
1281
1503
|
num_channels = Delegate('source')
|
|
1282
1504
|
|
|
1283
|
-
|
|
1505
|
+
#: The number of samples in the output, delegated from :attr:`source`.
|
|
1284
1506
|
num_samples = Delegate('source')
|
|
1285
1507
|
|
|
1286
|
-
|
|
1508
|
+
#: Internal identifier that tracks changes in the :attr:`sources` list.
|
|
1287
1509
|
sdigest = Str()
|
|
1288
1510
|
|
|
1289
1511
|
@observe('sources.items.digest')
|
|
1290
1512
|
def _set_sourcesdigest(self, event): # noqa ARG002
|
|
1291
1513
|
self.sdigest = ldigest(self.sources)
|
|
1292
1514
|
|
|
1293
|
-
|
|
1515
|
+
#: A unique identifier for the Mixer instance, based on the :attr:`primary source<source>` and
|
|
1516
|
+
#: the :attr:`list of additional sources<sources>`.
|
|
1294
1517
|
digest = Property(depends_on=['source.digest', 'sdigest'])
|
|
1295
1518
|
|
|
1296
1519
|
@cached_property
|
|
@@ -1298,7 +1521,16 @@ class Mixer(TimeOut):
|
|
|
1298
1521
|
return digest(self)
|
|
1299
1522
|
|
|
1300
1523
|
def validate_sources(self):
|
|
1301
|
-
|
|
1524
|
+
# Validate whether the additional sources are compatible with the primary source.
|
|
1525
|
+
#
|
|
1526
|
+
# This method checks if all sources have the same sampling frequency and the same number of
|
|
1527
|
+
# channels. If a mismatch is detected, a :obj:`ValueError` is raised.
|
|
1528
|
+
#
|
|
1529
|
+
# Raises
|
|
1530
|
+
# ------
|
|
1531
|
+
# :obj:`ValueError`
|
|
1532
|
+
# If any source in :attr:`sources` has a different sampling frequency or
|
|
1533
|
+
# number of channels than :attr:`source`.
|
|
1302
1534
|
if self.source:
|
|
1303
1535
|
for s in self.sources:
|
|
1304
1536
|
if self.sample_freq != s.sample_freq:
|
|
@@ -1309,21 +1541,34 @@ class Mixer(TimeOut):
|
|
|
1309
1541
|
raise ValueError(msg)
|
|
1310
1542
|
|
|
1311
1543
|
def result(self, num):
|
|
1312
|
-
"""
|
|
1313
|
-
|
|
1314
|
-
sources are being added.
|
|
1544
|
+
"""
|
|
1545
|
+
Generate mixed time signal data in blocks of ``num`` samples.
|
|
1315
1546
|
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
This parameter defines the size of the blocks to be yielded
|
|
1320
|
-
(i.e. the number of samples per block).
|
|
1547
|
+
This generator method retrieves time signal data from all sources and sums them together
|
|
1548
|
+
to produce a combined output. The data from each source is processed in blocks of the
|
|
1549
|
+
same size, ensuring synchronized mixing.
|
|
1321
1550
|
|
|
1322
|
-
|
|
1323
|
-
-------
|
|
1324
|
-
Samples in blocks of shape (num, num_channels).
|
|
1325
|
-
The last block may be shorter than num.
|
|
1551
|
+
.. note::
|
|
1326
1552
|
|
|
1553
|
+
Yielding stops when one of the additionally provied signals ends; i.e. if one of the
|
|
1554
|
+
additional sources holds a signal of shorter length than that of the
|
|
1555
|
+
:attr:`primary source<source>` that (shorter) signal forms the lower bound of the length
|
|
1556
|
+
of the mixed time signal yielded.
|
|
1557
|
+
|
|
1558
|
+
Parameters
|
|
1559
|
+
----------
|
|
1560
|
+
num : :obj:`int`
|
|
1561
|
+
Number of samples per block.
|
|
1562
|
+
|
|
1563
|
+
Yields
|
|
1564
|
+
------
|
|
1565
|
+
:class:`numpy.ndarray`
|
|
1566
|
+
An array containing the mixed time samples in blocks of shape
|
|
1567
|
+
(``num``, :attr:`~acoular.base.TimeOut.num_channels`), where
|
|
1568
|
+
:attr:`~acoular.base.TimeOut.num_channels` is inhereted from the
|
|
1569
|
+
:class:`~acoular.base.TimeOut` base class.
|
|
1570
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
1571
|
+
a multiple of ``num``.
|
|
1327
1572
|
"""
|
|
1328
1573
|
# check whether all sources fit together
|
|
1329
1574
|
self.validate_sources()
|
|
@@ -1345,52 +1590,96 @@ class Mixer(TimeOut):
|
|
|
1345
1590
|
|
|
1346
1591
|
|
|
1347
1592
|
class TimePower(TimeOut):
|
|
1348
|
-
"""
|
|
1593
|
+
"""
|
|
1594
|
+
Calculate the time-dependent power of a signal by squaring its samples.
|
|
1595
|
+
|
|
1596
|
+
This class computes the power of the input signal by squaring the value of each sample. It
|
|
1597
|
+
processes the signal in blocks, making it suitable for large datasets or real-time signal
|
|
1598
|
+
processing. The power is calculated on a per-block basis, and each block of the output is
|
|
1599
|
+
yielded as a NumPy array.
|
|
1600
|
+
|
|
1601
|
+
Attributes
|
|
1602
|
+
----------
|
|
1603
|
+
source : SamplesGenerator
|
|
1604
|
+
The input data source, which provides the time signal or signal samples
|
|
1605
|
+
to be processed. It must be an instance of :class:`~acoular.base.SamplesGenerator`
|
|
1606
|
+
or any derived class that provides a `result()` method.
|
|
1607
|
+
"""
|
|
1349
1608
|
|
|
1350
|
-
|
|
1609
|
+
#: The input data source. It must be an instance of a
|
|
1610
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
1351
1611
|
source = Instance(SamplesGenerator)
|
|
1352
1612
|
|
|
1353
1613
|
def result(self, num):
|
|
1354
|
-
"""
|
|
1614
|
+
"""
|
|
1615
|
+
Generate the time-dependent power of the input signal in blocks.
|
|
1616
|
+
|
|
1617
|
+
This method iterates through the signal samples provided by the :attr:`source` and
|
|
1618
|
+
calculates the power by squaring each sample. The output is yielded block-wise to
|
|
1619
|
+
facilitate processing large signals in chunks.
|
|
1355
1620
|
|
|
1356
1621
|
Parameters
|
|
1357
1622
|
----------
|
|
1358
|
-
num :
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1623
|
+
num : :obj:`int`
|
|
1624
|
+
Number of samples per block.
|
|
1625
|
+
|
|
1626
|
+
Yields
|
|
1627
|
+
------
|
|
1628
|
+
:class:`numpy.ndarray`
|
|
1629
|
+
An array containing the squared samples from the :attr:`source`. Each block will have
|
|
1630
|
+
the shape (``num``, :attr:`~acoular.base.TimeOut.num_channels`), where
|
|
1631
|
+
:attr:`~acoular.base.TimeOut.num_channels` is inhereted from the
|
|
1632
|
+
:class:`~acoular.base.TimeOut` base class.
|
|
1633
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
1634
|
+
a multiple of ``num``.
|
|
1368
1635
|
"""
|
|
1369
1636
|
for temp in self.source.result(num):
|
|
1370
1637
|
yield temp * temp
|
|
1371
1638
|
|
|
1372
1639
|
|
|
1373
1640
|
class TimeCumAverage(TimeOut):
|
|
1374
|
-
"""
|
|
1641
|
+
"""
|
|
1642
|
+
Calculates the cumulative average of the signal.
|
|
1375
1643
|
|
|
1376
|
-
|
|
1644
|
+
This class computes the cumulative average of the input signal over time, which is useful for
|
|
1645
|
+
metrics like the Equivalent Continuous Sound Level (Leq). It processes the signal in blocks,
|
|
1646
|
+
maintaining a running average of the samples. The result is yielded in blocks, allowing for
|
|
1647
|
+
memory-efficient processing of large datasets.
|
|
1648
|
+
"""
|
|
1649
|
+
|
|
1650
|
+
#: The input data source. It must be an instance of a
|
|
1651
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
1377
1652
|
source = Instance(SamplesGenerator)
|
|
1378
1653
|
|
|
1379
1654
|
def result(self, num):
|
|
1380
|
-
"""
|
|
1655
|
+
"""
|
|
1656
|
+
Generate the cumulative average of the input signal in blocks.
|
|
1657
|
+
|
|
1658
|
+
This method iterates through the signal samples provided by the :attr:`source`, and for each
|
|
1659
|
+
block, it computes the cumulative average of the samples up to that point. The result is
|
|
1660
|
+
yielded in blocks, with each block containing the cumulative average of the signal up to
|
|
1661
|
+
that sample.
|
|
1381
1662
|
|
|
1382
1663
|
Parameters
|
|
1383
1664
|
----------
|
|
1384
|
-
num :
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1665
|
+
num : :obj:`int`
|
|
1666
|
+
Number of samples per block.
|
|
1667
|
+
|
|
1668
|
+
Yields
|
|
1669
|
+
------
|
|
1670
|
+
:class:`numpy.ndarray`
|
|
1671
|
+
An array containing the cumulative average of the samples. Each block will have the
|
|
1672
|
+
shape (``num``, :attr:`~acoular.base.TimeOut.num_channels`), where
|
|
1673
|
+
:attr:`~acoular.base.TimeOut.num_channels` is inhereted from the :attr:`source`.
|
|
1674
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
1675
|
+
a multiple of ``num``.
|
|
1676
|
+
|
|
1677
|
+
Notes
|
|
1678
|
+
-----
|
|
1679
|
+
The cumulative average is updated iteratively by considering the previously accumulated sum
|
|
1680
|
+
and the current block of samples. For each new sample, the cumulative average is
|
|
1681
|
+
recalculated by summing the previous cumulative value and the new samples, then dividing by
|
|
1682
|
+
the total number of samples up to that point.
|
|
1394
1683
|
"""
|
|
1395
1684
|
count = (arange(num) + 1)[:, newaxis]
|
|
1396
1685
|
for i, temp in enumerate(self.source.result(num)):
|
|
@@ -1404,26 +1693,49 @@ class TimeCumAverage(TimeOut):
|
|
|
1404
1693
|
|
|
1405
1694
|
|
|
1406
1695
|
class TimeReverse(TimeOut):
|
|
1407
|
-
"""
|
|
1696
|
+
"""
|
|
1697
|
+
Calculates the time-reversed signal of a source.
|
|
1698
|
+
|
|
1699
|
+
This class takes the input signal from a source and computes the time-reversed version of the
|
|
1700
|
+
signal. It processes the signal in blocks, yielding the time-reversed signal block by block.
|
|
1701
|
+
This can be useful for various signal processing tasks, such as creating echoes or reversing
|
|
1702
|
+
the playback of time signal signals.
|
|
1703
|
+
"""
|
|
1408
1704
|
|
|
1409
|
-
|
|
1705
|
+
#: The input data source. It must be an instance of a
|
|
1706
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
1410
1707
|
source = Instance(SamplesGenerator)
|
|
1411
1708
|
|
|
1412
1709
|
def result(self, num):
|
|
1413
|
-
"""
|
|
1710
|
+
"""
|
|
1711
|
+
Generate the time-reversed version of the input signal block-wise.
|
|
1712
|
+
|
|
1713
|
+
This method processes the signal provided by the :attr:`source` in blocks, and for each
|
|
1714
|
+
block, it produces the time-reversed version of the signal. The result is yielded in blocks,
|
|
1715
|
+
with each block containing the time-reversed version of the signal for that segment.
|
|
1716
|
+
The signal is reversed in time by flipping the order of samples within each block.
|
|
1414
1717
|
|
|
1415
1718
|
Parameters
|
|
1416
1719
|
----------
|
|
1417
|
-
num :
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1720
|
+
num : :obj:`int`
|
|
1721
|
+
Number of samples per block.
|
|
1722
|
+
|
|
1723
|
+
Yields
|
|
1724
|
+
------
|
|
1725
|
+
:class:`numpy.ndarray`
|
|
1726
|
+
An array containing the time-reversed version of the signal for the current block.
|
|
1727
|
+
Each block will have the shape (``num``, :attr:`acoular.base.TimeOut.num_channels`),
|
|
1728
|
+
where :attr:`~acoular.base.TimeOut.num_channels` is inherited from the :attr:`source`.
|
|
1729
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
1730
|
+
a multiple of ``num``.
|
|
1731
|
+
|
|
1732
|
+
Notes
|
|
1733
|
+
-----
|
|
1734
|
+
The time-reversal is achieved by reversing the order of samples in each block of the signal.
|
|
1735
|
+
The :meth:`result` method first collects all the blocks from the source, then processes them
|
|
1736
|
+
in reverse order, yielding the time-reversed signal in blocks. The first block yielded
|
|
1737
|
+
corresponds to the last block of the source signal, and so on, until the entire signal has
|
|
1738
|
+
been processed in reverse.
|
|
1427
1739
|
"""
|
|
1428
1740
|
result_list = []
|
|
1429
1741
|
result_list.extend(self.source.result(num))
|
|
@@ -1439,37 +1751,56 @@ class TimeReverse(TimeOut):
|
|
|
1439
1751
|
|
|
1440
1752
|
|
|
1441
1753
|
class Filter(TimeOut):
|
|
1442
|
-
"""
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1754
|
+
"""
|
|
1755
|
+
Abstract base class for IIR filters using SciPy's :func:`~scipy.signal.lfilter`.
|
|
1756
|
+
|
|
1757
|
+
This class implements a digital Infinite Impulse Response (IIR) filter that applies filtering to
|
|
1758
|
+
a given signal in a block-wise manner. The filter coefficients can be dynamically changed during
|
|
1759
|
+
processing.
|
|
1760
|
+
|
|
1761
|
+
See Also
|
|
1762
|
+
--------
|
|
1763
|
+
:func:`scipy.signal.lfilter` :
|
|
1764
|
+
Filter data along one-dimension with an IIR or FIR (finite impulse response) filter.
|
|
1765
|
+
:func:`scipy.signal.sosfilt` :
|
|
1766
|
+
Filter data along one dimension using cascaded second-order sections.
|
|
1767
|
+
:class:`FiltOctave` :
|
|
1768
|
+
Octave or third-octave bandpass filter (causal, with non-zero phase delay).
|
|
1769
|
+
:class:`FiltFiltOctave` : Octave or third-octave bandpass filter with zero-phase distortion.
|
|
1447
1770
|
"""
|
|
1448
1771
|
|
|
1449
|
-
|
|
1772
|
+
#: The input data source. It must be an instance of a
|
|
1773
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
1450
1774
|
source = Instance(SamplesGenerator)
|
|
1451
1775
|
|
|
1452
|
-
|
|
1776
|
+
#: Second-order sections representation of the filter coefficients.
|
|
1777
|
+
#: This property is dynamically updated and can change during signal processing.
|
|
1453
1778
|
sos = Property()
|
|
1454
1779
|
|
|
1455
1780
|
def _get_sos(self):
|
|
1456
1781
|
return tf2sos([1], [1])
|
|
1457
1782
|
|
|
1458
1783
|
def result(self, num):
|
|
1459
|
-
"""
|
|
1784
|
+
"""
|
|
1785
|
+
Apply the IIR filter to the input signal and yields filtered data block-wise.
|
|
1786
|
+
|
|
1787
|
+
This method processes the signal provided by :attr:`source`, applying the defined filter
|
|
1788
|
+
coefficients (:attr:`sos`) using the :func:`scipy.signal.sosfilt` function. The filtering
|
|
1789
|
+
is performed in a streaming fashion, yielding blocks of filtered signal data.
|
|
1460
1790
|
|
|
1461
1791
|
Parameters
|
|
1462
1792
|
----------
|
|
1463
|
-
num :
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1793
|
+
num : :obj:`int`
|
|
1794
|
+
Number of samples per block.
|
|
1795
|
+
|
|
1796
|
+
Yields
|
|
1797
|
+
------
|
|
1798
|
+
:class:`numpy.ndarray`
|
|
1799
|
+
An array containing the bandpass-filtered signal for the current block. Each block has
|
|
1800
|
+
the shape (``num``, :attr:`~acoular.base.TimeOut.num_channels`), where
|
|
1801
|
+
:attr:`~acoular.base.TimeOut.num_channels` is inherited from the :attr:`source`.
|
|
1802
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
1803
|
+
a multiple of ``num``.
|
|
1473
1804
|
"""
|
|
1474
1805
|
sos = self.sos
|
|
1475
1806
|
zi = zeros((sos.shape[0], 2, self.source.num_channels))
|
|
@@ -1481,20 +1812,42 @@ class Filter(TimeOut):
|
|
|
1481
1812
|
|
|
1482
1813
|
|
|
1483
1814
|
class FiltOctave(Filter):
|
|
1484
|
-
"""
|
|
1815
|
+
"""
|
|
1816
|
+
Octave or third-octave bandpass filter (causal, with non-zero phase delay).
|
|
1817
|
+
|
|
1818
|
+
This class implements a bandpass filter that conforms to octave or third-octave frequency band
|
|
1819
|
+
standards. The filter is designed using a second-order section (SOS) Infinite Impulse Response
|
|
1820
|
+
(IIR) approach.
|
|
1821
|
+
|
|
1822
|
+
The filtering process introduces a non-zero phase delay due to its causal nature. The center
|
|
1823
|
+
frequency and the octave fraction determine the frequency band characteristics.
|
|
1824
|
+
|
|
1825
|
+
See Also
|
|
1826
|
+
--------
|
|
1827
|
+
:class:`Filter` : The base class implementing a general IIR filter.
|
|
1828
|
+
:class:`FiltFiltOctave` : Octave or third-octave bandpass filter with zero-phase distortion.
|
|
1829
|
+
"""
|
|
1485
1830
|
|
|
1486
|
-
|
|
1831
|
+
#: The center frequency of the octave or third-octave band. Default is ``1000``.
|
|
1487
1832
|
band = Float(1000.0, desc='band center frequency')
|
|
1488
1833
|
|
|
1489
|
-
|
|
1834
|
+
#: Defines whether the filter is an octave-band or third-octave-band filter.
|
|
1835
|
+
#:
|
|
1836
|
+
#: - ``'Octave'``: Full octave band filter.
|
|
1837
|
+
#: - ``'Third octave'``: Third-octave band filter.
|
|
1838
|
+
#:
|
|
1839
|
+
#: Default is ``'Octave'``.
|
|
1490
1840
|
fraction = Map({'Octave': 1, 'Third octave': 3}, default_value='Octave', desc='fraction of octave')
|
|
1491
1841
|
|
|
1492
|
-
|
|
1842
|
+
#: The order of the IIR filter, which affects the steepness of the filter's roll-off.
|
|
1843
|
+
#: Default is ``3``.
|
|
1493
1844
|
order = Int(3, desc='IIR filter order')
|
|
1494
1845
|
|
|
1846
|
+
#: Second-order sections representation of the filter coefficients. This property depends on
|
|
1847
|
+
#: :attr:`band`, :attr:`fraction`, :attr:`order`, and the source's digest.
|
|
1495
1848
|
sos = Property(depends_on=['band', 'fraction', 'source.digest', 'order'])
|
|
1496
1849
|
|
|
1497
|
-
|
|
1850
|
+
#: A unique identifier for the filter, based on its properties. (read-only)
|
|
1498
1851
|
digest = Property(depends_on=['source.digest', 'band', 'fraction', 'order'])
|
|
1499
1852
|
|
|
1500
1853
|
@cached_property
|
|
@@ -1503,6 +1856,25 @@ class FiltOctave(Filter):
|
|
|
1503
1856
|
|
|
1504
1857
|
@cached_property
|
|
1505
1858
|
def _get_sos(self):
|
|
1859
|
+
# Compute the second-order section coefficients for the bandpass filter.
|
|
1860
|
+
|
|
1861
|
+
# The filter design follows ANSI S1.11-1987 standards and adjusts
|
|
1862
|
+
# filter edge frequencies to maintain correct power bandwidth.
|
|
1863
|
+
|
|
1864
|
+
# The filter is implemented using a Butterworth design, with
|
|
1865
|
+
# appropriate frequency scaling to match the desired octave band.
|
|
1866
|
+
|
|
1867
|
+
# Returns
|
|
1868
|
+
# -------
|
|
1869
|
+
# :class:`numpy.ndarray`
|
|
1870
|
+
# SOS (second-order section) coefficients for the filter.
|
|
1871
|
+
|
|
1872
|
+
# Raises
|
|
1873
|
+
# ------
|
|
1874
|
+
# :obj:`ValueError`
|
|
1875
|
+
# If the center frequency (:attr:`band`) is too high relative to
|
|
1876
|
+
# the sampling frequency.
|
|
1877
|
+
|
|
1506
1878
|
# filter design
|
|
1507
1879
|
fs = self.sample_freq
|
|
1508
1880
|
# adjust filter edge frequencies for correct power bandwidth (see ANSI 1.11 1987
|
|
@@ -1522,16 +1894,31 @@ class FiltOctave(Filter):
|
|
|
1522
1894
|
|
|
1523
1895
|
|
|
1524
1896
|
class FiltFiltOctave(FiltOctave):
|
|
1525
|
-
"""
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1897
|
+
"""
|
|
1898
|
+
Octave or third-octave bandpass filter with zero-phase distortion.
|
|
1899
|
+
|
|
1900
|
+
This filter applies an IIR bandpass filter in both forward and reverse directions, effectively
|
|
1901
|
+
eliminating phase distortion. It provides zero-phase filtering but requires significantly more
|
|
1902
|
+
memory compared to causal filtering.
|
|
1903
|
+
|
|
1904
|
+
See Also
|
|
1905
|
+
--------
|
|
1906
|
+
:class:`Filter` : The base class implementing a general IIR filter.
|
|
1907
|
+
:class:`FiltOctave` : The standard octave or third-octave filter with causal filtering.
|
|
1908
|
+
|
|
1909
|
+
Notes
|
|
1910
|
+
-----
|
|
1911
|
+
- Due to the double-pass filtering, additional bandwidth correction is applied to maintain
|
|
1912
|
+
accurate frequency response.
|
|
1913
|
+
- This approach requires storing the entire signal in memory before processing, making it
|
|
1914
|
+
unsuitable for real-time applications with large datasets.
|
|
1529
1915
|
"""
|
|
1530
1916
|
|
|
1531
|
-
|
|
1917
|
+
#: The half-order of the IIR filter, applied twice (once forward and once backward). This
|
|
1918
|
+
#: results in a final filter order twice as large as the specified value. Default is ``2``.
|
|
1532
1919
|
order = Int(2, desc='IIR filter half order')
|
|
1533
1920
|
|
|
1534
|
-
|
|
1921
|
+
#: A unique identifier for the filter, based on its properties. (read-only)
|
|
1535
1922
|
digest = Property(depends_on=['source.digest', 'band', 'fraction', 'order'])
|
|
1536
1923
|
|
|
1537
1924
|
@cached_property
|
|
@@ -1540,6 +1927,22 @@ class FiltFiltOctave(FiltOctave):
|
|
|
1540
1927
|
|
|
1541
1928
|
@cached_property
|
|
1542
1929
|
def _get_sos(self):
|
|
1930
|
+
# Compute the second-order section (SOS) coefficients for the filter.
|
|
1931
|
+
#
|
|
1932
|
+
# The filter design follows ANSI S1.11-1987 standards and incorporates additional bandwidth
|
|
1933
|
+
# correction to compensate for the double-pass filtering effect.
|
|
1934
|
+
#
|
|
1935
|
+
# Returns
|
|
1936
|
+
# -------
|
|
1937
|
+
# :class:`numpy.ndarray`
|
|
1938
|
+
# SOS (second-order section) coefficients for the filter.
|
|
1939
|
+
#
|
|
1940
|
+
# Raises
|
|
1941
|
+
# ------
|
|
1942
|
+
# :obj:`ValueError`
|
|
1943
|
+
# If the center frequency (:attr:`band`) is too high relative to the
|
|
1944
|
+
# sampling frequency.
|
|
1945
|
+
|
|
1543
1946
|
# filter design
|
|
1544
1947
|
fs = self.sample_freq
|
|
1545
1948
|
# adjust filter edge frequencies for correct power bandwidth (see FiltOctave)
|
|
@@ -1560,20 +1963,31 @@ class FiltFiltOctave(FiltOctave):
|
|
|
1560
1963
|
return butter(self.order, [om1, om2], 'bandpass', output='sos')
|
|
1561
1964
|
|
|
1562
1965
|
def result(self, num):
|
|
1563
|
-
"""
|
|
1966
|
+
"""
|
|
1967
|
+
Apply the filter to the input signal and yields filtered data block-wise.
|
|
1968
|
+
|
|
1969
|
+
The input signal is first stored in memory, then filtered in both forward and reverse
|
|
1970
|
+
directions to achieve zero-phase distortion. The processed signal is yielded in blocks.
|
|
1564
1971
|
|
|
1565
1972
|
Parameters
|
|
1566
1973
|
----------
|
|
1567
|
-
num :
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1974
|
+
num : :obj:`int`
|
|
1975
|
+
Number of samples per block.
|
|
1976
|
+
|
|
1977
|
+
Yields
|
|
1978
|
+
------
|
|
1979
|
+
:class:`numpy.ndarray`
|
|
1980
|
+
An array containing the filtered signal for the current block. Each block has shape
|
|
1981
|
+
(``num``, :attr:`~acoular.base.TimeOut.num_channels`), where
|
|
1982
|
+
:attr:`~acoular.base.TimeOut.num_channels` is inherited from the :attr:`source`.
|
|
1983
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
1984
|
+
a multiple of ``num``.
|
|
1985
|
+
|
|
1986
|
+
Notes
|
|
1987
|
+
-----
|
|
1988
|
+
- This method requires the entire signal to be stored in memory, making it unsuitable for
|
|
1989
|
+
streaming or real-time applications.
|
|
1990
|
+
- Filtering is performed separately for each channel to optimize memory usage.
|
|
1577
1991
|
"""
|
|
1578
1992
|
sos = self.sos
|
|
1579
1993
|
data = empty((self.source.num_samples, self.source.num_channels))
|
|
@@ -1593,17 +2007,36 @@ class FiltFiltOctave(FiltOctave):
|
|
|
1593
2007
|
|
|
1594
2008
|
|
|
1595
2009
|
class TimeExpAverage(Filter):
|
|
1596
|
-
"""Computes exponential averaging according to IEC 61672-1
|
|
1597
|
-
time constant: F -> 125 ms, S -> 1 s
|
|
1598
|
-
I (non-standard) -> 35 ms.
|
|
1599
2010
|
"""
|
|
2011
|
+
Compute an exponentially weighted moving average of the input signal.
|
|
1600
2012
|
|
|
1601
|
-
|
|
2013
|
+
This filter implements exponential averaging as defined in IEC 61672-1, which is commonly used
|
|
2014
|
+
for sound level measurements. The time weighting determines how quickly past values decay in
|
|
2015
|
+
significance.
|
|
2016
|
+
|
|
2017
|
+
See Also
|
|
2018
|
+
--------
|
|
2019
|
+
:class:`Filter` : Base class for implementing IIR filters.
|
|
2020
|
+
|
|
2021
|
+
Notes
|
|
2022
|
+
-----
|
|
2023
|
+
The `Impulse` (``'I'``) weighting is not part of IEC 61672-1 but is included for additional
|
|
2024
|
+
flexibility.
|
|
2025
|
+
"""
|
|
2026
|
+
|
|
2027
|
+
#: Time weighting constant, determining the exponential decay rate.
|
|
2028
|
+
#:
|
|
2029
|
+
#: - ``'F'`` (Fast) → 0.125
|
|
2030
|
+
#: - ``'S'`` (Slow) → 1.0
|
|
2031
|
+
#: - ``'I'`` (Impulse) → 0.035 (non-standard)
|
|
2032
|
+
#:
|
|
2033
|
+
#: Default is ``'F'``.
|
|
1602
2034
|
weight = Map({'F': 0.125, 'S': 1.0, 'I': 0.035}, default_value='F', desc='time weighting')
|
|
1603
2035
|
|
|
2036
|
+
#: Filter coefficients in second-order section (SOS) format.
|
|
1604
2037
|
sos = Property(depends_on=['weight', 'source.digest'])
|
|
1605
2038
|
|
|
1606
|
-
|
|
2039
|
+
#: A unique identifier for the filter, based on its properties. (read-only)
|
|
1607
2040
|
digest = Property(depends_on=['source.digest', 'weight'])
|
|
1608
2041
|
|
|
1609
2042
|
@cached_property
|
|
@@ -1612,6 +2045,34 @@ class TimeExpAverage(Filter):
|
|
|
1612
2045
|
|
|
1613
2046
|
@cached_property
|
|
1614
2047
|
def _get_sos(self):
|
|
2048
|
+
# Compute the second-order section (SOS) coefficients for the exponential filter.
|
|
2049
|
+
#
|
|
2050
|
+
# The filter follows the form of a first-order IIR filter:
|
|
2051
|
+
#
|
|
2052
|
+
# .. math::
|
|
2053
|
+
# y[n] = \\alpha x[n] + (1 - \\alpha) y[n-1]
|
|
2054
|
+
#
|
|
2055
|
+
# where :math:`\\alpha` is determined by the selected time weighting.
|
|
2056
|
+
#
|
|
2057
|
+
# Returns
|
|
2058
|
+
# -------
|
|
2059
|
+
# :class:`numpy.ndarray`
|
|
2060
|
+
# SOS (second-order section) coefficients representing the filter.
|
|
2061
|
+
#
|
|
2062
|
+
# Notes
|
|
2063
|
+
# -----
|
|
2064
|
+
# The coefficient :math:`\\alpha` is calculated as:
|
|
2065
|
+
#
|
|
2066
|
+
# .. math::
|
|
2067
|
+
# \\alpha = 1 - e^{-1 / (\\tau f_s)}
|
|
2068
|
+
#
|
|
2069
|
+
# where:
|
|
2070
|
+
#
|
|
2071
|
+
# - :math:`\\tau` is the selected time constant (:attr:`weight`).
|
|
2072
|
+
# - :math:`f_s` is the sampling frequency of the source.
|
|
2073
|
+
#
|
|
2074
|
+
# This implementation ensures that the filter adapts dynamically
|
|
2075
|
+
# based on the source's sampling frequency.
|
|
1615
2076
|
alpha = 1 - exp(-1 / self.weight_ / self.sample_freq)
|
|
1616
2077
|
a = [1, alpha - 1]
|
|
1617
2078
|
b = [alpha]
|
|
@@ -1619,14 +2080,37 @@ class TimeExpAverage(Filter):
|
|
|
1619
2080
|
|
|
1620
2081
|
|
|
1621
2082
|
class FiltFreqWeight(Filter):
|
|
1622
|
-
"""
|
|
2083
|
+
"""
|
|
2084
|
+
Apply frequency weighting according to IEC 61672-1.
|
|
2085
|
+
|
|
2086
|
+
This filter implements frequency weighting curves commonly used in sound level meters for noise
|
|
2087
|
+
measurement. It provides A-weighting, C-weighting, and Z-weighting options.
|
|
2088
|
+
|
|
2089
|
+
See Also
|
|
2090
|
+
--------
|
|
2091
|
+
:class:`Filter` : Base class for implementing IIR filters.
|
|
1623
2092
|
|
|
1624
|
-
|
|
2093
|
+
Notes
|
|
2094
|
+
-----
|
|
2095
|
+
- The filter is designed following IEC 61672-1:2002, the standard for sound level meters.
|
|
2096
|
+
- The weighting curves are implemented using bilinear transformation of analog filter
|
|
2097
|
+
coefficients to the discrete domain.
|
|
2098
|
+
"""
|
|
2099
|
+
|
|
2100
|
+
#: Defines the frequency weighting curve:
|
|
2101
|
+
#:
|
|
2102
|
+
#: - ``'A'``: Mimics human hearing sensitivity at low sound levels.
|
|
2103
|
+
#: - ``'C'``: Used for high-level sound measurements with less attenuation at low frequencies.
|
|
2104
|
+
#: - ``'Z'``: A flat response with no frequency weighting.
|
|
2105
|
+
#:
|
|
2106
|
+
#: Default is ``'A'``.
|
|
1625
2107
|
weight = Enum('A', 'C', 'Z', desc='frequency weighting')
|
|
1626
2108
|
|
|
2109
|
+
#: Second-order sections (SOS) representation of the filter coefficients. This property is
|
|
2110
|
+
#: dynamically computed based on :attr:`weight` and the :attr:`Filter.source`'s digest.
|
|
1627
2111
|
sos = Property(depends_on=['weight', 'source.digest'])
|
|
1628
2112
|
|
|
1629
|
-
|
|
2113
|
+
#: A unique identifier for the filter, based on its properties. (read-only)
|
|
1630
2114
|
digest = Property(depends_on=['source.digest', 'weight'])
|
|
1631
2115
|
|
|
1632
2116
|
@cached_property
|
|
@@ -1635,6 +2119,45 @@ class FiltFreqWeight(Filter):
|
|
|
1635
2119
|
|
|
1636
2120
|
@cached_property
|
|
1637
2121
|
def _get_sos(self):
|
|
2122
|
+
# Compute the second-order section (SOS) coefficients for the frequency weighting filter.
|
|
2123
|
+
#
|
|
2124
|
+
# The filter design is based on analog weighting functions defined in IEC 61672-1,
|
|
2125
|
+
# transformed into the discrete-time domain using the bilinear transformation.
|
|
2126
|
+
#
|
|
2127
|
+
# Returns
|
|
2128
|
+
# -------
|
|
2129
|
+
# :class:`numpy.ndarray`
|
|
2130
|
+
# SOS (second-order section) coefficients representing the filter.
|
|
2131
|
+
#
|
|
2132
|
+
# Notes
|
|
2133
|
+
# -----
|
|
2134
|
+
# The analog weighting functions are defined as:
|
|
2135
|
+
#
|
|
2136
|
+
# - **A-weighting**:
|
|
2137
|
+
#
|
|
2138
|
+
# .. math::
|
|
2139
|
+
# H(s) = \\frac{(2 \\pi f_4)^2 (s + 2 \\pi f_3) (s + 2 \\pi f_2)}
|
|
2140
|
+
# {(s + 2 \\pi f_4) (s + 2 \\pi f_1) (s^2 + 4 \\pi f_1 s + (2 \\pi f_1)^2)}
|
|
2141
|
+
#
|
|
2142
|
+
# where the parameters are:
|
|
2143
|
+
#
|
|
2144
|
+
# - :math:`f_1 = 20.598997` Hz
|
|
2145
|
+
# - :math:`f_2 = 107.65265` Hz
|
|
2146
|
+
# - :math:`f_3 = 737.86223` Hz
|
|
2147
|
+
# - :math:`f_4 = 12194.217` Hz
|
|
2148
|
+
#
|
|
2149
|
+
# - **C-weighting** follows a similar approach but without the low-frequency roll-off.
|
|
2150
|
+
#
|
|
2151
|
+
# - **Z-weighting** is implemented as a flat response (no filtering).
|
|
2152
|
+
#
|
|
2153
|
+
# The bilinear transformation is used to convert these analog functions into
|
|
2154
|
+
# the digital domain, preserving the frequency response characteristics.
|
|
2155
|
+
#
|
|
2156
|
+
# Raises
|
|
2157
|
+
# ------
|
|
2158
|
+
# :obj:`ValueError`
|
|
2159
|
+
# If an invalid weight type is provided.
|
|
2160
|
+
|
|
1638
2161
|
# s domain coefficients
|
|
1639
2162
|
f1 = 20.598997
|
|
1640
2163
|
f2 = 107.65265
|
|
@@ -1657,59 +2180,82 @@ class FiltFreqWeight(Filter):
|
|
|
1657
2180
|
return tf2sos(b, a)
|
|
1658
2181
|
|
|
1659
2182
|
|
|
1660
|
-
@deprecated_alias({'numbands': 'num_bands'}, read_only=True)
|
|
2183
|
+
@deprecated_alias({'numbands': 'num_bands'}, read_only=True, removal_version='25.10')
|
|
1661
2184
|
class FilterBank(TimeOut):
|
|
1662
|
-
"""
|
|
1663
|
-
|
|
2185
|
+
"""
|
|
2186
|
+
Abstract base class for IIR filter banks based on :mod:`scipy.signal.lfilter`.
|
|
1664
2187
|
|
|
1665
|
-
|
|
2188
|
+
Implements a bank of parallel filters. This class should not be instantiated by itself.
|
|
2189
|
+
|
|
2190
|
+
Inherits from :class:`acoular.base.TimeOut`, and defines the structure for working with filter
|
|
2191
|
+
banks for processing multi-channel time series data, such as time signal signals.
|
|
2192
|
+
|
|
2193
|
+
See Also
|
|
2194
|
+
--------
|
|
2195
|
+
:class:`acoular.base.TimeOut` :
|
|
2196
|
+
ABC for signal processing blocks that interact with data from a source.
|
|
2197
|
+
:class:`acoular.base.SamplesGenerator` :
|
|
2198
|
+
Interface for any generating multi-channel time domain signal processing block.
|
|
2199
|
+
:mod:`scipy.signal` :
|
|
2200
|
+
SciPy module for signal processing.
|
|
1666
2201
|
"""
|
|
1667
2202
|
|
|
1668
|
-
|
|
2203
|
+
#: The input data source. It must be an instance of a
|
|
2204
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
1669
2205
|
source = Instance(SamplesGenerator)
|
|
1670
2206
|
|
|
1671
|
-
|
|
2207
|
+
#: The list containing second order section (SOS) coefficients for the filters in the filter
|
|
2208
|
+
#: bank.
|
|
1672
2209
|
sos = Property()
|
|
1673
2210
|
|
|
1674
|
-
|
|
2211
|
+
#: A list of labels describing the different frequency bands of the filter bank.
|
|
1675
2212
|
bands = Property()
|
|
1676
2213
|
|
|
1677
|
-
|
|
2214
|
+
#: The total number of bands in the filter bank.
|
|
1678
2215
|
num_bands = Property()
|
|
1679
2216
|
|
|
1680
|
-
|
|
2217
|
+
#: The total number of output channels resulting from the filter bank operation.
|
|
1681
2218
|
num_channels = Property()
|
|
1682
2219
|
|
|
1683
2220
|
@abstractmethod
|
|
1684
2221
|
def _get_sos(self):
|
|
1685
|
-
"""
|
|
2222
|
+
"""Return a list of second order section coefficients."""
|
|
1686
2223
|
|
|
1687
2224
|
@abstractmethod
|
|
1688
2225
|
def _get_bands(self):
|
|
1689
|
-
"""
|
|
2226
|
+
"""Return a list of labels for the bands."""
|
|
1690
2227
|
|
|
1691
2228
|
@abstractmethod
|
|
1692
2229
|
def _get_num_bands(self):
|
|
1693
|
-
"""
|
|
2230
|
+
"""Return the number of bands."""
|
|
1694
2231
|
|
|
1695
2232
|
def _get_num_channels(self):
|
|
1696
2233
|
return self.num_bands * self.source.num_channels
|
|
1697
2234
|
|
|
1698
2235
|
def result(self, num):
|
|
1699
|
-
"""
|
|
2236
|
+
"""
|
|
2237
|
+
Yield the bandpass filtered output of the source in blocks of samples.
|
|
2238
|
+
|
|
2239
|
+
This method uses the second order section coefficients (:attr:`sos`) to filter the input
|
|
2240
|
+
samples provided by the source in blocks. The result is returned as a generator.
|
|
1700
2241
|
|
|
1701
2242
|
Parameters
|
|
1702
2243
|
----------
|
|
1703
|
-
num :
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
The last block may
|
|
1712
|
-
|
|
2244
|
+
num : :obj:`int`
|
|
2245
|
+
Number of samples per block.
|
|
2246
|
+
|
|
2247
|
+
Yields
|
|
2248
|
+
------
|
|
2249
|
+
:obj:`numpy.ndarray`
|
|
2250
|
+
An array of shape (``num``, :attr:`num_channels`), delivering the filtered
|
|
2251
|
+
samples for each band.
|
|
2252
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
2253
|
+
a multiple of ``num``.
|
|
2254
|
+
|
|
2255
|
+
Notes
|
|
2256
|
+
-----
|
|
2257
|
+
The returned samples are bandpass filtered according to the coefficients in
|
|
2258
|
+
:attr:`sos`. Each block corresponds to the filtered samples for each frequency band.
|
|
1713
2259
|
"""
|
|
1714
2260
|
numbands = self.num_bands
|
|
1715
2261
|
snumch = self.source.num_channels
|
|
@@ -1723,27 +2269,49 @@ class FilterBank(TimeOut):
|
|
|
1723
2269
|
|
|
1724
2270
|
|
|
1725
2271
|
class OctaveFilterBank(FilterBank):
|
|
1726
|
-
"""
|
|
2272
|
+
"""
|
|
2273
|
+
Octave or third-octave filter bank.
|
|
2274
|
+
|
|
2275
|
+
Inherits from :class:`FilterBank` and implements an octave or third-octave filter bank.
|
|
2276
|
+
This class is used for filtering multi-channel time series data, such as time signal signals,
|
|
2277
|
+
using bandpass filters with center frequencies at octave or third-octave intervals.
|
|
2278
|
+
|
|
2279
|
+
See Also
|
|
2280
|
+
--------
|
|
2281
|
+
:class:`FilterBank` :
|
|
2282
|
+
The base class for implementing IIR filter banks.
|
|
2283
|
+
:class:`acoular.base.SamplesGenerator` :
|
|
2284
|
+
Interface for generating multi-channel time domain signal processing blocks.
|
|
2285
|
+
:mod:`scipy.signal` :
|
|
2286
|
+
SciPy module for signal processing.
|
|
2287
|
+
"""
|
|
1727
2288
|
|
|
1728
|
-
|
|
2289
|
+
#: The lowest band center frequency index. Default is ``21``.
|
|
2290
|
+
#: This index refers to the position in the scale of octave or third-octave bands.
|
|
1729
2291
|
lband = Int(21, desc='lowest band center frequency index')
|
|
1730
2292
|
|
|
1731
|
-
|
|
1732
|
-
|
|
2293
|
+
#: The highest band center frequency index + 1. Default is ``40``.
|
|
2294
|
+
#: This is the position in the scale of octave or third-octave bands.
|
|
2295
|
+
hband = Int(40, desc='highest band center frequency index + 1')
|
|
1733
2296
|
|
|
1734
|
-
|
|
2297
|
+
#: The fraction of an octave, either ``'Octave'`` or ``'Third octave'``.
|
|
2298
|
+
#: Default is ``'Octave'``.
|
|
2299
|
+
#: Determines the width of the frequency bands. 'Octave' refers to full octaves,
|
|
2300
|
+
#: and ``'Third octave'`` refers to third-octave bands.
|
|
1735
2301
|
fraction = Map({'Octave': 1, 'Third octave': 3}, default_value='Octave', desc='fraction of octave')
|
|
1736
2302
|
|
|
1737
|
-
|
|
2303
|
+
#: The list of filter coefficients for all filters in the filter bank.
|
|
2304
|
+
#: The coefficients are computed based on the :attr:`lband`, :attr:`hband`,
|
|
2305
|
+
#: and :attr:`fraction` attributes.
|
|
1738
2306
|
ba = Property(depends_on=['lband', 'hband', 'fraction', 'source.digest'])
|
|
1739
2307
|
|
|
1740
|
-
|
|
2308
|
+
#: The list of labels describing the frequency bands in the filter bank.
|
|
1741
2309
|
bands = Property(depends_on=['lband', 'hband', 'fraction'])
|
|
1742
2310
|
|
|
1743
|
-
|
|
2311
|
+
#: The total number of bands in the filter bank.
|
|
1744
2312
|
num_bands = Property(depends_on=['lband', 'hband', 'fraction'])
|
|
1745
2313
|
|
|
1746
|
-
|
|
2314
|
+
#: A unique identifier for the filter, based on its properties. (read-only)
|
|
1747
2315
|
digest = Property(depends_on=['source.digest', 'lband', 'hband', 'fraction', 'order'])
|
|
1748
2316
|
|
|
1749
2317
|
@cached_property
|
|
@@ -1760,6 +2328,16 @@ class OctaveFilterBank(FilterBank):
|
|
|
1760
2328
|
|
|
1761
2329
|
@cached_property
|
|
1762
2330
|
def _get_sos(self):
|
|
2331
|
+
# Generate and return the second-order section (SOS) coefficients for each filter.
|
|
2332
|
+
#
|
|
2333
|
+
# For each frequency band in the filter bank, the SOS coefficients are calculated using
|
|
2334
|
+
# the :class:`FiltOctave` object with the appropriate `fraction` setting. The coefficients
|
|
2335
|
+
# are then returned as a list.
|
|
2336
|
+
#
|
|
2337
|
+
# Returns
|
|
2338
|
+
# -------
|
|
2339
|
+
# :obj:`list` of :obj:`numpy.ndarray`
|
|
2340
|
+
# A list of SOS coefficients for each filter in the filter bank.
|
|
1763
2341
|
of = FiltOctave(source=self.source, fraction=self.fraction)
|
|
1764
2342
|
sos = []
|
|
1765
2343
|
for i in range(self.lband, self.hband, 4 - self.fraction_):
|
|
@@ -1769,26 +2347,47 @@ class OctaveFilterBank(FilterBank):
|
|
|
1769
2347
|
return sos
|
|
1770
2348
|
|
|
1771
2349
|
|
|
1772
|
-
@deprecated_alias({'name': 'file'})
|
|
2350
|
+
@deprecated_alias({'name': 'file'}, removal_version='25.10')
|
|
1773
2351
|
class WriteWAV(TimeOut):
|
|
1774
|
-
"""
|
|
1775
|
-
|
|
2352
|
+
"""
|
|
2353
|
+
Saves time signal from one or more channels as mono, stereo, or multi-channel ``.wav`` file.
|
|
2354
|
+
|
|
2355
|
+
Inherits from :class:`~acoular.base.TimeOut` and allows for exporting time-series data from one
|
|
2356
|
+
or more channels to a WAV file. Supports saving mono, stereo, or multi-channel signals to disk
|
|
2357
|
+
with automatic or user-defined file naming.
|
|
2358
|
+
|
|
2359
|
+
See Also
|
|
2360
|
+
--------
|
|
2361
|
+
:class:`acoular.base.TimeOut` :
|
|
2362
|
+
ABC for signal processing blocks that interact with data from a source.
|
|
2363
|
+
:class:`acoular.base.SamplesGenerator` :
|
|
2364
|
+
Interface for generating multi-channel time domain signal processing blocks.
|
|
2365
|
+
:mod:`wave` :
|
|
2366
|
+
Python module for handling WAV files.
|
|
1776
2367
|
"""
|
|
1777
2368
|
|
|
1778
|
-
|
|
2369
|
+
#: The input data source. It must be an instance of a
|
|
2370
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
1779
2371
|
source = Instance(SamplesGenerator)
|
|
1780
2372
|
|
|
1781
|
-
|
|
1782
|
-
|
|
2373
|
+
#: The name of the file to be saved. If none is given, the name will be automatically
|
|
2374
|
+
#: generated from the source.
|
|
1783
2375
|
file = File(filter=['*.wav'], desc='name of wave file')
|
|
1784
2376
|
|
|
1785
|
-
|
|
2377
|
+
#: The name of the cache file (without extension). It serves as an internal reference for data
|
|
2378
|
+
#: caching and tracking processed files. (automatically generated)
|
|
1786
2379
|
basename = Property(depends_on=['digest'])
|
|
1787
2380
|
|
|
1788
|
-
|
|
1789
|
-
channels = List(int, desc='
|
|
2381
|
+
#: The list of channels to save. Can only contain one or two channels.
|
|
2382
|
+
channels = List(int, desc='channels to save')
|
|
1790
2383
|
|
|
1791
|
-
#
|
|
2384
|
+
# Bit depth of the output file.
|
|
2385
|
+
encoding = Enum('uint8', 'int16', 'int32', desc='bit depth of the output file')
|
|
2386
|
+
|
|
2387
|
+
# Maximum value to scale the output to. If `None`, the maximum value of the data is used.
|
|
2388
|
+
max_val = Either(None, Float, desc='Maximum value to scale the output to.')
|
|
2389
|
+
|
|
2390
|
+
#: A unique identifier for the filter, based on its properties. (read-only)
|
|
1792
2391
|
digest = Property(depends_on=['source.digest', 'channels'])
|
|
1793
2392
|
|
|
1794
2393
|
@cached_property
|
|
@@ -1800,21 +2399,80 @@ class WriteWAV(TimeOut):
|
|
|
1800
2399
|
warn(
|
|
1801
2400
|
(
|
|
1802
2401
|
f'The basename attribute of a {self.__class__.__name__} object is deprecated'
|
|
1803
|
-
' and will be removed in
|
|
2402
|
+
' and will be removed in Acoular 26.01!'
|
|
1804
2403
|
),
|
|
1805
2404
|
DeprecationWarning,
|
|
1806
2405
|
stacklevel=2,
|
|
1807
2406
|
)
|
|
1808
2407
|
return find_basename(self.source)
|
|
1809
2408
|
|
|
1810
|
-
def
|
|
1811
|
-
|
|
2409
|
+
def _type_info(self):
|
|
2410
|
+
dtype = np.dtype(self.encoding)
|
|
2411
|
+
info = np.iinfo(dtype)
|
|
2412
|
+
return dtype, info.min, info.max, int(info.bits / 8)
|
|
2413
|
+
|
|
2414
|
+
def _encode(self, data):
|
|
2415
|
+
"""Encodes the data according to self.encoding."""
|
|
2416
|
+
dtype, dmin, dmax, _ = self._type_info()
|
|
2417
|
+
if dtype == np.dtype('uint8'):
|
|
2418
|
+
data = (data + 1) / 2 * dmax
|
|
2419
|
+
else:
|
|
2420
|
+
data *= -dmin
|
|
2421
|
+
data = np.round(data)
|
|
2422
|
+
if data.min() < dmin or data.max() > dmax:
|
|
2423
|
+
warn(
|
|
2424
|
+
f'Clipping occurred in WAV export. Data type {dtype} cannot represent all values in data. \
|
|
2425
|
+
Consider raising max_val.',
|
|
2426
|
+
stacklevel=1,
|
|
2427
|
+
)
|
|
2428
|
+
return data.clip(dmin, dmax).astype(dtype).tobytes()
|
|
2429
|
+
|
|
2430
|
+
def result(self, num):
|
|
2431
|
+
"""
|
|
2432
|
+
Generate and save time signal data as a WAV file in blocks.
|
|
2433
|
+
|
|
2434
|
+
This generator method retrieves time signal data from the :attr:`source` and writes it to a
|
|
2435
|
+
WAV file in blocks of size ``num``. The data is scaled and encoded according to the selected
|
|
2436
|
+
bit depth and channel configuration. If no file name is specified, a name is generated
|
|
2437
|
+
automatically. The method yields each block of data after it is written to the file,
|
|
2438
|
+
allowing for streaming or real-time processing.
|
|
2439
|
+
|
|
2440
|
+
Parameters
|
|
2441
|
+
----------
|
|
2442
|
+
num : :class:`int`
|
|
2443
|
+
Number of samples per block to write and yield.
|
|
2444
|
+
|
|
2445
|
+
Yields
|
|
2446
|
+
------
|
|
2447
|
+
:class:`numpy.ndarray`
|
|
2448
|
+
The block of time signal data that was written to the WAV file, with shape
|
|
2449
|
+
(``num``, number of channels).
|
|
2450
|
+
|
|
2451
|
+
Raises
|
|
2452
|
+
------
|
|
2453
|
+
:class:`ValueError`
|
|
2454
|
+
If no channels are specified for output.
|
|
2455
|
+
:class:`Warning`
|
|
2456
|
+
If more than two channels are specified, or if the sample frequency is not an integer.
|
|
2457
|
+
Also warns if clipping occurs due to data range limitations.
|
|
2458
|
+
|
|
2459
|
+
See Also
|
|
2460
|
+
--------
|
|
2461
|
+
:meth:`save` : Save the entire source output to a WAV file in one call.
|
|
2462
|
+
"""
|
|
1812
2463
|
nc = len(self.channels)
|
|
1813
2464
|
if nc == 0:
|
|
1814
2465
|
msg = 'No channels given for output.'
|
|
1815
2466
|
raise ValueError(msg)
|
|
1816
|
-
|
|
2467
|
+
elif nc > 2:
|
|
1817
2468
|
warn(f'More than two channels given for output, exported file will have {nc:d} channels', stacklevel=1)
|
|
2469
|
+
if self.sample_freq.is_integer():
|
|
2470
|
+
fs = self.sample_freq
|
|
2471
|
+
else:
|
|
2472
|
+
fs = int(round(self.sample_freq))
|
|
2473
|
+
msg = f'Sample frequency {self.sample_freq} is not a whole number. Proceeding with sampling frequency {fs}.'
|
|
2474
|
+
warn(msg, Warning, stacklevel=1)
|
|
2475
|
+
dtype, _, dmax, sw = self._type_info()
|
|
1818
2476
|
if self.file == '':
|
|
1819
2477
|
name = self.basename
|
|
1820
2478
|
for nr in self.channels:
|
|
@@ -1822,51 +2480,98 @@ class WriteWAV(TimeOut):
|
|
|
1822
2480
|
name += '.wav'
|
|
1823
2481
|
else:
|
|
1824
2482
|
name = self.file
|
|
2483
|
+
|
|
1825
2484
|
with wave.open(name, 'w') as wf:
|
|
1826
2485
|
wf.setnchannels(nc)
|
|
1827
|
-
wf.setsampwidth(
|
|
1828
|
-
wf.setframerate(
|
|
1829
|
-
wf.setnframes(self.source.num_samples)
|
|
1830
|
-
mx = 0.0
|
|
2486
|
+
wf.setsampwidth(sw)
|
|
2487
|
+
wf.setframerate(fs)
|
|
1831
2488
|
ind = array(self.channels)
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2489
|
+
if self.max_val is None:
|
|
2490
|
+
# compute maximum and remember result to avoid calling source twice
|
|
2491
|
+
if not isinstance(self.source, Cache):
|
|
2492
|
+
self.source = Cache(source=self.source)
|
|
2493
|
+
|
|
2494
|
+
# distinguish cases to use full dynamic range of dtype
|
|
2495
|
+
if dtype == np.dtype('uint8'):
|
|
2496
|
+
mx = 0
|
|
2497
|
+
for data in self.source.result(num):
|
|
2498
|
+
mx = max(abs(data).max(), mx)
|
|
2499
|
+
elif dtype in (np.dtype('int16'), np.dtype('int32')):
|
|
2500
|
+
# for signed integers, we need special treatment because of asymmetry
|
|
2501
|
+
negmax, posmax = 0, 0
|
|
2502
|
+
for data in self.source.result(num):
|
|
2503
|
+
negmax, posmax = max(abs(data.min()), negmax), max(data.max(), posmax)
|
|
2504
|
+
mx = negmax if negmax > posmax else posmax + 1 / dmax # correction for asymmetry
|
|
2505
|
+
else:
|
|
2506
|
+
mx = self.max_val
|
|
1837
2507
|
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2508
|
+
# write scaled data to file
|
|
2509
|
+
for data in self.source.result(num):
|
|
2510
|
+
frames = self._encode(data[:, ind] / mx)
|
|
2511
|
+
wf.writeframes(frames)
|
|
2512
|
+
yield data
|
|
1842
2513
|
|
|
2514
|
+
def save(self):
|
|
2515
|
+
"""
|
|
2516
|
+
Save the entire source output to a WAV file.
|
|
1843
2517
|
|
|
1844
|
-
|
|
2518
|
+
This method writes all available time signal data from the :attr:`source` to the specified
|
|
2519
|
+
WAV file in blocks. It calls the :meth:`result` method internally and discards the yielded
|
|
2520
|
+
data. The file is written according to the current :attr:`channels`, :attr:`encoding`, and
|
|
2521
|
+
scaling settings. If no file name is specified, a name is generated automatically.
|
|
2522
|
+
|
|
2523
|
+
See Also
|
|
2524
|
+
--------
|
|
2525
|
+
:meth:`result` : Generator for writing and yielding data block-wise.
|
|
2526
|
+
"""
|
|
2527
|
+
for _ in self.result(1024):
|
|
2528
|
+
pass
|
|
2529
|
+
|
|
2530
|
+
|
|
2531
|
+
@deprecated_alias(
|
|
2532
|
+
{'name': 'file', 'numsamples_write': 'num_samples_write', 'writeflag': 'write_flag'}, removal_version='25.10'
|
|
2533
|
+
)
|
|
1845
2534
|
class WriteH5(TimeOut):
|
|
1846
|
-
"""
|
|
2535
|
+
"""
|
|
2536
|
+
Saves time signal data as a ``.h5`` (HDF5) file.
|
|
2537
|
+
|
|
2538
|
+
Inherits from :class:`~acoular.base.TimeOut` and provides functionality for saving multi-channel
|
|
2539
|
+
time-domain signal data to an HDF5 file. The file can be written in blocks and supports
|
|
2540
|
+
metadata storage, precision control, and dynamic file generation based on timestamps.
|
|
2541
|
+
|
|
2542
|
+
See Also
|
|
2543
|
+
--------
|
|
2544
|
+
:class:`acoular.base.TimeOut` :
|
|
2545
|
+
ABC for signal processing blocks interacting with data from a source.
|
|
2546
|
+
:class:`acoular.base.SamplesGenerator` :
|
|
2547
|
+
Interface for generating multi-channel time-domain signal processing blocks.
|
|
2548
|
+
h5py :
|
|
2549
|
+
Python library for reading and writing HDF5 files.
|
|
2550
|
+
"""
|
|
1847
2551
|
|
|
1848
|
-
|
|
2552
|
+
#: The input data source. It must be an instance of a
|
|
2553
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
1849
2554
|
source = Instance(SamplesGenerator)
|
|
1850
2555
|
|
|
1851
|
-
|
|
1852
|
-
|
|
2556
|
+
#: The name of the file to be saved. If none is given, the name is automatically
|
|
2557
|
+
#: generated based on the current timestamp.
|
|
1853
2558
|
file = File(filter=['*.h5'], desc='name of data file')
|
|
1854
2559
|
|
|
1855
|
-
|
|
1856
|
-
|
|
2560
|
+
#: The number of samples to write to file per call to `result` method.
|
|
2561
|
+
#: Default is ``-1``, meaning all available data from the source will be written.
|
|
1857
2562
|
num_samples_write = Int(-1)
|
|
1858
2563
|
|
|
1859
|
-
|
|
2564
|
+
#: A flag that can be set to stop file writing. Default is ``True``.
|
|
1860
2565
|
write_flag = Bool(True)
|
|
1861
2566
|
|
|
1862
|
-
|
|
2567
|
+
#: A unique identifier for the object, based on its properties. (read-only)
|
|
1863
2568
|
digest = Property(depends_on=['source.digest'])
|
|
1864
2569
|
|
|
1865
|
-
|
|
1866
|
-
|
|
2570
|
+
#: Precision of the entries in the HDF5 file, represented as numpy data types.
|
|
2571
|
+
#: Default is ``'float32'``.
|
|
1867
2572
|
precision = Enum('float32', 'float64', desc='precision of H5 File')
|
|
1868
2573
|
|
|
1869
|
-
|
|
2574
|
+
#: Metadata to be stored in the HDF5 file.
|
|
1870
2575
|
metadata = Dict(desc='metadata to be stored in .h5 file')
|
|
1871
2576
|
|
|
1872
2577
|
@cached_property
|
|
@@ -1874,11 +2579,28 @@ class WriteH5(TimeOut):
|
|
|
1874
2579
|
return digest(self)
|
|
1875
2580
|
|
|
1876
2581
|
def create_filename(self):
|
|
2582
|
+
"""
|
|
2583
|
+
Generate a filename for the HDF5 file if needed.
|
|
2584
|
+
|
|
2585
|
+
Generate a filename for the HDF5 file based on the current timestamp if no filename is
|
|
2586
|
+
provided. If a filename is provided, it is used as the file name.
|
|
2587
|
+
"""
|
|
1877
2588
|
if self.file == '':
|
|
1878
2589
|
name = datetime.now(tz=timezone.utc).isoformat('_').replace(':', '-').replace('.', '_')
|
|
1879
2590
|
self.file = path.join(config.td_dir, name + '.h5')
|
|
1880
2591
|
|
|
1881
2592
|
def get_initialized_file(self):
|
|
2593
|
+
"""
|
|
2594
|
+
Initialize the HDF5 file and prepare the necessary datasets and metadata.
|
|
2595
|
+
|
|
2596
|
+
This method creates the file (if it doesn't exist), sets up the main data array,
|
|
2597
|
+
and appends metadata to the file.
|
|
2598
|
+
|
|
2599
|
+
Returns
|
|
2600
|
+
-------
|
|
2601
|
+
:class:`h5py.File`
|
|
2602
|
+
The initialized HDF5 file object ready for data insertion.
|
|
2603
|
+
"""
|
|
1882
2604
|
file = _get_h5file_class()
|
|
1883
2605
|
self.create_filename()
|
|
1884
2606
|
f5h = file(self.file, mode='w')
|
|
@@ -1889,7 +2611,18 @@ class WriteH5(TimeOut):
|
|
|
1889
2611
|
return f5h
|
|
1890
2612
|
|
|
1891
2613
|
def save(self):
|
|
1892
|
-
"""
|
|
2614
|
+
"""
|
|
2615
|
+
Save the source output to a HDF5 file.
|
|
2616
|
+
|
|
2617
|
+
This method writes the processed time-domain signal data from the source to the
|
|
2618
|
+
specified HDF5 file. Data is written in blocks and appended to the extendable
|
|
2619
|
+
``'time_data'`` array.
|
|
2620
|
+
|
|
2621
|
+
Notes
|
|
2622
|
+
-----
|
|
2623
|
+
- If no file is specified, a file name is automatically generated.
|
|
2624
|
+
- Metadata defined in the :attr:`metadata` attribute is stored in the file.
|
|
2625
|
+
"""
|
|
1893
2626
|
f5h = self.get_initialized_file()
|
|
1894
2627
|
ac = f5h.get_data_by_reference('time_data')
|
|
1895
2628
|
for data in self.source.result(4096):
|
|
@@ -1897,7 +2630,17 @@ class WriteH5(TimeOut):
|
|
|
1897
2630
|
f5h.close()
|
|
1898
2631
|
|
|
1899
2632
|
def add_metadata(self, f5h):
|
|
1900
|
-
"""
|
|
2633
|
+
"""
|
|
2634
|
+
Add metadata to the HDF5 file.
|
|
2635
|
+
|
|
2636
|
+
Metadata is stored in a separate 'metadata' group within the HDF5 file. The metadata
|
|
2637
|
+
is stored as arrays with each key-value pair corresponding to a separate array.
|
|
2638
|
+
|
|
2639
|
+
Parameters
|
|
2640
|
+
----------
|
|
2641
|
+
f5h : :obj:`h5py.File`
|
|
2642
|
+
The HDF5 file object to which metadata will be added.
|
|
2643
|
+
"""
|
|
1901
2644
|
nitems = len(self.metadata.items())
|
|
1902
2645
|
if nitems > 0:
|
|
1903
2646
|
f5h.create_new_group('metadata', '/')
|
|
@@ -1907,23 +2650,31 @@ class WriteH5(TimeOut):
|
|
|
1907
2650
|
f5h.create_array('/metadata', key, value)
|
|
1908
2651
|
|
|
1909
2652
|
def result(self, num):
|
|
1910
|
-
"""
|
|
1911
|
-
|
|
2653
|
+
"""
|
|
2654
|
+
Python generator that saves source output to an HDF5 file.
|
|
1912
2655
|
|
|
2656
|
+
This method processes data from the source in blocks and writes the data to the HDF5 file.
|
|
2657
|
+
It yields the processed blocks while the data is being written.
|
|
1913
2658
|
|
|
1914
2659
|
Parameters
|
|
1915
2660
|
----------
|
|
1916
|
-
num :
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2661
|
+
num : :obj:`int`
|
|
2662
|
+
Number of samples per block.
|
|
2663
|
+
|
|
2664
|
+
Yields
|
|
2665
|
+
------
|
|
2666
|
+
:obj:`numpy.ndarray`
|
|
2667
|
+
A numpy array of shape (``num``, :attr:`~acoular.base.SamplesGenerator.num_channels`),
|
|
2668
|
+
where :attr:`~acoular.base.SamplesGenerator.num_channels` is inhereted from the
|
|
2669
|
+
:attr:`source`, delivering the processed time-domain signal data.
|
|
2670
|
+
The last block may contain fewer samples if the total number of samples is not
|
|
2671
|
+
a multiple of ``num``.
|
|
2672
|
+
|
|
2673
|
+
Notes
|
|
2674
|
+
-----
|
|
2675
|
+
- If :attr:`num_samples_write` is set to a value other than ``-1``, only that number of
|
|
2676
|
+
samples will be written to the file.
|
|
2677
|
+
- The data is echoed as it is yielded, after being written to the file.
|
|
1927
2678
|
"""
|
|
1928
2679
|
self.write_flag = True
|
|
1929
2680
|
f5h = self.get_initialized_file()
|
|
@@ -1951,27 +2702,47 @@ class WriteH5(TimeOut):
|
|
|
1951
2702
|
|
|
1952
2703
|
|
|
1953
2704
|
class TimeConvolve(TimeOut):
|
|
1954
|
-
"""
|
|
1955
|
-
|
|
1956
|
-
|
|
2705
|
+
"""
|
|
2706
|
+
Perform frequency domain convolution with the uniformly partitioned overlap-save (UPOLS) method.
|
|
2707
|
+
|
|
2708
|
+
This class convolves a source signal with a kernel in the frequency domain. It uses the UPOLS
|
|
2709
|
+
method, which efficiently computes convolutions by processing signal blocks and kernel blocks
|
|
2710
|
+
separately in the frequency domain. For detailed theoretical background,
|
|
2711
|
+
refer to :cite:`Wefers2015`.
|
|
2712
|
+
|
|
2713
|
+
Inherits from :class:`~acoular.base.TimeOut`, which allows the class to process signals
|
|
2714
|
+
generated by a source object. The kernel used for convolution can be one-dimensional or
|
|
2715
|
+
two-dimensional, and it can be applied across one or more channels of the source signal.
|
|
2716
|
+
|
|
2717
|
+
See Also
|
|
2718
|
+
--------
|
|
2719
|
+
:class:`acoular.base.TimeOut` :
|
|
2720
|
+
The parent class for signal processing blocks.
|
|
2721
|
+
:class:`acoular.base.SamplesGenerator` :
|
|
2722
|
+
The interface for generating multi-channel time-domain signals.
|
|
1957
2723
|
"""
|
|
1958
2724
|
|
|
1959
|
-
|
|
2725
|
+
#: The input data source. It must be an instance of a
|
|
2726
|
+
#: :class:`~acoular.base.SamplesGenerator`-derived class.
|
|
1960
2727
|
source = Instance(SamplesGenerator)
|
|
1961
2728
|
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2729
|
+
#: Convolution kernel in the time domain.
|
|
2730
|
+
#: The second dimension of the kernel array has to be either ``1`` or match
|
|
2731
|
+
#: the :attr:`source`'s :attr:`~acoular.base.SamplesGenerator.num_channels` attribute.
|
|
2732
|
+
#: If only a single kernel is supplied, it is applied to all channels.
|
|
1965
2733
|
kernel = CArray(dtype=float, desc='Convolution kernel.')
|
|
1966
2734
|
|
|
2735
|
+
# Internal block size for partitioning signals into smaller segments during processing.
|
|
1967
2736
|
_block_size = Int(desc='Block size')
|
|
1968
2737
|
|
|
2738
|
+
# Blocks of the convolution kernel in the frequency domain.
|
|
2739
|
+
# Computed using Fast Fourier Transform (FFT).
|
|
1969
2740
|
_kernel_blocks = Property(
|
|
1970
2741
|
depends_on=['kernel', '_block_size'],
|
|
1971
2742
|
desc='Frequency domain Kernel blocks',
|
|
1972
2743
|
)
|
|
1973
2744
|
|
|
1974
|
-
|
|
2745
|
+
#: A unique identifier for the object, based on its properties. (read-only)
|
|
1975
2746
|
digest = Property(depends_on=['source.digest', 'kernel'])
|
|
1976
2747
|
|
|
1977
2748
|
@cached_property
|
|
@@ -1979,7 +2750,16 @@ class TimeConvolve(TimeOut):
|
|
|
1979
2750
|
return digest(self)
|
|
1980
2751
|
|
|
1981
2752
|
def _validate_kernel(self):
|
|
1982
|
-
#
|
|
2753
|
+
# Validate the dimensions of the convolution kernel.
|
|
2754
|
+
#
|
|
2755
|
+
# Reshapes the kernel to match the required dimensions for broadcasting. Checks if the
|
|
2756
|
+
# kernel is either one-dimensional or two-dimensional, and ensures that the second dimension
|
|
2757
|
+
# matches the number of channels in the source signal.
|
|
2758
|
+
#
|
|
2759
|
+
# Raises
|
|
2760
|
+
# ------
|
|
2761
|
+
# ValueError
|
|
2762
|
+
# If the kernel's shape is invalid or incompatible with the source signal.
|
|
1983
2763
|
if self.kernel.ndim == 1:
|
|
1984
2764
|
self.kernel = self.kernel.reshape([-1, 1])
|
|
1985
2765
|
return
|
|
@@ -1995,6 +2775,15 @@ class TimeConvolve(TimeOut):
|
|
|
1995
2775
|
# compute the rfft of the kernel blockwise
|
|
1996
2776
|
@cached_property
|
|
1997
2777
|
def _get__kernel_blocks(self):
|
|
2778
|
+
# Compute the frequency-domain blocks of the kernel using the FFT.
|
|
2779
|
+
#
|
|
2780
|
+
# This method splits the kernel into blocks and applies the Fast Fourier Transform (FFT)
|
|
2781
|
+
# to each block. The result is used in the convolution process for efficient computation.
|
|
2782
|
+
#
|
|
2783
|
+
# Returns
|
|
2784
|
+
# -------
|
|
2785
|
+
# :class:`numpy.ndarray`
|
|
2786
|
+
# A 3D array of complex values representing the frequency-domain blocks of the kernel.
|
|
1998
2787
|
[L, N] = self.kernel.shape
|
|
1999
2788
|
num = self._block_size
|
|
2000
2789
|
P = int(ceil(L / num))
|
|
@@ -2012,20 +2801,30 @@ class TimeConvolve(TimeOut):
|
|
|
2012
2801
|
return blocks
|
|
2013
2802
|
|
|
2014
2803
|
def result(self, num=128):
|
|
2015
|
-
"""
|
|
2016
|
-
|
|
2804
|
+
"""
|
|
2805
|
+
Convolve the source signal with the kernel and yield the result in blocks.
|
|
2806
|
+
|
|
2807
|
+
The method generates the convolution of the source signal with the kernel by processing the
|
|
2808
|
+
signal in small blocks, performing the convolution in the frequency domain, and yielding the
|
|
2809
|
+
results block by block.
|
|
2017
2810
|
|
|
2018
2811
|
Parameters
|
|
2019
2812
|
----------
|
|
2020
|
-
num :
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2813
|
+
num : :obj:`int`, optional
|
|
2814
|
+
Number of samples per block.
|
|
2815
|
+
Default is ``128``.
|
|
2816
|
+
|
|
2817
|
+
Yields
|
|
2818
|
+
------
|
|
2819
|
+
:obj:`numpy.ndarray`
|
|
2820
|
+
A array of shape (``num``, :attr:`~acoular.base.SamplesGenerator.num_channels`),
|
|
2821
|
+
where :attr:`~acoular.base.SamplesGenerator.num_channels` is inhereted from the
|
|
2822
|
+
:attr:`source`, representing the convolution result in blocks.
|
|
2823
|
+
|
|
2824
|
+
Notes
|
|
2825
|
+
-----
|
|
2826
|
+
- The kernel is first validated and reshaped if necessary.
|
|
2827
|
+
- The convolution is computed efficiently using the FFT in the frequency domain.
|
|
2029
2828
|
"""
|
|
2030
2829
|
self._validate_kernel()
|
|
2031
2830
|
# initialize variables
|
|
@@ -2097,21 +2896,3 @@ def _spectral_sum(out, fdl, kb): # pragma: no cover
|
|
|
2097
2896
|
out[b, n] += fdl[i, b, n] * kb[i, b, n]
|
|
2098
2897
|
|
|
2099
2898
|
return out
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
class MaskedTimeInOut(MaskedTimeOut):
|
|
2103
|
-
"""Signal processing block for channel and sample selection.
|
|
2104
|
-
|
|
2105
|
-
.. deprecated:: 24.10
|
|
2106
|
-
Using :class:`~acoular.tprocess.MaskedTimeInOut` is deprecated and will be removed in
|
|
2107
|
-
Acoular version 25.07. Use :class:`~acoular.tprocess.MaskedTimeOut` instead.
|
|
2108
|
-
"""
|
|
2109
|
-
|
|
2110
|
-
def __init__(self, *args, **kwargs):
|
|
2111
|
-
super().__init__(*args, **kwargs)
|
|
2112
|
-
warn(
|
|
2113
|
-
'Using MaskedTimeInOut is deprecated and will be removed in Acoular version 25.07. \
|
|
2114
|
-
Use class MaskedTimeOut instead.',
|
|
2115
|
-
DeprecationWarning,
|
|
2116
|
-
stacklevel=2,
|
|
2117
|
-
)
|