acoular 24.7__py3-none-any.whl → 25.1__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 +21 -9
- acoular/aiaa/__init__.py +12 -0
- acoular/{tools → aiaa}/aiaa.py +26 -31
- acoular/base.py +332 -0
- acoular/calib.py +129 -34
- acoular/configuration.py +13 -11
- acoular/demo/__init__.py +1 -0
- acoular/demo/acoular_demo.py +30 -17
- acoular/deprecation.py +85 -0
- acoular/environments.py +38 -24
- acoular/fastFuncs.py +90 -84
- acoular/fbeamform.py +342 -387
- acoular/fprocess.py +376 -0
- acoular/grids.py +122 -150
- acoular/h5cache.py +29 -40
- acoular/h5files.py +2 -6
- acoular/microphones.py +50 -59
- acoular/process.py +771 -0
- acoular/sdinput.py +35 -21
- acoular/signals.py +120 -113
- acoular/sources.py +208 -234
- acoular/spectra.py +59 -254
- acoular/tbeamform.py +280 -280
- acoular/tfastfuncs.py +21 -21
- acoular/tools/__init__.py +3 -7
- acoular/tools/helpers.py +218 -4
- acoular/tools/metrics.py +5 -5
- acoular/tools/utils.py +116 -0
- acoular/tprocess.py +416 -741
- acoular/traitsviews.py +15 -13
- acoular/trajectory.py +7 -10
- acoular/version.py +2 -2
- {acoular-24.7.dist-info → acoular-25.1.dist-info}/METADATA +63 -21
- acoular-25.1.dist-info/RECORD +56 -0
- {acoular-24.7.dist-info → acoular-25.1.dist-info}/WHEEL +1 -1
- acoular-24.7.dist-info/RECORD +0 -50
- {acoular-24.7.dist-info → acoular-25.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {acoular-24.7.dist-info → acoular-25.1.dist-info}/licenses/LICENSE +0 -0
acoular/tprocess.py
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
# ------------------------------------------------------------------------------
|
|
2
2
|
# Copyright (c) Acoular Development Team.
|
|
3
3
|
# ------------------------------------------------------------------------------
|
|
4
|
-
"""Implements processing in the time domain.
|
|
4
|
+
"""Implements blockwise processing in the time domain.
|
|
5
5
|
|
|
6
6
|
.. autosummary::
|
|
7
7
|
:toctree: generated/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
TimeInOut
|
|
11
|
-
MaskedTimeInOut
|
|
9
|
+
MaskedTimeOut
|
|
12
10
|
Trigger
|
|
13
11
|
AngleTracker
|
|
14
12
|
ChannelMixer
|
|
@@ -17,7 +15,7 @@
|
|
|
17
15
|
SpatialInterpolatorConstantRotation
|
|
18
16
|
Mixer
|
|
19
17
|
TimePower
|
|
20
|
-
|
|
18
|
+
TimeCumAverage
|
|
21
19
|
TimeReverse
|
|
22
20
|
Filter
|
|
23
21
|
FilterBank
|
|
@@ -26,20 +24,16 @@
|
|
|
26
24
|
TimeExpAverage
|
|
27
25
|
FiltFreqWeight
|
|
28
26
|
OctaveFilterBank
|
|
29
|
-
TimeCache
|
|
30
|
-
TimeCumAverage
|
|
31
27
|
WriteWAV
|
|
32
28
|
WriteH5
|
|
33
|
-
SampleSplitter
|
|
34
29
|
TimeConvolve
|
|
30
|
+
MaskedTimeInOut
|
|
35
31
|
"""
|
|
36
32
|
|
|
37
33
|
# imports from other packages
|
|
38
|
-
import threading
|
|
39
34
|
import wave
|
|
40
|
-
from
|
|
35
|
+
from abc import abstractmethod
|
|
41
36
|
from datetime import datetime, timezone
|
|
42
|
-
from inspect import currentframe
|
|
43
37
|
from os import path
|
|
44
38
|
from warnings import warn
|
|
45
39
|
|
|
@@ -77,158 +71,93 @@ from numpy import (
|
|
|
77
71
|
split,
|
|
78
72
|
sqrt,
|
|
79
73
|
stack,
|
|
80
|
-
sum,
|
|
74
|
+
sum, # noqa: A004
|
|
75
|
+
tile,
|
|
81
76
|
unique,
|
|
82
77
|
zeros,
|
|
83
78
|
)
|
|
84
|
-
from numpy.linalg import norm
|
|
85
|
-
from numpy.matlib import repmat
|
|
86
79
|
from scipy.fft import irfft, rfft
|
|
87
80
|
from scipy.interpolate import CloughTocher2DInterpolator, CubicSpline, LinearNDInterpolator, Rbf, splev, splrep
|
|
81
|
+
from scipy.linalg import norm
|
|
88
82
|
from scipy.signal import bilinear, butter, sosfilt, sosfiltfilt, tf2sos
|
|
89
83
|
from scipy.spatial import Delaunay
|
|
90
84
|
from traits.api import (
|
|
91
85
|
Bool,
|
|
92
86
|
CArray,
|
|
93
|
-
|
|
87
|
+
CInt,
|
|
94
88
|
Constant,
|
|
95
89
|
Delegate,
|
|
96
90
|
Dict,
|
|
91
|
+
Enum,
|
|
97
92
|
File,
|
|
98
93
|
Float,
|
|
99
|
-
HasPrivateTraits,
|
|
100
94
|
Instance,
|
|
101
95
|
Int,
|
|
102
96
|
List,
|
|
103
|
-
|
|
97
|
+
Map,
|
|
104
98
|
Property,
|
|
105
99
|
Str,
|
|
106
|
-
|
|
100
|
+
Union,
|
|
107
101
|
cached_property,
|
|
108
102
|
observe,
|
|
109
103
|
on_trait_change,
|
|
110
104
|
)
|
|
111
105
|
|
|
106
|
+
# acoular imports
|
|
107
|
+
from .base import SamplesGenerator, TimeOut
|
|
112
108
|
from .configuration import config
|
|
109
|
+
from .deprecation import deprecated_alias
|
|
113
110
|
from .environments import cartToCyl, cylToCart
|
|
114
|
-
from .
|
|
115
|
-
from .h5files import H5CacheFileBase, _get_h5file_class
|
|
116
|
-
|
|
117
|
-
# acoular imports
|
|
111
|
+
from .h5files import _get_h5file_class
|
|
118
112
|
from .internal import digest, ldigest
|
|
119
113
|
from .microphones import MicGeom
|
|
114
|
+
from .tools.utils import find_basename
|
|
120
115
|
|
|
121
116
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
It provides a common interface for all SamplesGenerator classes, which
|
|
126
|
-
generate an output via the generator :meth:`result`.
|
|
127
|
-
This class has no real functionality on its own and should not be
|
|
128
|
-
used directly.
|
|
129
|
-
"""
|
|
130
|
-
|
|
131
|
-
#: Sampling frequency of the signal, defaults to 1.0
|
|
132
|
-
sample_freq = Float(1.0, desc='sampling frequency')
|
|
133
|
-
|
|
134
|
-
#: Number of channels
|
|
135
|
-
numchannels = CLong
|
|
136
|
-
|
|
137
|
-
#: Number of samples
|
|
138
|
-
numsamples = CLong
|
|
139
|
-
|
|
140
|
-
# internal identifier
|
|
141
|
-
digest = Property(depends_on=['sample_freq', 'numchannels', 'numsamples'])
|
|
142
|
-
|
|
143
|
-
def _get_digest(self):
|
|
144
|
-
return digest(self)
|
|
145
|
-
|
|
146
|
-
def result(self, num):
|
|
147
|
-
"""Python generator that yields the output block-wise.
|
|
148
|
-
|
|
149
|
-
Parameters
|
|
150
|
-
----------
|
|
151
|
-
num : integer
|
|
152
|
-
This parameter defines the size of the blocks to be yielded
|
|
153
|
-
(i.e. the number of samples per block)
|
|
154
|
-
|
|
155
|
-
Returns
|
|
156
|
-
-------
|
|
157
|
-
No output since `SamplesGenerator` only represents a base class to derive
|
|
158
|
-
other classes from.
|
|
159
|
-
|
|
160
|
-
"""
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
class TimeInOut(SamplesGenerator):
|
|
164
|
-
"""Base class for any time domain signal processing block,
|
|
165
|
-
gets samples from :attr:`source` and generates output via the
|
|
166
|
-
generator :meth:`result`.
|
|
167
|
-
"""
|
|
168
|
-
|
|
169
|
-
#: Data source; :class:`~acoular.sources.SamplesGenerator` or derived object.
|
|
170
|
-
source = Trait(SamplesGenerator)
|
|
171
|
-
|
|
172
|
-
#: Sampling frequency of output signal, as given by :attr:`source`.
|
|
173
|
-
sample_freq = Delegate('source')
|
|
174
|
-
|
|
175
|
-
#: Number of channels in output, as given by :attr:`source`.
|
|
176
|
-
numchannels = Delegate('source')
|
|
177
|
-
|
|
178
|
-
#: Number of samples in output, as given by :attr:`source`.
|
|
179
|
-
numsamples = Delegate('source')
|
|
180
|
-
|
|
181
|
-
# internal identifier
|
|
182
|
-
digest = Property(depends_on=['source.digest'])
|
|
183
|
-
|
|
184
|
-
@cached_property
|
|
185
|
-
def _get_digest(self):
|
|
186
|
-
return digest(self)
|
|
187
|
-
|
|
188
|
-
def result(self, num):
|
|
189
|
-
"""Python generator: dummy function, just echoes the output of source,
|
|
190
|
-
yields samples in blocks of shape (num, :attr:`numchannels`), the last block
|
|
191
|
-
may be shorter than num.
|
|
192
|
-
"""
|
|
193
|
-
yield from self.source.result(num)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
class MaskedTimeInOut(TimeInOut):
|
|
117
|
+
@deprecated_alias({'numchannels_total': 'num_channels_total', 'numsamples_total': 'num_samples_total'})
|
|
118
|
+
class MaskedTimeOut(TimeOut):
|
|
197
119
|
"""Signal processing block for channel and sample selection.
|
|
198
120
|
|
|
199
121
|
This class serves as intermediary to define (in)valid
|
|
200
122
|
channels and samples for any
|
|
201
123
|
:class:`~acoular.sources.SamplesGenerator` (or derived) object.
|
|
202
|
-
It gets samples from :attr:`~acoular.
|
|
124
|
+
It gets samples from :attr:`~acoular.base.TimeOut.source`
|
|
203
125
|
and generates output via the generator :meth:`result`.
|
|
204
126
|
"""
|
|
205
127
|
|
|
206
|
-
|
|
207
|
-
|
|
128
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
129
|
+
source = Instance(SamplesGenerator)
|
|
130
|
+
|
|
131
|
+
# Index of the first sample to be considered valid.
|
|
132
|
+
start = CInt(0, desc='start of valid samples')
|
|
208
133
|
|
|
209
|
-
|
|
210
|
-
stop =
|
|
134
|
+
# Index of the last sample to be considered valid.
|
|
135
|
+
stop = Union(None, CInt, desc='stop of valid samples')
|
|
211
136
|
|
|
212
|
-
|
|
213
|
-
invalid_channels =
|
|
137
|
+
# Channels that are to be treated as invalid.
|
|
138
|
+
invalid_channels = List(int, desc='list of invalid channels')
|
|
214
139
|
|
|
215
|
-
|
|
216
|
-
channels = Property(depends_on=['invalid_channels', 'source.
|
|
140
|
+
# Channel mask to serve as an index for all valid channels, is set automatically.
|
|
141
|
+
channels = Property(depends_on=['invalid_channels', 'source.num_channels'], desc='channel mask')
|
|
217
142
|
|
|
218
|
-
|
|
219
|
-
|
|
143
|
+
# Number of channels in input, as given by :attr:`~acoular.base.TimeOut.source`.
|
|
144
|
+
num_channels_total = Delegate('source', 'num_channels')
|
|
220
145
|
|
|
221
|
-
|
|
222
|
-
|
|
146
|
+
# Number of samples in input, as given by :attr:`~acoular.base.TimeOut.source`.
|
|
147
|
+
num_samples_total = Delegate('source', 'num_samples')
|
|
223
148
|
|
|
224
|
-
|
|
225
|
-
|
|
149
|
+
# Number of valid channels, is set automatically.
|
|
150
|
+
num_channels = Property(
|
|
151
|
+
depends_on=['invalid_channels', 'source.num_channels'], desc='number of valid input channels'
|
|
152
|
+
)
|
|
226
153
|
|
|
227
|
-
|
|
228
|
-
|
|
154
|
+
# Number of valid time samples, is set automatically.
|
|
155
|
+
num_samples = Property(
|
|
156
|
+
depends_on=['start', 'stop', 'source.num_samples'], desc='number of valid samples per channel'
|
|
157
|
+
)
|
|
229
158
|
|
|
230
|
-
|
|
231
|
-
basename = Property(depends_on='source.digest', desc='basename for cache file')
|
|
159
|
+
# Name of the cache file without extension, readonly.
|
|
160
|
+
basename = Property(depends_on=['source.digest'], desc='basename for cache file')
|
|
232
161
|
|
|
233
162
|
# internal identifier
|
|
234
163
|
digest = Property(depends_on=['source.digest', 'start', 'stop', 'invalid_channels'])
|
|
@@ -239,26 +168,32 @@ class MaskedTimeInOut(TimeInOut):
|
|
|
239
168
|
|
|
240
169
|
@cached_property
|
|
241
170
|
def _get_basename(self):
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
171
|
+
warn(
|
|
172
|
+
(
|
|
173
|
+
f'The basename attribute of a {self.__class__.__name__} object is deprecated'
|
|
174
|
+
' and will be removed in a future release!'
|
|
175
|
+
),
|
|
176
|
+
DeprecationWarning,
|
|
177
|
+
stacklevel=2,
|
|
178
|
+
)
|
|
179
|
+
return find_basename(self.source, alternative_basename=self.source.__class__.__name__ + self.source.digest)
|
|
245
180
|
|
|
246
181
|
@cached_property
|
|
247
182
|
def _get_channels(self):
|
|
248
183
|
if len(self.invalid_channels) == 0:
|
|
249
184
|
return slice(0, None, None)
|
|
250
|
-
allr = [i for i in range(self.
|
|
185
|
+
allr = [i for i in range(self.num_channels_total) if i not in self.invalid_channels]
|
|
251
186
|
return array(allr)
|
|
252
187
|
|
|
253
188
|
@cached_property
|
|
254
|
-
def
|
|
189
|
+
def _get_num_channels(self):
|
|
255
190
|
if len(self.invalid_channels) == 0:
|
|
256
|
-
return self.
|
|
191
|
+
return self.num_channels_total
|
|
257
192
|
return len(self.channels)
|
|
258
193
|
|
|
259
194
|
@cached_property
|
|
260
|
-
def
|
|
261
|
-
sli = slice(self.start, self.stop).indices(self.
|
|
195
|
+
def _get_num_samples(self):
|
|
196
|
+
sli = slice(self.start, self.stop).indices(self.num_samples_total)
|
|
262
197
|
return sli[1] - sli[0]
|
|
263
198
|
|
|
264
199
|
def result(self, num):
|
|
@@ -272,22 +207,22 @@ class MaskedTimeInOut(TimeInOut):
|
|
|
272
207
|
|
|
273
208
|
Returns
|
|
274
209
|
-------
|
|
275
|
-
Samples in blocks of shape (num, :attr:`
|
|
210
|
+
Samples in blocks of shape (num, :attr:`num_channels`).
|
|
276
211
|
The last block may be shorter than num.
|
|
277
212
|
|
|
278
213
|
"""
|
|
279
|
-
sli = slice(self.start, self.stop).indices(self.
|
|
214
|
+
sli = slice(self.start, self.stop).indices(self.num_samples_total)
|
|
280
215
|
start = sli[0]
|
|
281
216
|
stop = sli[1]
|
|
282
217
|
if start >= stop:
|
|
283
218
|
msg = 'no samples available'
|
|
284
219
|
raise OSError(msg)
|
|
285
220
|
|
|
286
|
-
if start != 0 or stop != self.
|
|
221
|
+
if start != 0 or stop != self.num_samples_total:
|
|
287
222
|
offset = -start % num
|
|
288
223
|
if offset == 0:
|
|
289
224
|
offset = num
|
|
290
|
-
buf = empty((num + offset, self.
|
|
225
|
+
buf = empty((num + offset, self.num_channels), dtype=float)
|
|
291
226
|
bsize = 0
|
|
292
227
|
i = 0
|
|
293
228
|
fblock = True
|
|
@@ -323,16 +258,19 @@ class MaskedTimeInOut(TimeInOut):
|
|
|
323
258
|
yield block[:, self.channels]
|
|
324
259
|
|
|
325
260
|
|
|
326
|
-
class ChannelMixer(
|
|
261
|
+
class ChannelMixer(TimeOut):
|
|
327
262
|
"""Class for directly mixing the channels of a multi-channel source.
|
|
328
263
|
Outputs a single channel.
|
|
329
264
|
"""
|
|
330
265
|
|
|
331
|
-
|
|
266
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
267
|
+
source = Instance(SamplesGenerator)
|
|
268
|
+
|
|
269
|
+
# Amplitude weight(s) for the channels as array. If not set, all channels are equally weighted.
|
|
332
270
|
weights = CArray(desc='channel weights')
|
|
333
271
|
|
|
334
272
|
# Number of channels is always one here.
|
|
335
|
-
|
|
273
|
+
num_channels = Constant(1)
|
|
336
274
|
|
|
337
275
|
# internal identifier
|
|
338
276
|
digest = Property(depends_on=['source.digest', 'weights'])
|
|
@@ -357,10 +295,10 @@ class ChannelMixer(TimeInOut):
|
|
|
357
295
|
|
|
358
296
|
"""
|
|
359
297
|
if self.weights.size:
|
|
360
|
-
if self.weights.shape in {(self.source.
|
|
298
|
+
if self.weights.shape in {(self.source.num_channels,), (1,)}:
|
|
361
299
|
weights = self.weights
|
|
362
300
|
else:
|
|
363
|
-
msg = f'Weight factors can not be broadcasted: {self.weights.shape}, {(self.source.
|
|
301
|
+
msg = f'Weight factors can not be broadcasted: {self.weights.shape}, {(self.source.num_channels,)}'
|
|
364
302
|
raise ValueError(msg)
|
|
365
303
|
else:
|
|
366
304
|
weights = 1
|
|
@@ -369,7 +307,7 @@ class ChannelMixer(TimeInOut):
|
|
|
369
307
|
yield sum(weights * block, 1, keepdims=True)
|
|
370
308
|
|
|
371
309
|
|
|
372
|
-
class Trigger(
|
|
310
|
+
class Trigger(TimeOut): # pragma: no cover
|
|
373
311
|
"""Class for identifying trigger signals.
|
|
374
312
|
Gets samples from :attr:`source` and stores the trigger samples in :meth:`trigger_data`.
|
|
375
313
|
|
|
@@ -383,59 +321,56 @@ class Trigger(TimeInOut):
|
|
|
383
321
|
vary too much.
|
|
384
322
|
"""
|
|
385
323
|
|
|
386
|
-
|
|
324
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
387
325
|
source = Instance(SamplesGenerator)
|
|
388
326
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
327
|
+
# Threshold of trigger. Has different meanings for different
|
|
328
|
+
# :attr:`~acoular.tprocess.Trigger.trigger_type`. The sign is relevant.
|
|
329
|
+
# If a sample of the signal is above/below the positive/negative threshold,
|
|
330
|
+
# it is assumed to be a peak.
|
|
331
|
+
# Default is None, in which case a first estimate is used: The threshold
|
|
332
|
+
# is assumed to be 75% of the max/min difference between all extremums and the
|
|
333
|
+
# mean value of the trigger signal. E.g: the mean value is 0 and there are positive
|
|
334
|
+
# extremums at 400 and negative extremums at -800. Then the estimated threshold would be
|
|
335
|
+
# 0.75 * -800 = -600.
|
|
398
336
|
threshold = Float(None)
|
|
399
337
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
338
|
+
# Maximum allowable variation of length of each revolution duration. Default is
|
|
339
|
+
# 2%. A warning is thrown, if any revolution length surpasses this value:
|
|
340
|
+
# abs(durationEachRev - meanDuration) > 0.02 * meanDuration
|
|
403
341
|
max_variation_of_duration = Float(0.02)
|
|
404
342
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
343
|
+
# Defines the length of hunks via lenHunk = hunk_length * maxOncePerRevDuration.
|
|
344
|
+
# If there are multiple peaks within lenHunk, then the algorithm will
|
|
345
|
+
# cancel all but one out (see :attr:`~acoular.tprocess.Trigger.multiple_peaks_in_hunk`).
|
|
346
|
+
# Default is to 0.1.
|
|
409
347
|
hunk_length = Float(0.1)
|
|
410
348
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
#: 2.: -maximum of number of samples between adjacent trigger samples
|
|
437
|
-
#:
|
|
438
|
-
#: 3.: -minimum of number of samples between adjacent trigger samples
|
|
349
|
+
# Type of trigger.
|
|
350
|
+
#
|
|
351
|
+
# 'dirac': a single pulse is assumed (sign of :attr:`~acoular.tprocess.Trigger.trigger_type` is
|
|
352
|
+
# important). Sample will trigger if its value is above/below the pos/neg threshold.
|
|
353
|
+
#
|
|
354
|
+
# 'rect' : repeating rectangular functions. Only every second edge is assumed to be a trigger.
|
|
355
|
+
# The sign of :attr:`~acoular.tprocess.Trigger.trigger_type` gives information on which edge
|
|
356
|
+
# should be used (+ for rising edge, - for falling edge). Sample will trigger if the difference
|
|
357
|
+
# between its value and its predecessors value is above/below the pos/neg threshold.
|
|
358
|
+
#
|
|
359
|
+
# Default is 'dirac'.
|
|
360
|
+
trigger_type = Enum('dirac', 'rect')
|
|
361
|
+
|
|
362
|
+
# Identifier which peak to consider, if there are multiple peaks in one hunk : (see
|
|
363
|
+
# :attr:`~acoular.tprocess.Trigger.hunk_length`). Default is to 'extremum', : in which case the
|
|
364
|
+
# extremal peak (maximum if threshold > 0, minimum if threshold < 0) is considered.
|
|
365
|
+
multiple_peaks_in_hunk = Enum('extremum', 'first')
|
|
366
|
+
|
|
367
|
+
# Tuple consisting of 3 entries:
|
|
368
|
+
#
|
|
369
|
+
# 1.: -Vector with the sample indices of the 1/Rev trigger samples
|
|
370
|
+
#
|
|
371
|
+
# 2.: -maximum of number of samples between adjacent trigger samples
|
|
372
|
+
#
|
|
373
|
+
# 3.: -minimum of number of samples between adjacent trigger samples
|
|
439
374
|
trigger_data = Property(
|
|
440
375
|
depends_on=[
|
|
441
376
|
'source.digest',
|
|
@@ -513,8 +448,8 @@ class Trigger(TimeInOut):
|
|
|
513
448
|
faultyInd = flatnonzero(diffDist > self.max_variation_of_duration * meanDist)
|
|
514
449
|
if faultyInd.size != 0:
|
|
515
450
|
warn(
|
|
516
|
-
'In Trigger-Identification: The distances between the peaks (and
|
|
517
|
-
|
|
451
|
+
f'In Trigger-Identification: The distances between the peaks (and therefore the lengths of the \
|
|
452
|
+
revolutions) vary too much (check samples {peakLoc[faultyInd] + self.source.start}).',
|
|
518
453
|
Warning,
|
|
519
454
|
stacklevel=2,
|
|
520
455
|
)
|
|
@@ -527,7 +462,8 @@ class Trigger(TimeInOut):
|
|
|
527
462
|
def _trigger_rect(self, x0, x, threshold):
|
|
528
463
|
# x0 stores the last value of the the last generator cycle
|
|
529
464
|
xNew = append(x0, x)
|
|
530
|
-
# indPeakHunk = abs(xNew[1:] - xNew[:-1]) > abs(threshold)
|
|
465
|
+
# indPeakHunk = abs(xNew[1:] - xNew[:-1]) > abs(threshold)
|
|
466
|
+
# with above line, every edge would be located
|
|
531
467
|
return self._trigger_value_comp(xNew[1:] - xNew[:-1], threshold)
|
|
532
468
|
|
|
533
469
|
def _trigger_value_comp(self, trigger_data, threshold):
|
|
@@ -551,30 +487,33 @@ class Trigger(TimeInOut):
|
|
|
551
487
|
maxTriggerHelp = [minVal, maxVal] - meanVal
|
|
552
488
|
argInd = argmax(abs(maxTriggerHelp))
|
|
553
489
|
thresh = maxTriggerHelp[argInd] * 0.75 # 0.75 for 75% of max trigger signal
|
|
554
|
-
warn('No threshold was passed. An estimated threshold of
|
|
490
|
+
warn(f'No threshold was passed. An estimated threshold of {thresh} is assumed.', Warning, stacklevel=2)
|
|
555
491
|
else: # take user defined threshold
|
|
556
492
|
thresh = self.threshold
|
|
557
493
|
return thresh
|
|
558
494
|
|
|
559
495
|
def _check_trigger_existence(self):
|
|
560
|
-
nChannels = self.source.
|
|
561
|
-
if
|
|
562
|
-
|
|
496
|
+
nChannels = self.source.num_channels
|
|
497
|
+
if nChannels != 1:
|
|
498
|
+
msg = f'Trigger signal must consist of ONE channel, instead {nChannels} channels are given!'
|
|
499
|
+
raise Exception(msg)
|
|
563
500
|
return 0
|
|
564
501
|
|
|
502
|
+
def result(self, num):
|
|
503
|
+
msg = 'result method not implemented yet! Data from source will be passed without transformation.'
|
|
504
|
+
warn(msg, Warning, stacklevel=2)
|
|
505
|
+
yield from self.source.result(num)
|
|
506
|
+
|
|
565
507
|
|
|
566
|
-
class AngleTracker(
|
|
508
|
+
class AngleTracker(MaskedTimeOut):
|
|
567
509
|
"""Calculates rotation angle and rpm per sample from a trigger signal
|
|
568
510
|
using spline interpolation in the time domain.
|
|
569
511
|
|
|
570
|
-
Gets samples from :attr:`trigger` and stores the angle and rpm samples in :meth:`angle` and
|
|
571
|
-
|
|
512
|
+
Gets samples from :attr:`trigger` and stores the angle and rpm samples in :meth:`angle` and
|
|
513
|
+
:meth:`rpm`.
|
|
572
514
|
"""
|
|
573
515
|
|
|
574
|
-
|
|
575
|
-
source = Instance(SamplesGenerator)
|
|
576
|
-
|
|
577
|
-
#: Trigger data from :class:`acoular.tprocess.Trigger`.
|
|
516
|
+
# Trigger data from :class:`acoular.tprocess.Trigger`.
|
|
578
517
|
trigger = Instance(Trigger)
|
|
579
518
|
|
|
580
519
|
# internal identifier
|
|
@@ -589,29 +528,29 @@ class AngleTracker(MaskedTimeInOut):
|
|
|
589
528
|
],
|
|
590
529
|
)
|
|
591
530
|
|
|
592
|
-
|
|
593
|
-
|
|
531
|
+
# Trigger signals per revolution,
|
|
532
|
+
# defaults to 1.
|
|
594
533
|
trigger_per_revo = Int(1, desc='trigger signals per revolution')
|
|
595
534
|
|
|
596
|
-
|
|
597
|
-
|
|
535
|
+
# Flag to set counter-clockwise (1) or clockwise (-1) rotation,
|
|
536
|
+
# defaults to -1.
|
|
598
537
|
rot_direction = Int(-1, desc='mathematical direction of rotation')
|
|
599
538
|
|
|
600
|
-
|
|
601
|
-
|
|
539
|
+
# Points of interpolation used for spline,
|
|
540
|
+
# defaults to 4.
|
|
602
541
|
interp_points = Int(4, desc='Points of interpolation used for spline')
|
|
603
542
|
|
|
604
|
-
|
|
543
|
+
# rotation angle in radians for first trigger position
|
|
605
544
|
start_angle = Float(0, desc='rotation angle for trigger position')
|
|
606
545
|
|
|
607
|
-
|
|
608
|
-
rpm = Property(depends_on='digest', desc='revolutions per minute for each sample')
|
|
546
|
+
# revolutions per minute for each sample, read-only
|
|
547
|
+
rpm = Property(depends_on=['digest'], desc='revolutions per minute for each sample')
|
|
609
548
|
|
|
610
|
-
|
|
611
|
-
average_rpm = Property(depends_on='digest', desc='average revolutions per minute')
|
|
549
|
+
# average revolutions per minute, read-only
|
|
550
|
+
average_rpm = Property(depends_on=['digest'], desc='average revolutions per minute')
|
|
612
551
|
|
|
613
|
-
|
|
614
|
-
angle = Property(depends_on='digest', desc='rotation angle for each sample')
|
|
552
|
+
# rotation angle in radians for each sample, read-only
|
|
553
|
+
angle = Property(depends_on=['digest'], desc='rotation angle for each sample')
|
|
615
554
|
|
|
616
555
|
# Internal flag to determine whether rpm and angle calculation has been processed,
|
|
617
556
|
# prevents recalculation
|
|
@@ -642,17 +581,17 @@ class AngleTracker(MaskedTimeInOut):
|
|
|
642
581
|
# init
|
|
643
582
|
ind = 0
|
|
644
583
|
# trigger data
|
|
645
|
-
peakloc, maxdist, mindist = self.trigger.trigger_data
|
|
584
|
+
peakloc, maxdist, mindist = self.trigger.trigger_data
|
|
646
585
|
TriggerPerRevo = self.trigger_per_revo
|
|
647
586
|
rotDirection = self.rot_direction
|
|
648
|
-
num = self.source.
|
|
587
|
+
num = self.source.num_samples
|
|
649
588
|
samplerate = self.source.sample_freq
|
|
650
589
|
self._rpm = zeros(num)
|
|
651
590
|
self._angle = zeros(num)
|
|
652
591
|
# number of spline points
|
|
653
592
|
InterpPoints = self.interp_points
|
|
654
593
|
|
|
655
|
-
# loop over
|
|
594
|
+
# loop over all timesamples
|
|
656
595
|
while ind < num:
|
|
657
596
|
# when starting spline forward
|
|
658
597
|
if ind < peakloc[InterpPoints]:
|
|
@@ -716,21 +655,24 @@ class AngleTracker(MaskedTimeInOut):
|
|
|
716
655
|
|
|
717
656
|
"""
|
|
718
657
|
# trigger indices data
|
|
719
|
-
peakloc = self.trigger.trigger_data
|
|
658
|
+
peakloc = self.trigger.trigger_data[0]
|
|
720
659
|
# calculation of average rpm in 1/min
|
|
721
660
|
return (len(peakloc) - 1) / (peakloc[-1] - peakloc[0]) / self.trigger_per_revo * self.source.sample_freq * 60
|
|
722
661
|
|
|
723
662
|
|
|
724
|
-
class SpatialInterpolator(
|
|
663
|
+
class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
725
664
|
"""Base class for spatial interpolation of microphone data.
|
|
726
665
|
Gets samples from :attr:`source` and generates output via the
|
|
727
666
|
generator :meth:`result`.
|
|
728
667
|
"""
|
|
729
668
|
|
|
730
|
-
|
|
669
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
670
|
+
source = Instance(SamplesGenerator)
|
|
671
|
+
|
|
672
|
+
# :class:`~acoular.microphones.MicGeom` object that provides the real microphone locations.
|
|
731
673
|
mics = Instance(MicGeom(), desc='microphone geometry')
|
|
732
674
|
|
|
733
|
-
|
|
675
|
+
# :class:`~acoular.microphones.MicGeom` object that provides the virtual microphone locations.
|
|
734
676
|
mics_virtual = Property(desc='microphone geometry')
|
|
735
677
|
|
|
736
678
|
_mics_virtual = Instance(MicGeom, desc='internal microphone geometry;internal usage, read only')
|
|
@@ -743,15 +685,12 @@ class SpatialInterpolator(TimeInOut):
|
|
|
743
685
|
def _set_mics_virtual(self, mics_virtual):
|
|
744
686
|
self._mics_virtual = mics_virtual
|
|
745
687
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
#: rbf is scipy radial basis function with multiquadric, cubic and sinc functions
|
|
753
|
-
#: idw refers to the inverse distance weighting algorithm
|
|
754
|
-
method = Trait(
|
|
688
|
+
# Interpolation method in spatial domain, defaults to linear
|
|
689
|
+
# linear uses numpy linear interpolation
|
|
690
|
+
# spline uses scipy CloughTocher algorithm
|
|
691
|
+
# rbf is scipy radial basis function with multiquadric, cubic and sinc functions
|
|
692
|
+
# idw refers to the inverse distance weighting algorithm
|
|
693
|
+
method = Enum(
|
|
755
694
|
'linear',
|
|
756
695
|
'spline',
|
|
757
696
|
'rbf-multiquadric',
|
|
@@ -762,42 +701,41 @@ class SpatialInterpolator(TimeInOut):
|
|
|
762
701
|
desc='method for interpolation used',
|
|
763
702
|
)
|
|
764
703
|
|
|
765
|
-
|
|
766
|
-
array_dimension =
|
|
704
|
+
# spatial dimensionality of the array geometry
|
|
705
|
+
array_dimension = Enum('1D', '2D', 'ring', '3D', 'custom', desc='spatial dimensionality of the array geometry')
|
|
767
706
|
|
|
768
|
-
|
|
707
|
+
# Sampling frequency of output signal, as given by :attr:`source`.
|
|
769
708
|
sample_freq = Delegate('source', 'sample_freq')
|
|
770
709
|
|
|
771
|
-
|
|
772
|
-
|
|
710
|
+
# Number of channels in output.
|
|
711
|
+
num_channels = Property()
|
|
773
712
|
|
|
774
|
-
|
|
775
|
-
|
|
713
|
+
# Number of samples in output, as given by :attr:`source`.
|
|
714
|
+
num_samples = Delegate('source', 'num_samples')
|
|
776
715
|
|
|
777
|
-
|
|
716
|
+
# Interpolate a point at the origin of the Array geometry
|
|
778
717
|
interp_at_zero = Bool(False)
|
|
779
718
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
719
|
+
# The rotation must be around the z-axis, which means from x to y axis.
|
|
720
|
+
# If the coordinates are not build like that, than this 3x3 orthogonal
|
|
721
|
+
# transformation matrix Q can be used to modify the coordinates.
|
|
722
|
+
# It is assumed that with the modified coordinates the rotation is around the z-axis.
|
|
723
|
+
# The transformation is done via [x,y,z]_mod = Q * [x,y,z]. (default is Identity).
|
|
785
724
|
Q = CArray(dtype=float64, shape=(3, 3), value=identity(3))
|
|
786
725
|
|
|
787
|
-
num_IDW =
|
|
726
|
+
num_IDW = Int(3, desc='number of neighboring microphones, DEFAULT=3') # noqa: N815
|
|
788
727
|
|
|
789
|
-
p_weight =
|
|
728
|
+
p_weight = Float(
|
|
790
729
|
2,
|
|
791
|
-
dtype=float,
|
|
792
730
|
desc='used in interpolation for virtual microphone, weighting power exponent for IDW',
|
|
793
731
|
)
|
|
794
732
|
|
|
795
|
-
|
|
733
|
+
# Stores the output of :meth:`_virtNewCoord_func`; Read-Only
|
|
796
734
|
_virtNewCoord_func = Property( # noqa: N815
|
|
797
735
|
depends_on=['mics.digest', 'mics_virtual.digest', 'method', 'array_dimension', 'interp_at_zero'],
|
|
798
736
|
)
|
|
799
737
|
|
|
800
|
-
|
|
738
|
+
# internal identifier
|
|
801
739
|
digest = Property(
|
|
802
740
|
depends_on=[
|
|
803
741
|
'mics.digest',
|
|
@@ -810,7 +748,7 @@ class SpatialInterpolator(TimeInOut):
|
|
|
810
748
|
],
|
|
811
749
|
)
|
|
812
750
|
|
|
813
|
-
def
|
|
751
|
+
def _get_num_channels(self):
|
|
814
752
|
return self.mics_virtual.num_mics
|
|
815
753
|
|
|
816
754
|
@cached_property
|
|
@@ -842,26 +780,30 @@ class SpatialInterpolator(TimeInOut):
|
|
|
842
780
|
Returns
|
|
843
781
|
-------
|
|
844
782
|
mesh : List[]
|
|
845
|
-
The items of these lists
|
|
783
|
+
The items of these lists depend on the reduced interpolation dimension of each subarray.
|
|
846
784
|
If the Array is 1D the list items are:
|
|
847
785
|
1. item : float64[nMicsInSpecificSubarray]
|
|
848
|
-
Ordered positions of the real mics on the new 1d axis,
|
|
786
|
+
Ordered positions of the real mics on the new 1d axis,
|
|
787
|
+
to be used as inputs for numpys interp.
|
|
849
788
|
2. item : int64[nMicsInArray]
|
|
850
|
-
Indices identifying how the measured pressures must be evaluated, s.t. the
|
|
851
|
-
correspond to their initial
|
|
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.
|
|
852
792
|
If the Array is 2D or 3d the list items are:
|
|
853
793
|
1. item : Delaunay mesh object
|
|
854
|
-
|
|
794
|
+
Delaunay mesh (see scipy.spatial.Delaunay) for the specific Array
|
|
855
795
|
2. item : int64[nMicsInArray]
|
|
856
|
-
same as 1d case, BUT with the difference, that here the rotational
|
|
857
|
-
|
|
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.
|
|
858
799
|
|
|
859
800
|
virtNewCoord : float64[3, nVirtualMics]
|
|
860
|
-
Projection of each virtual mic onto its new coordinates. The columns of virtNewCoord
|
|
801
|
+
Projection of each virtual mic onto its new coordinates. The columns of virtNewCoord
|
|
802
|
+
correspond to [phi, rho, z].
|
|
861
803
|
|
|
862
804
|
newCoord : float64[3, nMics]
|
|
863
|
-
Projection of each mic onto its new coordinates. The columns of newCoordinates
|
|
864
|
-
|
|
805
|
+
Projection of each mic onto its new coordinates. The columns of newCoordinates
|
|
806
|
+
correspond to [phi, rho, z].
|
|
865
807
|
"""
|
|
866
808
|
# init positions of virtual mics in cyl coordinates
|
|
867
809
|
nVirtMics = mpos_virt.shape[1]
|
|
@@ -1017,12 +959,12 @@ class SpatialInterpolator(TimeInOut):
|
|
|
1017
959
|
if self.array_dimension == '1D' or self.array_dimension == 'ring':
|
|
1018
960
|
# for rotation add phi_delay
|
|
1019
961
|
if not array_equal(phi_delay, []):
|
|
1020
|
-
xInterpHelp =
|
|
962
|
+
xInterpHelp = tile(virtNewCoord[0, :], (nTime, 1)) + tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1021
963
|
xInterp = ((xInterpHelp + pi) % (2 * pi)) - pi # shifting phi cootrdinate into feasible area [-pi, pi]
|
|
1022
964
|
# if no rotation given
|
|
1023
965
|
else:
|
|
1024
|
-
xInterp =
|
|
1025
|
-
# get ordered microphone
|
|
966
|
+
xInterp = tile(virtNewCoord[0, :], (nTime, 1))
|
|
967
|
+
# get ordered microphone positions in radiant
|
|
1026
968
|
x = newCoord[0]
|
|
1027
969
|
for cntTime in range(nTime):
|
|
1028
970
|
if self.method == 'linear':
|
|
@@ -1075,10 +1017,10 @@ class SpatialInterpolator(TimeInOut):
|
|
|
1075
1017
|
elif self.array_dimension == '2D':
|
|
1076
1018
|
# check rotation
|
|
1077
1019
|
if not array_equal(phi_delay, []):
|
|
1078
|
-
xInterpHelp =
|
|
1020
|
+
xInterpHelp = tile(virtNewCoord[0, :], (nTime, 1)) + tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1079
1021
|
xInterp = ((xInterpHelp + pi) % (2 * pi)) - pi # shifting phi cootrdinate into feasible area [-pi, pi]
|
|
1080
1022
|
else:
|
|
1081
|
-
xInterp =
|
|
1023
|
+
xInterp = tile(virtNewCoord[0, :], (nTime, 1))
|
|
1082
1024
|
|
|
1083
1025
|
mesh = meshList[0][0]
|
|
1084
1026
|
for cntTime in range(nTime):
|
|
@@ -1134,10 +1076,10 @@ class SpatialInterpolator(TimeInOut):
|
|
|
1134
1076
|
# using inverse distance weighting
|
|
1135
1077
|
elif self.method == 'IDW':
|
|
1136
1078
|
newPoint2_M = newPoint.T
|
|
1137
|
-
newPoint3_M = append(newPoint2_M, zeros([1, self.
|
|
1079
|
+
newPoint3_M = append(newPoint2_M, zeros([1, self.num_channels]), axis=0)
|
|
1138
1080
|
newPointCart = cylToCart(newPoint3_M)
|
|
1139
1081
|
for ind in arange(len(newPoint[:, 0])):
|
|
1140
|
-
newPoint_Rep =
|
|
1082
|
+
newPoint_Rep = tile(newPointCart[:, ind], (len(newPoint[:, 0]), 1)).T
|
|
1141
1083
|
subtract = newPoint_Rep - newCoordCart
|
|
1142
1084
|
normDistance = norm(subtract, axis=0)
|
|
1143
1085
|
index_norm = argsort(normDistance)[: self.num_IDW]
|
|
@@ -1154,10 +1096,10 @@ class SpatialInterpolator(TimeInOut):
|
|
|
1154
1096
|
elif self.array_dimension == '3D':
|
|
1155
1097
|
# check rotation
|
|
1156
1098
|
if not array_equal(phi_delay, []):
|
|
1157
|
-
xInterpHelp =
|
|
1099
|
+
xInterpHelp = tile(virtNewCoord[0, :], (nTime, 1)) + tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1158
1100
|
xInterp = ((xInterpHelp + pi) % (2 * pi)) - pi # shifting phi cootrdinate into feasible area [-pi, pi]
|
|
1159
1101
|
else:
|
|
1160
|
-
xInterp =
|
|
1102
|
+
xInterp = tile(virtNewCoord[0, :], (nTime, 1))
|
|
1161
1103
|
|
|
1162
1104
|
mesh = meshList[0][0]
|
|
1163
1105
|
for cntTime in range(nTime):
|
|
@@ -1207,17 +1149,22 @@ class SpatialInterpolator(TimeInOut):
|
|
|
1207
1149
|
# return interpolated pressure values
|
|
1208
1150
|
return pInterp
|
|
1209
1151
|
|
|
1152
|
+
def result(self, num):
|
|
1153
|
+
msg = 'result method not implemented yet! Data from source will be passed without transformation.'
|
|
1154
|
+
warn(msg, Warning, stacklevel=2)
|
|
1155
|
+
yield from self.source.result(num)
|
|
1156
|
+
|
|
1210
1157
|
|
|
1211
|
-
class SpatialInterpolatorRotation(SpatialInterpolator):
|
|
1158
|
+
class SpatialInterpolatorRotation(SpatialInterpolator): # pragma: no cover
|
|
1212
1159
|
"""Spatial Interpolation for rotating sources. Gets samples from :attr:`source`
|
|
1213
1160
|
and angles from :attr:`AngleTracker`.Generates output via the generator :meth:`result`.
|
|
1214
1161
|
|
|
1215
1162
|
"""
|
|
1216
1163
|
|
|
1217
|
-
|
|
1164
|
+
# Angle data from AngleTracker class
|
|
1218
1165
|
angle_source = Instance(AngleTracker)
|
|
1219
1166
|
|
|
1220
|
-
|
|
1167
|
+
# Internal identifier
|
|
1221
1168
|
digest = Property(
|
|
1222
1169
|
depends_on=[
|
|
1223
1170
|
'source.digest',
|
|
@@ -1246,7 +1193,7 @@ class SpatialInterpolatorRotation(SpatialInterpolator):
|
|
|
1246
1193
|
|
|
1247
1194
|
Returns
|
|
1248
1195
|
-------
|
|
1249
|
-
Samples in blocks of shape (num, :attr:`
|
|
1196
|
+
Samples in blocks of shape (num, :attr:`num_channels`).
|
|
1250
1197
|
The last block may be shorter than num.
|
|
1251
1198
|
|
|
1252
1199
|
"""
|
|
@@ -1263,14 +1210,14 @@ class SpatialInterpolatorRotation(SpatialInterpolator):
|
|
|
1263
1210
|
count += num
|
|
1264
1211
|
|
|
1265
1212
|
|
|
1266
|
-
class SpatialInterpolatorConstantRotation(SpatialInterpolator):
|
|
1213
|
+
class SpatialInterpolatorConstantRotation(SpatialInterpolator): # pragma: no cover
|
|
1267
1214
|
"""Spatial linear Interpolation for constantly rotating sources.
|
|
1268
1215
|
Gets samples from :attr:`source` and generates output via the
|
|
1269
1216
|
generator :meth:`result`.
|
|
1270
1217
|
"""
|
|
1271
1218
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1219
|
+
# Rotational speed in rps. Positive, if rotation is around positive z-axis sense,
|
|
1220
|
+
# which means from x to y axis.
|
|
1274
1221
|
rotational_speed = Float(0.0)
|
|
1275
1222
|
|
|
1276
1223
|
# internal identifier
|
|
@@ -1302,7 +1249,7 @@ class SpatialInterpolatorConstantRotation(SpatialInterpolator):
|
|
|
1302
1249
|
|
|
1303
1250
|
Returns
|
|
1304
1251
|
-------
|
|
1305
|
-
Samples in blocks of shape (num, :attr:`
|
|
1252
|
+
Samples in blocks of shape (num, :attr:`num_channels`).
|
|
1306
1253
|
The last block may be shorter than num.
|
|
1307
1254
|
|
|
1308
1255
|
"""
|
|
@@ -1317,30 +1264,30 @@ class SpatialInterpolatorConstantRotation(SpatialInterpolator):
|
|
|
1317
1264
|
yield interpVal
|
|
1318
1265
|
|
|
1319
1266
|
|
|
1320
|
-
class Mixer(
|
|
1267
|
+
class Mixer(TimeOut):
|
|
1321
1268
|
"""Mixes the signals from several sources."""
|
|
1322
1269
|
|
|
1323
|
-
|
|
1324
|
-
source =
|
|
1270
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` object.
|
|
1271
|
+
source = Instance(SamplesGenerator)
|
|
1325
1272
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1273
|
+
# List of additional :class:`~acoular.base.SamplesGenerator` objects
|
|
1274
|
+
# to be mixed.
|
|
1328
1275
|
sources = List(Instance(SamplesGenerator, ()))
|
|
1329
1276
|
|
|
1330
|
-
|
|
1277
|
+
# Sampling frequency of the signal as given by :attr:`source`.
|
|
1331
1278
|
sample_freq = Delegate('source')
|
|
1332
1279
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1280
|
+
# Number of channels in output as given by :attr:`source`.
|
|
1281
|
+
num_channels = Delegate('source')
|
|
1335
1282
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1283
|
+
# Number of samples in output as given by :attr:`source`.
|
|
1284
|
+
num_samples = Delegate('source')
|
|
1338
1285
|
|
|
1339
1286
|
# internal identifier
|
|
1340
1287
|
sdigest = Str()
|
|
1341
1288
|
|
|
1342
1289
|
@observe('sources.items.digest')
|
|
1343
|
-
def
|
|
1290
|
+
def _set_sourcesdigest(self, event): # noqa ARG002
|
|
1344
1291
|
self.sdigest = ldigest(self.sources)
|
|
1345
1292
|
|
|
1346
1293
|
# internal identifier
|
|
@@ -1355,9 +1302,11 @@ class Mixer(TimeInOut):
|
|
|
1355
1302
|
if self.source:
|
|
1356
1303
|
for s in self.sources:
|
|
1357
1304
|
if self.sample_freq != s.sample_freq:
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1305
|
+
msg = f'Sample frequency of {s} does not fit'
|
|
1306
|
+
raise ValueError(msg)
|
|
1307
|
+
if self.num_channels != s.num_channels:
|
|
1308
|
+
msg = f'Channel count of {s} does not fit'
|
|
1309
|
+
raise ValueError(msg)
|
|
1361
1310
|
|
|
1362
1311
|
def result(self, num):
|
|
1363
1312
|
"""Python generator that yields the output block-wise.
|
|
@@ -1372,7 +1321,7 @@ class Mixer(TimeInOut):
|
|
|
1372
1321
|
|
|
1373
1322
|
Returns
|
|
1374
1323
|
-------
|
|
1375
|
-
Samples in blocks of shape (num,
|
|
1324
|
+
Samples in blocks of shape (num, num_channels).
|
|
1376
1325
|
The last block may be shorter than num.
|
|
1377
1326
|
|
|
1378
1327
|
"""
|
|
@@ -1395,9 +1344,12 @@ class Mixer(TimeInOut):
|
|
|
1395
1344
|
break
|
|
1396
1345
|
|
|
1397
1346
|
|
|
1398
|
-
class TimePower(
|
|
1347
|
+
class TimePower(TimeOut):
|
|
1399
1348
|
"""Calculates time-depended power of the signal."""
|
|
1400
1349
|
|
|
1350
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1351
|
+
source = Instance(SamplesGenerator)
|
|
1352
|
+
|
|
1401
1353
|
def result(self, num):
|
|
1402
1354
|
"""Python generator that yields the output block-wise.
|
|
1403
1355
|
|
|
@@ -1410,7 +1362,7 @@ class TimePower(TimeInOut):
|
|
|
1410
1362
|
Returns
|
|
1411
1363
|
-------
|
|
1412
1364
|
Squared output of source.
|
|
1413
|
-
Yields samples in blocks of shape (num,
|
|
1365
|
+
Yields samples in blocks of shape (num, num_channels).
|
|
1414
1366
|
The last block may be shorter than num.
|
|
1415
1367
|
|
|
1416
1368
|
"""
|
|
@@ -1418,64 +1370,12 @@ class TimePower(TimeInOut):
|
|
|
1418
1370
|
yield temp * temp
|
|
1419
1371
|
|
|
1420
1372
|
|
|
1421
|
-
class
|
|
1422
|
-
"""Calculates time-dependent average of the signal."""
|
|
1423
|
-
|
|
1424
|
-
#: Number of samples to average over, defaults to 64.
|
|
1425
|
-
naverage = Int(64, desc='number of samples to average over')
|
|
1426
|
-
|
|
1427
|
-
#: Sampling frequency of the output signal, is set automatically.
|
|
1428
|
-
sample_freq = Property(depends_on='source.sample_freq, naverage')
|
|
1429
|
-
|
|
1430
|
-
#: Number of samples of the output signal, is set automatically.
|
|
1431
|
-
numsamples = Property(depends_on='source.numsamples, naverage')
|
|
1432
|
-
|
|
1433
|
-
# internal identifier
|
|
1434
|
-
digest = Property(depends_on=['source.digest', '__class__', 'naverage'])
|
|
1435
|
-
|
|
1436
|
-
@cached_property
|
|
1437
|
-
def _get_digest(self):
|
|
1438
|
-
return digest(self)
|
|
1439
|
-
|
|
1440
|
-
@cached_property
|
|
1441
|
-
def _get_sample_freq(self):
|
|
1442
|
-
if self.source:
|
|
1443
|
-
return 1.0 * self.source.sample_freq / self.naverage
|
|
1444
|
-
return None
|
|
1445
|
-
|
|
1446
|
-
@cached_property
|
|
1447
|
-
def _get_numsamples(self):
|
|
1448
|
-
if self.source:
|
|
1449
|
-
return self.source.numsamples / self.naverage
|
|
1450
|
-
return None
|
|
1451
|
-
|
|
1452
|
-
def result(self, num):
|
|
1453
|
-
"""Python generator that yields the output block-wise.
|
|
1454
|
-
|
|
1455
|
-
Parameters
|
|
1456
|
-
----------
|
|
1457
|
-
num : integer
|
|
1458
|
-
This parameter defines the size of the blocks to be yielded
|
|
1459
|
-
(i.e. the number of samples per block).
|
|
1460
|
-
|
|
1461
|
-
Returns
|
|
1462
|
-
-------
|
|
1463
|
-
Average of the output of source.
|
|
1464
|
-
Yields samples in blocks of shape (num, numchannels).
|
|
1465
|
-
The last block may be shorter than num.
|
|
1466
|
-
|
|
1467
|
-
"""
|
|
1468
|
-
nav = self.naverage
|
|
1469
|
-
for temp in self.source.result(num * nav):
|
|
1470
|
-
ns, nc = temp.shape
|
|
1471
|
-
nso = int(ns / nav)
|
|
1472
|
-
if nso > 0:
|
|
1473
|
-
yield temp[: nso * nav].reshape((nso, -1, nc)).mean(axis=1)
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
class TimeCumAverage(TimeInOut):
|
|
1373
|
+
class TimeCumAverage(TimeOut):
|
|
1477
1374
|
"""Calculates cumulative average of the signal, useful for Leq."""
|
|
1478
1375
|
|
|
1376
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1377
|
+
source = Instance(SamplesGenerator)
|
|
1378
|
+
|
|
1479
1379
|
def result(self, num):
|
|
1480
1380
|
"""Python generator that yields the output block-wise.
|
|
1481
1381
|
|
|
@@ -1488,7 +1388,7 @@ class TimeCumAverage(TimeInOut):
|
|
|
1488
1388
|
Returns
|
|
1489
1389
|
-------
|
|
1490
1390
|
Cumulative average of the output of source.
|
|
1491
|
-
Yields samples in blocks of shape (num,
|
|
1391
|
+
Yields samples in blocks of shape (num, num_channels).
|
|
1492
1392
|
The last block may be shorter than num.
|
|
1493
1393
|
|
|
1494
1394
|
"""
|
|
@@ -1503,9 +1403,12 @@ class TimeCumAverage(TimeInOut):
|
|
|
1503
1403
|
yield temp
|
|
1504
1404
|
|
|
1505
1405
|
|
|
1506
|
-
class TimeReverse(
|
|
1406
|
+
class TimeReverse(TimeOut):
|
|
1507
1407
|
"""Calculates the time-reversed signal of a source."""
|
|
1508
1408
|
|
|
1409
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1410
|
+
source = Instance(SamplesGenerator)
|
|
1411
|
+
|
|
1509
1412
|
def result(self, num):
|
|
1510
1413
|
"""Python generator that yields the output block-wise.
|
|
1511
1414
|
|
|
@@ -1517,7 +1420,7 @@ class TimeReverse(TimeInOut):
|
|
|
1517
1420
|
|
|
1518
1421
|
Returns
|
|
1519
1422
|
-------
|
|
1520
|
-
Yields samples in blocks of shape (num,
|
|
1423
|
+
Yields samples in blocks of shape (num, num_channels).
|
|
1521
1424
|
Time-reversed output of source.
|
|
1522
1425
|
The last block may be shorter than num.
|
|
1523
1426
|
|
|
@@ -1535,15 +1438,18 @@ class TimeReverse(TimeInOut):
|
|
|
1535
1438
|
yield temp[:nsh]
|
|
1536
1439
|
|
|
1537
1440
|
|
|
1538
|
-
class Filter(
|
|
1441
|
+
class Filter(TimeOut):
|
|
1539
1442
|
"""Abstract base class for IIR filters based on scipy lfilter
|
|
1540
1443
|
implements a filter with coefficients that may be changed
|
|
1541
1444
|
during processing.
|
|
1542
1445
|
|
|
1543
|
-
Should not be
|
|
1446
|
+
Should not be instantiated by itself.
|
|
1544
1447
|
"""
|
|
1545
1448
|
|
|
1546
|
-
|
|
1449
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1450
|
+
source = Instance(SamplesGenerator)
|
|
1451
|
+
|
|
1452
|
+
# Filter coefficients
|
|
1547
1453
|
sos = Property()
|
|
1548
1454
|
|
|
1549
1455
|
def _get_sos(self):
|
|
@@ -1560,13 +1466,13 @@ class Filter(TimeInOut):
|
|
|
1560
1466
|
|
|
1561
1467
|
Returns
|
|
1562
1468
|
-------
|
|
1563
|
-
Samples in blocks of shape (num,
|
|
1469
|
+
Samples in blocks of shape (num, num_channels).
|
|
1564
1470
|
Delivers the bandpass filtered output of source.
|
|
1565
1471
|
The last block may be shorter than num.
|
|
1566
1472
|
|
|
1567
1473
|
"""
|
|
1568
1474
|
sos = self.sos
|
|
1569
|
-
zi = zeros((sos.shape[0], 2, self.source.
|
|
1475
|
+
zi = zeros((sos.shape[0], 2, self.source.num_channels))
|
|
1570
1476
|
for block in self.source.result(num):
|
|
1571
1477
|
sos = self.sos # this line is useful in case of changes
|
|
1572
1478
|
# to self.sos during generator lifetime
|
|
@@ -1577,19 +1483,19 @@ class Filter(TimeInOut):
|
|
|
1577
1483
|
class FiltOctave(Filter):
|
|
1578
1484
|
"""Octave or third-octave filter (causal, non-zero phase delay)."""
|
|
1579
1485
|
|
|
1580
|
-
|
|
1486
|
+
# Band center frequency; defaults to 1000.
|
|
1581
1487
|
band = Float(1000.0, desc='band center frequency')
|
|
1582
1488
|
|
|
1583
|
-
|
|
1584
|
-
fraction =
|
|
1489
|
+
# Octave fraction: 'Octave' or 'Third octave'; defaults to 'Octave'.
|
|
1490
|
+
fraction = Map({'Octave': 1, 'Third octave': 3}, default_value='Octave', desc='fraction of octave')
|
|
1585
1491
|
|
|
1586
|
-
|
|
1492
|
+
# Filter order
|
|
1587
1493
|
order = Int(3, desc='IIR filter order')
|
|
1588
1494
|
|
|
1589
1495
|
sos = Property(depends_on=['band', 'fraction', 'source.digest', 'order'])
|
|
1590
1496
|
|
|
1591
1497
|
# internal identifier
|
|
1592
|
-
digest = Property(depends_on=['source.digest', '
|
|
1498
|
+
digest = Property(depends_on=['source.digest', 'band', 'fraction', 'order'])
|
|
1593
1499
|
|
|
1594
1500
|
@cached_property
|
|
1595
1501
|
def _get_digest(self):
|
|
@@ -1622,11 +1528,11 @@ class FiltFiltOctave(FiltOctave):
|
|
|
1622
1528
|
It requires large amounts of memory!
|
|
1623
1529
|
"""
|
|
1624
1530
|
|
|
1625
|
-
|
|
1531
|
+
# Filter order (applied for forward filter and backward filter)
|
|
1626
1532
|
order = Int(2, desc='IIR filter half order')
|
|
1627
1533
|
|
|
1628
1534
|
# internal identifier
|
|
1629
|
-
digest = Property(depends_on=['source.digest', '
|
|
1535
|
+
digest = Property(depends_on=['source.digest', 'band', 'fraction', 'order'])
|
|
1630
1536
|
|
|
1631
1537
|
@cached_property
|
|
1632
1538
|
def _get_digest(self):
|
|
@@ -1664,20 +1570,20 @@ class FiltFiltOctave(FiltOctave):
|
|
|
1664
1570
|
|
|
1665
1571
|
Returns
|
|
1666
1572
|
-------
|
|
1667
|
-
Samples in blocks of shape (num,
|
|
1573
|
+
Samples in blocks of shape (num, num_channels).
|
|
1668
1574
|
Delivers the zero-phase bandpass filtered output of source.
|
|
1669
1575
|
The last block may be shorter than num.
|
|
1670
1576
|
|
|
1671
1577
|
"""
|
|
1672
1578
|
sos = self.sos
|
|
1673
|
-
data = empty((self.source.
|
|
1579
|
+
data = empty((self.source.num_samples, self.source.num_channels))
|
|
1674
1580
|
j = 0
|
|
1675
1581
|
for block in self.source.result(num):
|
|
1676
1582
|
ns, nc = block.shape
|
|
1677
1583
|
data[j : j + ns] = block
|
|
1678
1584
|
j += ns
|
|
1679
1585
|
# filter one channel at a time to save memory
|
|
1680
|
-
for j in range(self.source.
|
|
1586
|
+
for j in range(self.source.num_channels):
|
|
1681
1587
|
data[:, j] = sosfiltfilt(sos, data[:, j])
|
|
1682
1588
|
j = 0
|
|
1683
1589
|
ns = data.shape[0]
|
|
@@ -1692,13 +1598,13 @@ class TimeExpAverage(Filter):
|
|
|
1692
1598
|
I (non-standard) -> 35 ms.
|
|
1693
1599
|
"""
|
|
1694
1600
|
|
|
1695
|
-
|
|
1696
|
-
weight =
|
|
1601
|
+
# time weighting
|
|
1602
|
+
weight = Map({'F': 0.125, 'S': 1.0, 'I': 0.035}, default_value='F', desc='time weighting')
|
|
1697
1603
|
|
|
1698
1604
|
sos = Property(depends_on=['weight', 'source.digest'])
|
|
1699
1605
|
|
|
1700
1606
|
# internal identifier
|
|
1701
|
-
digest = Property(depends_on=['source.digest', '
|
|
1607
|
+
digest = Property(depends_on=['source.digest', 'weight'])
|
|
1702
1608
|
|
|
1703
1609
|
@cached_property
|
|
1704
1610
|
def _get_digest(self):
|
|
@@ -1713,15 +1619,15 @@ class TimeExpAverage(Filter):
|
|
|
1713
1619
|
|
|
1714
1620
|
|
|
1715
1621
|
class FiltFreqWeight(Filter):
|
|
1716
|
-
"""Frequency weighting filter
|
|
1622
|
+
"""Frequency weighting filter according to IEC 61672."""
|
|
1717
1623
|
|
|
1718
|
-
|
|
1719
|
-
weight =
|
|
1624
|
+
# weighting characteristics
|
|
1625
|
+
weight = Enum('A', 'C', 'Z', desc='frequency weighting')
|
|
1720
1626
|
|
|
1721
1627
|
sos = Property(depends_on=['weight', 'source.digest'])
|
|
1722
1628
|
|
|
1723
1629
|
# internal identifier
|
|
1724
|
-
digest = Property(depends_on=['source.digest', '
|
|
1630
|
+
digest = Property(depends_on=['source.digest', 'weight'])
|
|
1725
1631
|
|
|
1726
1632
|
@cached_property
|
|
1727
1633
|
def _get_digest(self):
|
|
@@ -1751,36 +1657,43 @@ class FiltFreqWeight(Filter):
|
|
|
1751
1657
|
return tf2sos(b, a)
|
|
1752
1658
|
|
|
1753
1659
|
|
|
1754
|
-
|
|
1660
|
+
@deprecated_alias({'numbands': 'num_bands'}, read_only=True)
|
|
1661
|
+
class FilterBank(TimeOut):
|
|
1755
1662
|
"""Abstract base class for IIR filter banks based on scipy lfilter
|
|
1756
1663
|
implements a bank of parallel filters.
|
|
1757
1664
|
|
|
1758
|
-
Should not be
|
|
1665
|
+
Should not be instantiated by itself.
|
|
1759
1666
|
"""
|
|
1760
1667
|
|
|
1761
|
-
|
|
1668
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1669
|
+
source = Instance(SamplesGenerator)
|
|
1670
|
+
|
|
1671
|
+
# List of filter coefficients for all filters
|
|
1762
1672
|
sos = Property()
|
|
1763
1673
|
|
|
1764
|
-
|
|
1674
|
+
# List of labels for bands
|
|
1765
1675
|
bands = Property()
|
|
1766
1676
|
|
|
1767
|
-
|
|
1768
|
-
|
|
1677
|
+
# Number of bands
|
|
1678
|
+
num_bands = Property()
|
|
1769
1679
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1680
|
+
# Number of bands
|
|
1681
|
+
num_channels = Property()
|
|
1772
1682
|
|
|
1683
|
+
@abstractmethod
|
|
1773
1684
|
def _get_sos(self):
|
|
1774
|
-
|
|
1685
|
+
"""Returns a list of second order section coefficients."""
|
|
1775
1686
|
|
|
1687
|
+
@abstractmethod
|
|
1776
1688
|
def _get_bands(self):
|
|
1777
|
-
|
|
1689
|
+
"""Returns a list of labels for the bands."""
|
|
1778
1690
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1691
|
+
@abstractmethod
|
|
1692
|
+
def _get_num_bands(self):
|
|
1693
|
+
"""Returns the number of bands."""
|
|
1781
1694
|
|
|
1782
|
-
def
|
|
1783
|
-
return self.
|
|
1695
|
+
def _get_num_channels(self):
|
|
1696
|
+
return self.num_bands * self.source.num_channels
|
|
1784
1697
|
|
|
1785
1698
|
def result(self, num):
|
|
1786
1699
|
"""Python generator that yields the output block-wise.
|
|
@@ -1793,16 +1706,16 @@ class FilterBank(TimeInOut):
|
|
|
1793
1706
|
|
|
1794
1707
|
Returns
|
|
1795
1708
|
-------
|
|
1796
|
-
Samples in blocks of shape (num,
|
|
1709
|
+
Samples in blocks of shape (num, num_channels).
|
|
1797
1710
|
Delivers the bandpass filtered output of source.
|
|
1798
1711
|
The last block may be shorter than num.
|
|
1799
1712
|
|
|
1800
1713
|
"""
|
|
1801
|
-
numbands = self.
|
|
1802
|
-
snumch = self.source.
|
|
1714
|
+
numbands = self.num_bands
|
|
1715
|
+
snumch = self.source.num_channels
|
|
1803
1716
|
sos = self.sos
|
|
1804
1717
|
zi = [zeros((sos[0].shape[0], 2, snumch)) for _ in range(numbands)]
|
|
1805
|
-
res = zeros((num, self.
|
|
1718
|
+
res = zeros((num, self.num_channels), dtype='float')
|
|
1806
1719
|
for block in self.source.result(num):
|
|
1807
1720
|
for i in range(numbands):
|
|
1808
1721
|
res[:, i * snumch : (i + 1) * snumch], zi[i] = sosfilt(sos[i], block, axis=0, zi=zi[i])
|
|
@@ -1812,26 +1725,26 @@ class FilterBank(TimeInOut):
|
|
|
1812
1725
|
class OctaveFilterBank(FilterBank):
|
|
1813
1726
|
"""Octave or third-octave filter bank."""
|
|
1814
1727
|
|
|
1815
|
-
|
|
1728
|
+
# Lowest band center frequency index; defaults to 21 (=125 Hz).
|
|
1816
1729
|
lband = Int(21, desc='lowest band center frequency index')
|
|
1817
1730
|
|
|
1818
|
-
|
|
1731
|
+
# Lowest band center frequency index + 1; defaults to 40 (=8000 Hz).
|
|
1819
1732
|
hband = Int(40, desc='lowest band center frequency index')
|
|
1820
1733
|
|
|
1821
|
-
|
|
1822
|
-
fraction =
|
|
1734
|
+
# Octave fraction: 'Octave' or 'Third octave'; defaults to 'Octave'.
|
|
1735
|
+
fraction = Map({'Octave': 1, 'Third octave': 3}, default_value='Octave', desc='fraction of octave')
|
|
1823
1736
|
|
|
1824
|
-
|
|
1737
|
+
# List of filter coefficients for all filters
|
|
1825
1738
|
ba = Property(depends_on=['lband', 'hband', 'fraction', 'source.digest'])
|
|
1826
1739
|
|
|
1827
|
-
|
|
1740
|
+
# List of labels for bands
|
|
1828
1741
|
bands = Property(depends_on=['lband', 'hband', 'fraction'])
|
|
1829
1742
|
|
|
1830
|
-
|
|
1831
|
-
|
|
1743
|
+
# Number of bands
|
|
1744
|
+
num_bands = Property(depends_on=['lband', 'hband', 'fraction'])
|
|
1832
1745
|
|
|
1833
1746
|
# internal identifier
|
|
1834
|
-
digest = Property(depends_on=['source.digest', '
|
|
1747
|
+
digest = Property(depends_on=['source.digest', 'lband', 'hband', 'fraction', 'order'])
|
|
1835
1748
|
|
|
1836
1749
|
@cached_property
|
|
1837
1750
|
def _get_digest(self):
|
|
@@ -1842,7 +1755,7 @@ class OctaveFilterBank(FilterBank):
|
|
|
1842
1755
|
return [10 ** (i / 10) for i in range(self.lband, self.hband, 4 - self.fraction_)]
|
|
1843
1756
|
|
|
1844
1757
|
@cached_property
|
|
1845
|
-
def
|
|
1758
|
+
def _get_num_bands(self):
|
|
1846
1759
|
return len(self.bands)
|
|
1847
1760
|
|
|
1848
1761
|
@cached_property
|
|
@@ -1856,148 +1769,27 @@ class OctaveFilterBank(FilterBank):
|
|
|
1856
1769
|
return sos
|
|
1857
1770
|
|
|
1858
1771
|
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
# basename for cache
|
|
1863
|
-
basename = Property(depends_on='digest')
|
|
1864
|
-
|
|
1865
|
-
# hdf5 cache file
|
|
1866
|
-
h5f = Instance(H5CacheFileBase, transient=True)
|
|
1867
|
-
|
|
1868
|
-
# internal identifier
|
|
1869
|
-
digest = Property(depends_on=['source.digest', '__class__'])
|
|
1870
|
-
|
|
1871
|
-
@cached_property
|
|
1872
|
-
def _get_digest(self):
|
|
1873
|
-
return digest(self)
|
|
1874
|
-
|
|
1875
|
-
@cached_property
|
|
1876
|
-
def _get_basename(self):
|
|
1877
|
-
obj = self.source # start with source
|
|
1878
|
-
basename = 'void' # if no file source is found
|
|
1879
|
-
while obj:
|
|
1880
|
-
if 'basename' in obj.all_trait_names(): # at original source?
|
|
1881
|
-
basename = obj.basename # get the name
|
|
1882
|
-
break
|
|
1883
|
-
try:
|
|
1884
|
-
obj = obj.source # traverse down until original data source
|
|
1885
|
-
except AttributeError:
|
|
1886
|
-
obj = None
|
|
1887
|
-
return basename
|
|
1888
|
-
|
|
1889
|
-
def _pass_data(self, num):
|
|
1890
|
-
yield from self.source.result(num)
|
|
1891
|
-
|
|
1892
|
-
def _write_data_to_cache(self, num):
|
|
1893
|
-
nodename = 'tc_' + self.digest
|
|
1894
|
-
self.h5f.create_extendable_array(nodename, (0, self.numchannels), 'float32')
|
|
1895
|
-
ac = self.h5f.get_data_by_reference(nodename)
|
|
1896
|
-
self.h5f.set_node_attribute(ac, 'sample_freq', self.sample_freq)
|
|
1897
|
-
self.h5f.set_node_attribute(ac, 'complete', False)
|
|
1898
|
-
for data in self.source.result(num):
|
|
1899
|
-
self.h5f.append_data(ac, data)
|
|
1900
|
-
yield data
|
|
1901
|
-
self.h5f.set_node_attribute(ac, 'complete', True)
|
|
1902
|
-
|
|
1903
|
-
def _get_data_from_cache(self, num):
|
|
1904
|
-
nodename = 'tc_' + self.digest
|
|
1905
|
-
ac = self.h5f.get_data_by_reference(nodename)
|
|
1906
|
-
i = 0
|
|
1907
|
-
while i < ac.shape[0]:
|
|
1908
|
-
yield ac[i : i + num]
|
|
1909
|
-
i += num
|
|
1910
|
-
|
|
1911
|
-
def _get_data_from_incomplete_cache(self, num):
|
|
1912
|
-
nodename = 'tc_' + self.digest
|
|
1913
|
-
ac = self.h5f.get_data_by_reference(nodename)
|
|
1914
|
-
i = 0
|
|
1915
|
-
nblocks = 0
|
|
1916
|
-
while i + num <= ac.shape[0]:
|
|
1917
|
-
yield ac[i : i + num]
|
|
1918
|
-
nblocks += 1
|
|
1919
|
-
i += num
|
|
1920
|
-
self.h5f.remove_data(nodename)
|
|
1921
|
-
self.h5f.create_extendable_array(nodename, (0, self.numchannels), 'float32')
|
|
1922
|
-
ac = self.h5f.get_data_by_reference(nodename)
|
|
1923
|
-
self.h5f.set_node_attribute(ac, 'sample_freq', self.sample_freq)
|
|
1924
|
-
self.h5f.set_node_attribute(ac, 'complete', False)
|
|
1925
|
-
for j, data in enumerate(self.source.result(num)):
|
|
1926
|
-
self.h5f.append_data(ac, data)
|
|
1927
|
-
if j >= nblocks:
|
|
1928
|
-
yield data
|
|
1929
|
-
self.h5f.set_node_attribute(ac, 'complete', True)
|
|
1930
|
-
|
|
1931
|
-
# result generator: delivers input, possibly from cache
|
|
1932
|
-
def result(self, num):
|
|
1933
|
-
"""Python generator that yields the output from cache block-wise.
|
|
1934
|
-
|
|
1935
|
-
Parameters
|
|
1936
|
-
----------
|
|
1937
|
-
num : integer
|
|
1938
|
-
This parameter defines the size of the blocks to be yielded
|
|
1939
|
-
(i.e. the number of samples per block).
|
|
1940
|
-
|
|
1941
|
-
Returns
|
|
1942
|
-
-------
|
|
1943
|
-
Samples in blocks of shape (num, numchannels).
|
|
1944
|
-
The last block may be shorter than num.
|
|
1945
|
-
Echos the source output, but reads it from cache
|
|
1946
|
-
when available and prevents unnecassary recalculation.
|
|
1947
|
-
|
|
1948
|
-
"""
|
|
1949
|
-
if config.global_caching == 'none':
|
|
1950
|
-
generator = self._pass_data
|
|
1951
|
-
else:
|
|
1952
|
-
nodename = 'tc_' + self.digest
|
|
1953
|
-
H5cache.get_cache_file(self, self.basename)
|
|
1954
|
-
if not self.h5f:
|
|
1955
|
-
generator = self._pass_data
|
|
1956
|
-
elif self.h5f.is_cached(nodename):
|
|
1957
|
-
generator = self._get_data_from_cache
|
|
1958
|
-
if config.global_caching == 'overwrite':
|
|
1959
|
-
self.h5f.remove_data(nodename)
|
|
1960
|
-
generator = self._write_data_to_cache
|
|
1961
|
-
elif not self.h5f.get_data_by_reference(nodename).attrs.__contains__('complete'):
|
|
1962
|
-
if config.global_caching == 'readonly':
|
|
1963
|
-
generator = self._pass_data
|
|
1964
|
-
else:
|
|
1965
|
-
generator = self._get_data_from_incomplete_cache
|
|
1966
|
-
elif not self.h5f.get_data_by_reference(nodename).attrs['complete']:
|
|
1967
|
-
if config.global_caching == 'readonly':
|
|
1968
|
-
warn(
|
|
1969
|
-
"Cache file is incomplete for nodename %s. With config.global_caching='readonly', the cache file will not be used!"
|
|
1970
|
-
% str(nodename),
|
|
1971
|
-
Warning,
|
|
1972
|
-
stacklevel=1,
|
|
1973
|
-
)
|
|
1974
|
-
generator = self._pass_data
|
|
1975
|
-
else:
|
|
1976
|
-
generator = self._get_data_from_incomplete_cache
|
|
1977
|
-
elif not self.h5f.is_cached(nodename):
|
|
1978
|
-
generator = self._write_data_to_cache
|
|
1979
|
-
if config.global_caching == 'readonly':
|
|
1980
|
-
generator = self._pass_data
|
|
1981
|
-
yield from generator(num)
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
class WriteWAV(TimeInOut):
|
|
1772
|
+
@deprecated_alias({'name': 'file'})
|
|
1773
|
+
class WriteWAV(TimeOut):
|
|
1985
1774
|
"""Saves time signal from one or more channels as mono/stereo/multi-channel
|
|
1986
1775
|
`*.wav` file.
|
|
1987
1776
|
"""
|
|
1988
1777
|
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1778
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1779
|
+
source = Instance(SamplesGenerator)
|
|
1780
|
+
|
|
1781
|
+
# Name of the file to be saved. If none is given, the name will be
|
|
1782
|
+
# automatically generated from the sources.
|
|
1783
|
+
file = File(filter=['*.wav'], desc='name of wave file')
|
|
1992
1784
|
|
|
1993
|
-
|
|
1994
|
-
basename = Property(depends_on='digest')
|
|
1785
|
+
# Basename for cache, readonly.
|
|
1786
|
+
basename = Property(depends_on=['digest'])
|
|
1995
1787
|
|
|
1996
|
-
|
|
1997
|
-
channels =
|
|
1788
|
+
# Channel(s) to save. List can only contain one or two channels.
|
|
1789
|
+
channels = List(int, desc='channel to save')
|
|
1998
1790
|
|
|
1999
1791
|
# internal identifier
|
|
2000
|
-
digest = Property(depends_on=['source.digest', 'channels'
|
|
1792
|
+
digest = Property(depends_on=['source.digest', 'channels'])
|
|
2001
1793
|
|
|
2002
1794
|
@cached_property
|
|
2003
1795
|
def _get_digest(self):
|
|
@@ -2005,18 +1797,15 @@ class WriteWAV(TimeInOut):
|
|
|
2005
1797
|
|
|
2006
1798
|
@cached_property
|
|
2007
1799
|
def _get_basename(self):
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
except AttributeError:
|
|
2018
|
-
basename = 'void' # if no file source is found
|
|
2019
|
-
return basename
|
|
1800
|
+
warn(
|
|
1801
|
+
(
|
|
1802
|
+
f'The basename attribute of a {self.__class__.__name__} object is deprecated'
|
|
1803
|
+
' and will be removed in a future release!'
|
|
1804
|
+
),
|
|
1805
|
+
DeprecationWarning,
|
|
1806
|
+
stacklevel=2,
|
|
1807
|
+
)
|
|
1808
|
+
return find_basename(self.source)
|
|
2020
1809
|
|
|
2021
1810
|
def save(self):
|
|
2022
1811
|
"""Saves source output to one- or multiple-channel `*.wav` file."""
|
|
@@ -2025,51 +1814,59 @@ class WriteWAV(TimeInOut):
|
|
|
2025
1814
|
msg = 'No channels given for output.'
|
|
2026
1815
|
raise ValueError(msg)
|
|
2027
1816
|
if nc > 2:
|
|
2028
|
-
warn('More than two channels given for output, exported file will have
|
|
2029
|
-
if self.
|
|
1817
|
+
warn(f'More than two channels given for output, exported file will have {nc:d} channels', stacklevel=1)
|
|
1818
|
+
if self.file == '':
|
|
2030
1819
|
name = self.basename
|
|
2031
1820
|
for nr in self.channels:
|
|
2032
|
-
name += '
|
|
1821
|
+
name += f'{nr:d}'
|
|
2033
1822
|
name += '.wav'
|
|
2034
1823
|
else:
|
|
2035
|
-
name = self.
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
1824
|
+
name = self.file
|
|
1825
|
+
with wave.open(name, 'w') as wf:
|
|
1826
|
+
wf.setnchannels(nc)
|
|
1827
|
+
wf.setsampwidth(2)
|
|
1828
|
+
wf.setframerate(self.source.sample_freq)
|
|
1829
|
+
wf.setnframes(self.source.num_samples)
|
|
1830
|
+
mx = 0.0
|
|
1831
|
+
ind = array(self.channels)
|
|
1832
|
+
for data in self.source.result(1024):
|
|
1833
|
+
mx = max(abs(data[:, ind]).max(), mx)
|
|
1834
|
+
scale = 0.9 * 2**15 / mx
|
|
1835
|
+
for data in self.source.result(1024):
|
|
1836
|
+
wf.writeframesraw(array(data[:, ind] * scale, dtype=int16).tostring())
|
|
1837
|
+
|
|
1838
|
+
def result(self, num):
|
|
1839
|
+
msg = 'result method not implemented yet! Data from source will be passed without transformation.'
|
|
1840
|
+
warn(msg, Warning, stacklevel=2)
|
|
1841
|
+
yield from self.source.result(num)
|
|
1842
|
+
|
|
1843
|
+
|
|
1844
|
+
@deprecated_alias({'name': 'file', 'numsamples_write': 'num_samples_write', 'writeflag': 'write_flag'})
|
|
1845
|
+
class WriteH5(TimeOut):
|
|
2052
1846
|
"""Saves time signal as `*.h5` file."""
|
|
2053
1847
|
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
1848
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1849
|
+
source = Instance(SamplesGenerator)
|
|
1850
|
+
|
|
1851
|
+
# Name of the file to be saved. If none is given, the name will be
|
|
1852
|
+
# automatically generated from a time stamp.
|
|
1853
|
+
file = File(filter=['*.h5'], desc='name of data file')
|
|
2057
1854
|
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
1855
|
+
# Number of samples to write to file by `result` method.
|
|
1856
|
+
# defaults to -1 (write as long as source yields data).
|
|
1857
|
+
num_samples_write = Int(-1)
|
|
2061
1858
|
|
|
2062
1859
|
# flag that can be raised to stop file writing
|
|
2063
|
-
|
|
1860
|
+
write_flag = Bool(True)
|
|
2064
1861
|
|
|
2065
1862
|
# internal identifier
|
|
2066
|
-
digest = Property(depends_on=['source.digest'
|
|
1863
|
+
digest = Property(depends_on=['source.digest'])
|
|
2067
1864
|
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
precision =
|
|
1865
|
+
# The floating-number-precision of entries of H5 File corresponding
|
|
1866
|
+
# to numpy dtypes. Default is 32 bit.
|
|
1867
|
+
precision = Enum('float32', 'float64', desc='precision of H5 File')
|
|
2071
1868
|
|
|
2072
|
-
|
|
1869
|
+
# Metadata to be stored in HDF5 file object
|
|
2073
1870
|
metadata = Dict(desc='metadata to be stored in .h5 file')
|
|
2074
1871
|
|
|
2075
1872
|
@cached_property
|
|
@@ -2077,15 +1874,15 @@ class WriteH5(TimeInOut):
|
|
|
2077
1874
|
return digest(self)
|
|
2078
1875
|
|
|
2079
1876
|
def create_filename(self):
|
|
2080
|
-
if self.
|
|
1877
|
+
if self.file == '':
|
|
2081
1878
|
name = datetime.now(tz=timezone.utc).isoformat('_').replace(':', '-').replace('.', '_')
|
|
2082
|
-
self.
|
|
1879
|
+
self.file = path.join(config.td_dir, name + '.h5')
|
|
2083
1880
|
|
|
2084
1881
|
def get_initialized_file(self):
|
|
2085
1882
|
file = _get_h5file_class()
|
|
2086
1883
|
self.create_filename()
|
|
2087
|
-
f5h = file(self.
|
|
2088
|
-
f5h.create_extendable_array('time_data', (0, self.
|
|
1884
|
+
f5h = file(self.file, mode='w')
|
|
1885
|
+
f5h.create_extendable_array('time_data', (0, self.num_channels), self.precision)
|
|
2089
1886
|
ac = f5h.get_data_by_reference('time_data')
|
|
2090
1887
|
f5h.set_node_attribute(ac, 'sample_freq', self.sample_freq)
|
|
2091
1888
|
self.add_metadata(f5h)
|
|
@@ -2105,6 +1902,8 @@ class WriteH5(TimeInOut):
|
|
|
2105
1902
|
if nitems > 0:
|
|
2106
1903
|
f5h.create_new_group('metadata', '/')
|
|
2107
1904
|
for key, value in self.metadata.items():
|
|
1905
|
+
if isinstance(value, str):
|
|
1906
|
+
value = array(value, dtype='S')
|
|
2108
1907
|
f5h.create_array('/metadata', key, value)
|
|
2109
1908
|
|
|
2110
1909
|
def result(self, num):
|
|
@@ -2120,19 +1919,19 @@ class WriteH5(TimeInOut):
|
|
|
2120
1919
|
|
|
2121
1920
|
Returns
|
|
2122
1921
|
-------
|
|
2123
|
-
Samples in blocks of shape (num,
|
|
1922
|
+
Samples in blocks of shape (num, num_channels).
|
|
2124
1923
|
The last block may be shorter than num.
|
|
2125
1924
|
Echos the source output, but reads it from cache
|
|
2126
|
-
when available and prevents
|
|
1925
|
+
when available and prevents unnecessary recalculation.
|
|
2127
1926
|
|
|
2128
1927
|
"""
|
|
2129
|
-
self.
|
|
1928
|
+
self.write_flag = True
|
|
2130
1929
|
f5h = self.get_initialized_file()
|
|
2131
1930
|
ac = f5h.get_data_by_reference('time_data')
|
|
2132
1931
|
scount = 0
|
|
2133
|
-
stotal = self.
|
|
1932
|
+
stotal = self.num_samples_write
|
|
2134
1933
|
source_gen = self.source.result(num)
|
|
2135
|
-
while self.
|
|
1934
|
+
while self.write_flag:
|
|
2136
1935
|
sleft = stotal - scount
|
|
2137
1936
|
if stotal != -1 and sleft > 0:
|
|
2138
1937
|
anz = min(num, sleft)
|
|
@@ -2145,166 +1944,24 @@ class WriteH5(TimeInOut):
|
|
|
2145
1944
|
except StopIteration:
|
|
2146
1945
|
break
|
|
2147
1946
|
f5h.append_data(ac, data[:anz])
|
|
2148
|
-
yield data
|
|
2149
1947
|
f5h.flush()
|
|
1948
|
+
yield data
|
|
2150
1949
|
scount += anz
|
|
2151
1950
|
f5h.close()
|
|
2152
1951
|
|
|
2153
1952
|
|
|
2154
|
-
class
|
|
2155
|
-
"""
|
|
2156
|
-
Takes an iterator/generator and makes it thread-safe by
|
|
2157
|
-
serializing call to the `next` method of given iterator/generator.
|
|
2158
|
-
"""
|
|
2159
|
-
|
|
2160
|
-
def __init__(self, it):
|
|
2161
|
-
self.it = it
|
|
2162
|
-
self.lock = threading.Lock()
|
|
1953
|
+
class TimeConvolve(TimeOut):
|
|
1954
|
+
"""Fast frequency domain convolution with the Uniformly partitioned overlap-save method (UPOLS).
|
|
2163
1955
|
|
|
2164
|
-
|
|
2165
|
-
with self.lock:
|
|
2166
|
-
return self.it.__next__()
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
class SampleSplitter(TimeInOut):
|
|
2170
|
-
"""Distributes data blocks from source to several following objects.
|
|
2171
|
-
A separate block buffer is created for each registered object in
|
|
2172
|
-
(:attr:`block_buffer`) .
|
|
1956
|
+
See :cite:`Wefers2015` for details.
|
|
2173
1957
|
"""
|
|
2174
1958
|
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
block_buffer = Dict(key_trait=Instance(SamplesGenerator))
|
|
2178
|
-
|
|
2179
|
-
#: max elements/blocks in block buffers.
|
|
2180
|
-
buffer_size = Int(100)
|
|
2181
|
-
|
|
2182
|
-
#: defines behaviour in case of block_buffer overflow. Can be set individually
|
|
2183
|
-
#: for each registered object.
|
|
2184
|
-
#:
|
|
2185
|
-
#: * 'error': an IOError is thrown by the class
|
|
2186
|
-
#: * 'warning': a warning is displayed. Possibly leads to lost blocks of data
|
|
2187
|
-
#: * 'none': nothing happens. Possibly leads to lost blocks of data
|
|
2188
|
-
buffer_overflow_treatment = Dict(
|
|
2189
|
-
key_trait=Instance(SamplesGenerator),
|
|
2190
|
-
value_trait=Trait('error', 'warning', 'none'),
|
|
2191
|
-
desc='defines buffer overflow behaviour.',
|
|
2192
|
-
)
|
|
2193
|
-
|
|
2194
|
-
# shadow trait to monitor if source deliver samples or is empty
|
|
2195
|
-
_source_generator_exist = Bool(False)
|
|
2196
|
-
|
|
2197
|
-
# shadow trait to monitor if buffer of objects with overflow treatment = 'error'
|
|
2198
|
-
# or warning is overfilled. Error will be raised in all threads.
|
|
2199
|
-
_buffer_overflow = Bool(False)
|
|
2200
|
-
|
|
2201
|
-
# Helper Trait holds source generator
|
|
2202
|
-
_source_generator = Trait()
|
|
2203
|
-
|
|
2204
|
-
def _create_block_buffer(self, obj):
|
|
2205
|
-
self.block_buffer[obj] = deque([], maxlen=self.buffer_size)
|
|
2206
|
-
|
|
2207
|
-
def _create_buffer_overflow_treatment(self, obj):
|
|
2208
|
-
self.buffer_overflow_treatment[obj] = 'error'
|
|
2209
|
-
|
|
2210
|
-
def _clear_block_buffer(self, obj):
|
|
2211
|
-
self.block_buffer[obj].clear()
|
|
2212
|
-
|
|
2213
|
-
def _remove_block_buffer(self, obj):
|
|
2214
|
-
del self.block_buffer[obj]
|
|
2215
|
-
|
|
2216
|
-
def _remove_buffer_overflow_treatment(self, obj):
|
|
2217
|
-
del self.buffer_overflow_treatment[obj]
|
|
2218
|
-
|
|
2219
|
-
def _assert_obj_registered(self, obj):
|
|
2220
|
-
if obj not in self.block_buffer:
|
|
2221
|
-
raise OSError('calling object %s is not registered.' % obj)
|
|
2222
|
-
|
|
2223
|
-
def _get_objs_to_inspect(self):
|
|
2224
|
-
return [obj for obj in self.buffer_overflow_treatment if self.buffer_overflow_treatment[obj] != 'none']
|
|
2225
|
-
|
|
2226
|
-
def _inspect_buffer_levels(self, inspect_objs):
|
|
2227
|
-
for obj in inspect_objs:
|
|
2228
|
-
if len(self.block_buffer[obj]) == self.buffer_size:
|
|
2229
|
-
if self.buffer_overflow_treatment[obj] == 'error':
|
|
2230
|
-
self._buffer_overflow = True
|
|
2231
|
-
elif self.buffer_overflow_treatment[obj] == 'warning':
|
|
2232
|
-
warn('overfilled buffer for object: %s data will get lost' % obj, UserWarning, stacklevel=1)
|
|
2233
|
-
|
|
2234
|
-
def _create_source_generator(self, num):
|
|
2235
|
-
for obj in self.block_buffer:
|
|
2236
|
-
self._clear_block_buffer(obj)
|
|
2237
|
-
self._buffer_overflow = False # reset overflow bool
|
|
2238
|
-
self._source_generator = LockedGenerator(self.source.result(num))
|
|
2239
|
-
self._source_generator_exist = True # indicates full generator
|
|
2240
|
-
|
|
2241
|
-
def _fill_block_buffers(self):
|
|
2242
|
-
next_block = next(self._source_generator)
|
|
2243
|
-
[self.block_buffer[obj].appendleft(next_block) for obj in self.block_buffer]
|
|
2244
|
-
|
|
2245
|
-
@on_trait_change('buffer_size')
|
|
2246
|
-
def _change_buffer_size(self): #
|
|
2247
|
-
for obj in self.block_buffer:
|
|
2248
|
-
self._remove_block_buffer(obj)
|
|
2249
|
-
self._create_block_buffer(obj)
|
|
2250
|
-
|
|
2251
|
-
def register_object(self, *objects_to_register):
|
|
2252
|
-
"""Function that can be used to register objects that receive blocks from this class."""
|
|
2253
|
-
for obj in objects_to_register:
|
|
2254
|
-
if obj not in self.block_buffer:
|
|
2255
|
-
self._create_block_buffer(obj)
|
|
2256
|
-
self._create_buffer_overflow_treatment(obj)
|
|
2257
|
-
|
|
2258
|
-
def remove_object(self, *objects_to_remove):
|
|
2259
|
-
"""Function that can be used to remove registered objects."""
|
|
2260
|
-
for obj in objects_to_remove:
|
|
2261
|
-
self._remove_block_buffer(obj)
|
|
2262
|
-
self._remove_buffer_overflow_treatment(obj)
|
|
2263
|
-
|
|
2264
|
-
def result(self, num):
|
|
2265
|
-
"""Python generator that yields the output block-wise from block-buffer.
|
|
2266
|
-
|
|
2267
|
-
Parameters
|
|
2268
|
-
----------
|
|
2269
|
-
num : integer
|
|
2270
|
-
This parameter defines the size of the blocks to be yielded
|
|
2271
|
-
(i.e. the number of samples per block).
|
|
2272
|
-
|
|
2273
|
-
Returns
|
|
2274
|
-
-------
|
|
2275
|
-
Samples in blocks of shape (num, numchannels).
|
|
2276
|
-
Delivers a block of samples to the calling object.
|
|
2277
|
-
The last block may be shorter than num.
|
|
2278
|
-
|
|
2279
|
-
"""
|
|
2280
|
-
calling_obj = currentframe().f_back.f_locals['self']
|
|
2281
|
-
self._assert_obj_registered(calling_obj)
|
|
2282
|
-
objs_to_inspect = self._get_objs_to_inspect()
|
|
2283
|
-
|
|
2284
|
-
if not self._source_generator_exist:
|
|
2285
|
-
self._create_source_generator(num)
|
|
2286
|
-
|
|
2287
|
-
while not self._buffer_overflow:
|
|
2288
|
-
if self.block_buffer[calling_obj]:
|
|
2289
|
-
yield self.block_buffer[calling_obj].pop()
|
|
2290
|
-
else:
|
|
2291
|
-
self._inspect_buffer_levels(objs_to_inspect)
|
|
2292
|
-
try:
|
|
2293
|
-
self._fill_block_buffers()
|
|
2294
|
-
except StopIteration:
|
|
2295
|
-
self._source_generator_exist = False
|
|
2296
|
-
return
|
|
2297
|
-
else:
|
|
2298
|
-
msg = 'Maximum size of block buffer is reached!'
|
|
2299
|
-
raise OSError(msg)
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
class TimeConvolve(TimeInOut):
|
|
2303
|
-
"""Uniformly partitioned overlap-save method (UPOLS) for fast convolution in the frequency domain, see :ref:`Wefers, 2015<Wefers2015>`."""
|
|
1959
|
+
# Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1960
|
+
source = Instance(SamplesGenerator)
|
|
2304
1961
|
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
1962
|
+
# Convolution kernel in the time domain. The second dimension of the kernel array has to be
|
|
1963
|
+
# either 1 or match :attr:`~SamplesGenerator.num_channels`. If only a single kernel is supplied,
|
|
1964
|
+
# it is applied to all channels.
|
|
2308
1965
|
kernel = CArray(dtype=float, desc='Convolution kernel.')
|
|
2309
1966
|
|
|
2310
1967
|
_block_size = Int(desc='Block size')
|
|
@@ -2315,7 +1972,7 @@ class TimeConvolve(TimeInOut):
|
|
|
2315
1972
|
)
|
|
2316
1973
|
|
|
2317
1974
|
# internal identifier
|
|
2318
|
-
digest = Property(depends_on=['source.digest', 'kernel'
|
|
1975
|
+
digest = Property(depends_on=['source.digest', 'kernel'])
|
|
2319
1976
|
|
|
2320
1977
|
@cached_property
|
|
2321
1978
|
def _get_digest(self):
|
|
@@ -2330,9 +1987,9 @@ class TimeConvolve(TimeInOut):
|
|
|
2330
1987
|
if self.kernel.ndim > 2:
|
|
2331
1988
|
msg = 'Only one or two dimensional kernels accepted.'
|
|
2332
1989
|
raise ValueError(msg)
|
|
2333
|
-
# check if number of kernels matches
|
|
2334
|
-
if self.kernel.shape[1] not in (1, self.source.
|
|
2335
|
-
msg = 'Number of kernels must be either `
|
|
1990
|
+
# check if number of kernels matches num_channels
|
|
1991
|
+
if self.kernel.shape[1] not in (1, self.source.num_channels):
|
|
1992
|
+
msg = 'Number of kernels must be either `num_channels` or one.'
|
|
2336
1993
|
raise ValueError(msg)
|
|
2337
1994
|
|
|
2338
1995
|
# compute the rfft of the kernel blockwise
|
|
@@ -2366,7 +2023,7 @@ class TimeConvolve(TimeInOut):
|
|
|
2366
2023
|
|
|
2367
2024
|
Returns
|
|
2368
2025
|
-------
|
|
2369
|
-
Samples in blocks of shape (num,
|
|
2026
|
+
Samples in blocks of shape (num, num_channels).
|
|
2370
2027
|
The last block may be shorter than num.
|
|
2371
2028
|
|
|
2372
2029
|
"""
|
|
@@ -2374,8 +2031,8 @@ class TimeConvolve(TimeInOut):
|
|
|
2374
2031
|
# initialize variables
|
|
2375
2032
|
self._block_size = num
|
|
2376
2033
|
L = self.kernel.shape[0]
|
|
2377
|
-
N = self.source.
|
|
2378
|
-
M = self.source.
|
|
2034
|
+
N = self.source.num_channels
|
|
2035
|
+
M = self.source.num_samples
|
|
2379
2036
|
numblocks_kernel = int(ceil(L / num)) # number of kernel blocks
|
|
2380
2037
|
Q = int(ceil(M / num)) # number of signal blocks
|
|
2381
2038
|
R = int(ceil((L + M - 1) / num)) # number of output blocks
|
|
@@ -2425,13 +2082,13 @@ class TimeConvolve(TimeInOut):
|
|
|
2425
2082
|
|
|
2426
2083
|
|
|
2427
2084
|
@nb.jit(nopython=True, cache=True)
|
|
2428
|
-
def _append_to_fdl(fdl, idx, numblocks_kernel, buff):
|
|
2085
|
+
def _append_to_fdl(fdl, idx, numblocks_kernel, buff): # pragma: no cover
|
|
2429
2086
|
fdl[idx] = buff
|
|
2430
2087
|
idx = int(idx + 1 % numblocks_kernel)
|
|
2431
2088
|
|
|
2432
2089
|
|
|
2433
2090
|
@nb.jit(nopython=True, cache=True)
|
|
2434
|
-
def _spectral_sum(out, fdl, kb):
|
|
2091
|
+
def _spectral_sum(out, fdl, kb): # pragma: no cover
|
|
2435
2092
|
P, B, N = kb.shape
|
|
2436
2093
|
for n in range(N):
|
|
2437
2094
|
for b in range(B):
|
|
@@ -2440,3 +2097,21 @@ def _spectral_sum(out, fdl, kb):
|
|
|
2440
2097
|
out[b, n] += fdl[i, b, n] * kb[i, b, n]
|
|
2441
2098
|
|
|
2442
2099
|
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
|
+
)
|