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