acoular 24.5__py3-none-any.whl → 24.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 +17 -11
- acoular/base.py +312 -0
- acoular/configuration.py +23 -16
- acoular/demo/acoular_demo.py +28 -35
- acoular/environments.py +20 -15
- acoular/fastFuncs.py +40 -40
- acoular/fbeamform.py +1100 -1130
- acoular/fprocess.py +368 -0
- acoular/grids.py +36 -22
- acoular/h5cache.py +34 -34
- acoular/h5files.py +13 -13
- acoular/internal.py +3 -3
- acoular/process.py +464 -0
- acoular/sdinput.py +24 -4
- acoular/signals.py +20 -6
- acoular/sources.py +140 -56
- acoular/spectra.py +34 -53
- acoular/tbeamform.py +264 -142
- acoular/tfastfuncs.py +17 -18
- acoular/tools/__init__.py +2 -0
- acoular/tools/aiaa.py +7 -8
- acoular/tools/helpers.py +2 -2
- acoular/tools/metrics.py +1 -1
- acoular/tools/utils.py +210 -0
- acoular/tprocess.py +168 -532
- acoular/traitsviews.py +5 -3
- acoular/version.py +2 -2
- {acoular-24.5.dist-info → acoular-24.10.dist-info}/METADATA +49 -8
- acoular-24.10.dist-info/RECORD +54 -0
- {acoular-24.5.dist-info → acoular-24.10.dist-info}/WHEEL +1 -1
- acoular-24.5.dist-info/RECORD +0 -50
- {acoular-24.5.dist-info → acoular-24.10.dist-info}/licenses/AUTHORS.rst +0 -0
- {acoular-24.5.dist-info → acoular-24.10.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,15 @@
|
|
|
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 collections import deque
|
|
41
35
|
from datetime import datetime, timezone
|
|
42
|
-
from inspect import currentframe
|
|
43
36
|
from os import path
|
|
44
37
|
from warnings import warn
|
|
45
38
|
|
|
@@ -78,13 +71,13 @@ from numpy import (
|
|
|
78
71
|
sqrt,
|
|
79
72
|
stack,
|
|
80
73
|
sum,
|
|
74
|
+
tile,
|
|
81
75
|
unique,
|
|
82
76
|
zeros,
|
|
83
77
|
)
|
|
84
|
-
from numpy.linalg import norm
|
|
85
|
-
from numpy.matlib import repmat
|
|
86
78
|
from scipy.fft import irfft, rfft
|
|
87
79
|
from scipy.interpolate import CloughTocher2DInterpolator, CubicSpline, LinearNDInterpolator, Rbf, splev, splrep
|
|
80
|
+
from scipy.linalg import norm
|
|
88
81
|
from scipy.signal import bilinear, butter, sosfilt, sosfiltfilt, tf2sos
|
|
89
82
|
from scipy.spatial import Delaunay
|
|
90
83
|
from traits.api import (
|
|
@@ -96,7 +89,6 @@ from traits.api import (
|
|
|
96
89
|
Dict,
|
|
97
90
|
File,
|
|
98
91
|
Float,
|
|
99
|
-
HasPrivateTraits,
|
|
100
92
|
Instance,
|
|
101
93
|
Int,
|
|
102
94
|
List,
|
|
@@ -109,100 +101,28 @@ from traits.api import (
|
|
|
109
101
|
on_trait_change,
|
|
110
102
|
)
|
|
111
103
|
|
|
104
|
+
# acoular imports
|
|
105
|
+
from .base import SamplesGenerator, TimeOut
|
|
112
106
|
from .configuration import config
|
|
113
107
|
from .environments import cartToCyl, cylToCart
|
|
114
|
-
from .
|
|
115
|
-
from .h5files import H5CacheFileBase, _get_h5file_class
|
|
116
|
-
|
|
117
|
-
# acoular imports
|
|
108
|
+
from .h5files import _get_h5file_class
|
|
118
109
|
from .internal import digest, ldigest
|
|
119
110
|
from .microphones import MicGeom
|
|
120
111
|
|
|
121
112
|
|
|
122
|
-
class
|
|
123
|
-
"""Base class for any generating signal processing block.
|
|
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):
|
|
113
|
+
class MaskedTimeOut(TimeOut):
|
|
197
114
|
"""Signal processing block for channel and sample selection.
|
|
198
115
|
|
|
199
116
|
This class serves as intermediary to define (in)valid
|
|
200
117
|
channels and samples for any
|
|
201
118
|
:class:`~acoular.sources.SamplesGenerator` (or derived) object.
|
|
202
|
-
It gets samples from :attr:`~acoular.
|
|
119
|
+
It gets samples from :attr:`~acoular.base.TimeOut.source`
|
|
203
120
|
and generates output via the generator :meth:`result`.
|
|
204
121
|
"""
|
|
205
122
|
|
|
123
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
124
|
+
source = Instance(SamplesGenerator)
|
|
125
|
+
|
|
206
126
|
#: Index of the first sample to be considered valid.
|
|
207
127
|
start = CLong(0, desc='start of valid samples')
|
|
208
128
|
|
|
@@ -215,10 +135,10 @@ class MaskedTimeInOut(TimeInOut):
|
|
|
215
135
|
#: Channel mask to serve as an index for all valid channels, is set automatically.
|
|
216
136
|
channels = Property(depends_on=['invalid_channels', 'source.numchannels'], desc='channel mask')
|
|
217
137
|
|
|
218
|
-
#: Number of channels in input, as given by :attr:`~acoular.
|
|
138
|
+
#: Number of channels in input, as given by :attr:`~acoular.base.TimeOut.source`.
|
|
219
139
|
numchannels_total = Delegate('source', 'numchannels')
|
|
220
140
|
|
|
221
|
-
#: Number of samples in input, as given by :attr:`~acoular.
|
|
141
|
+
#: Number of samples in input, as given by :attr:`~acoular.base.TimeOut.source`.
|
|
222
142
|
numsamples_total = Delegate('source', 'numsamples')
|
|
223
143
|
|
|
224
144
|
#: Number of valid channels, is set automatically.
|
|
@@ -323,11 +243,14 @@ class MaskedTimeInOut(TimeInOut):
|
|
|
323
243
|
yield block[:, self.channels]
|
|
324
244
|
|
|
325
245
|
|
|
326
|
-
class ChannelMixer(
|
|
246
|
+
class ChannelMixer(TimeOut):
|
|
327
247
|
"""Class for directly mixing the channels of a multi-channel source.
|
|
328
248
|
Outputs a single channel.
|
|
329
249
|
"""
|
|
330
250
|
|
|
251
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
252
|
+
source = Instance(SamplesGenerator)
|
|
253
|
+
|
|
331
254
|
#: Amplitude weight(s) for the channels as array. If not set, all channels are equally weighted.
|
|
332
255
|
weights = CArray(desc='channel weights')
|
|
333
256
|
|
|
@@ -369,7 +292,7 @@ class ChannelMixer(TimeInOut):
|
|
|
369
292
|
yield sum(weights * block, 1, keepdims=True)
|
|
370
293
|
|
|
371
294
|
|
|
372
|
-
class Trigger(
|
|
295
|
+
class Trigger(TimeOut):
|
|
373
296
|
"""Class for identifying trigger signals.
|
|
374
297
|
Gets samples from :attr:`source` and stores the trigger samples in :meth:`trigger_data`.
|
|
375
298
|
|
|
@@ -383,7 +306,7 @@ class Trigger(TimeInOut):
|
|
|
383
306
|
vary too much.
|
|
384
307
|
"""
|
|
385
308
|
|
|
386
|
-
#: Data source; :class:`~acoular.
|
|
309
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
387
310
|
source = Instance(SamplesGenerator)
|
|
388
311
|
|
|
389
312
|
#: Threshold of trigger. Has different meanings for different
|
|
@@ -467,20 +390,20 @@ class Trigger(TimeInOut):
|
|
|
467
390
|
def _get_trigger_data(self):
|
|
468
391
|
self._check_trigger_existence()
|
|
469
392
|
triggerFunc = {'dirac': self._trigger_dirac, 'rect': self._trigger_rect}[self.trigger_type]
|
|
470
|
-
|
|
471
|
-
threshold = self._threshold(
|
|
393
|
+
num = 2048 # number samples for result-method of source
|
|
394
|
+
threshold = self._threshold(num)
|
|
472
395
|
|
|
473
396
|
# get all samples which surpasse the threshold
|
|
474
397
|
peakLoc = array([], dtype='int') # all indices which surpasse the threshold
|
|
475
|
-
|
|
398
|
+
trigger_data = array([])
|
|
476
399
|
x0 = []
|
|
477
400
|
dSamples = 0
|
|
478
|
-
for triggerSignal in self.source.result(
|
|
401
|
+
for triggerSignal in self.source.result(num):
|
|
479
402
|
localTrigger = flatnonzero(triggerFunc(x0, triggerSignal, threshold))
|
|
480
403
|
if len(localTrigger) != 0:
|
|
481
404
|
peakLoc = append(peakLoc, localTrigger + dSamples)
|
|
482
|
-
|
|
483
|
-
dSamples +=
|
|
405
|
+
trigger_data = append(trigger_data, triggerSignal[localTrigger])
|
|
406
|
+
dSamples += num
|
|
484
407
|
x0 = triggerSignal[-1]
|
|
485
408
|
if len(peakLoc) <= 1:
|
|
486
409
|
msg = 'Not enough trigger info. Check *threshold* sign and value!'
|
|
@@ -498,12 +421,12 @@ class Trigger(TimeInOut):
|
|
|
498
421
|
peakLocHelp = multiplePeaksWithinHunk[0]
|
|
499
422
|
indHelp = [peakLocHelp, peakLocHelp + 1]
|
|
500
423
|
if self.multiple_peaks_in_hunk == 'extremum':
|
|
501
|
-
values =
|
|
424
|
+
values = trigger_data[indHelp]
|
|
502
425
|
deleteInd = indHelp[argmin(abs(values))]
|
|
503
426
|
elif self.multiple_peaks_in_hunk == 'first':
|
|
504
427
|
deleteInd = indHelp[1]
|
|
505
428
|
peakLoc = delete(peakLoc, deleteInd)
|
|
506
|
-
|
|
429
|
+
trigger_data = delete(trigger_data, deleteInd)
|
|
507
430
|
peakDist = peakLoc[1:] - peakLoc[:-1]
|
|
508
431
|
multiplePeaksWithinHunk = flatnonzero(peakDist < self.hunk_length * maxPeakDist)
|
|
509
432
|
|
|
@@ -520,7 +443,7 @@ class Trigger(TimeInOut):
|
|
|
520
443
|
)
|
|
521
444
|
return peakLoc, max(peakDist), min(peakDist)
|
|
522
445
|
|
|
523
|
-
def _trigger_dirac(self, x0, x, threshold):
|
|
446
|
+
def _trigger_dirac(self, x0, x, threshold): # noqa: ARG002
|
|
524
447
|
# x0 not needed here, but needed in _trigger_rect
|
|
525
448
|
return self._trigger_value_comp(x, threshold)
|
|
526
449
|
|
|
@@ -530,20 +453,20 @@ class Trigger(TimeInOut):
|
|
|
530
453
|
# indPeakHunk = abs(xNew[1:] - xNew[:-1]) > abs(threshold) # with this line: every edge would be located
|
|
531
454
|
return self._trigger_value_comp(xNew[1:] - xNew[:-1], threshold)
|
|
532
455
|
|
|
533
|
-
def _trigger_value_comp(self,
|
|
534
|
-
return
|
|
456
|
+
def _trigger_value_comp(self, trigger_data, threshold):
|
|
457
|
+
return trigger_data > threshold if threshold > 0.0 else trigger_data < threshold
|
|
535
458
|
|
|
536
|
-
def _threshold(self,
|
|
459
|
+
def _threshold(self, num):
|
|
537
460
|
if self.threshold is None: # take a guessed threshold
|
|
538
461
|
# get max and min values of whole trigger signal
|
|
539
462
|
maxVal = -inf
|
|
540
463
|
minVal = inf
|
|
541
464
|
meanVal = 0
|
|
542
465
|
cntMean = 0
|
|
543
|
-
for
|
|
544
|
-
maxVal = max(maxVal,
|
|
545
|
-
minVal = min(minVal,
|
|
546
|
-
meanVal +=
|
|
466
|
+
for trigger_data in self.source.result(num):
|
|
467
|
+
maxVal = max(maxVal, trigger_data.max())
|
|
468
|
+
minVal = min(minVal, trigger_data.min())
|
|
469
|
+
meanVal += trigger_data.mean()
|
|
547
470
|
cntMean += 1
|
|
548
471
|
meanVal /= cntMean
|
|
549
472
|
|
|
@@ -563,7 +486,7 @@ class Trigger(TimeInOut):
|
|
|
563
486
|
return 0
|
|
564
487
|
|
|
565
488
|
|
|
566
|
-
class AngleTracker(
|
|
489
|
+
class AngleTracker(MaskedTimeOut):
|
|
567
490
|
"""Calculates rotation angle and rpm per sample from a trigger signal
|
|
568
491
|
using spline interpolation in the time domain.
|
|
569
492
|
|
|
@@ -571,9 +494,6 @@ class AngleTracker(MaskedTimeInOut):
|
|
|
571
494
|
|
|
572
495
|
"""
|
|
573
496
|
|
|
574
|
-
#: Data source; :class:`~acoular.tprocess.SamplesGenerator` or derived object.
|
|
575
|
-
source = Instance(SamplesGenerator)
|
|
576
|
-
|
|
577
497
|
#: Trigger data from :class:`acoular.tprocess.Trigger`.
|
|
578
498
|
trigger = Instance(Trigger)
|
|
579
499
|
|
|
@@ -642,18 +562,18 @@ class AngleTracker(MaskedTimeInOut):
|
|
|
642
562
|
# init
|
|
643
563
|
ind = 0
|
|
644
564
|
# trigger data
|
|
645
|
-
peakloc, maxdist, mindist = self.trigger.
|
|
565
|
+
peakloc, maxdist, mindist = self.trigger.trigger_data()
|
|
646
566
|
TriggerPerRevo = self.trigger_per_revo
|
|
647
567
|
rotDirection = self.rot_direction
|
|
648
|
-
|
|
568
|
+
num = self.source.numsamples
|
|
649
569
|
samplerate = self.source.sample_freq
|
|
650
|
-
self._rpm = zeros(
|
|
651
|
-
self._angle = zeros(
|
|
570
|
+
self._rpm = zeros(num)
|
|
571
|
+
self._angle = zeros(num)
|
|
652
572
|
# number of spline points
|
|
653
573
|
InterpPoints = self.interp_points
|
|
654
574
|
|
|
655
575
|
# loop over alle timesamples
|
|
656
|
-
while ind <
|
|
576
|
+
while ind < num:
|
|
657
577
|
# when starting spline forward
|
|
658
578
|
if ind < peakloc[InterpPoints]:
|
|
659
579
|
peakdist = (
|
|
@@ -716,17 +636,20 @@ class AngleTracker(MaskedTimeInOut):
|
|
|
716
636
|
|
|
717
637
|
"""
|
|
718
638
|
# trigger indices data
|
|
719
|
-
peakloc = self.trigger.
|
|
639
|
+
peakloc = self.trigger.trigger_data()[0]
|
|
720
640
|
# calculation of average rpm in 1/min
|
|
721
641
|
return (len(peakloc) - 1) / (peakloc[-1] - peakloc[0]) / self.trigger_per_revo * self.source.sample_freq * 60
|
|
722
642
|
|
|
723
643
|
|
|
724
|
-
class SpatialInterpolator(
|
|
644
|
+
class SpatialInterpolator(TimeOut):
|
|
725
645
|
"""Base class for spatial interpolation of microphone data.
|
|
726
646
|
Gets samples from :attr:`source` and generates output via the
|
|
727
647
|
generator :meth:`result`.
|
|
728
648
|
"""
|
|
729
649
|
|
|
650
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
651
|
+
source = Instance(SamplesGenerator)
|
|
652
|
+
|
|
730
653
|
#: :class:`~acoular.microphones.MicGeom` object that provides the real microphone locations.
|
|
731
654
|
mics = Instance(MicGeom(), desc='microphone geometry')
|
|
732
655
|
|
|
@@ -743,9 +666,6 @@ class SpatialInterpolator(TimeInOut):
|
|
|
743
666
|
def _set_mics_virtual(self, mics_virtual):
|
|
744
667
|
self._mics_virtual = mics_virtual
|
|
745
668
|
|
|
746
|
-
#: Data source; :class:`~acoular.tprocess.SamplesGenerator` or derived object.
|
|
747
|
-
source = Instance(SamplesGenerator)
|
|
748
|
-
|
|
749
669
|
#: Interpolation method in spacial domain, defaults to linear
|
|
750
670
|
#: linear uses numpy linear interpolation
|
|
751
671
|
#: spline uses scipy CloughTocher algorithm
|
|
@@ -784,7 +704,7 @@ class SpatialInterpolator(TimeInOut):
|
|
|
784
704
|
#: The transformation is done via [x,y,z]_mod = Q * [x,y,z]. (default is Identity).
|
|
785
705
|
Q = CArray(dtype=float64, shape=(3, 3), value=identity(3))
|
|
786
706
|
|
|
787
|
-
num_IDW = Trait(3, dtype=int, desc='number of neighboring microphones, DEFAULT=3')
|
|
707
|
+
num_IDW = Trait(3, dtype=int, desc='number of neighboring microphones, DEFAULT=3') # noqa: N815
|
|
788
708
|
|
|
789
709
|
p_weight = Trait(
|
|
790
710
|
2,
|
|
@@ -793,7 +713,7 @@ class SpatialInterpolator(TimeInOut):
|
|
|
793
713
|
)
|
|
794
714
|
|
|
795
715
|
#: Stores the output of :meth:`_virtNewCoord_func`; Read-Only
|
|
796
|
-
_virtNewCoord_func = Property(
|
|
716
|
+
_virtNewCoord_func = Property( # noqa: N815
|
|
797
717
|
depends_on=['mics.digest', 'mics_virtual.digest', 'method', 'array_dimension', 'interp_at_zero'],
|
|
798
718
|
)
|
|
799
719
|
|
|
@@ -818,21 +738,21 @@ class SpatialInterpolator(TimeInOut):
|
|
|
818
738
|
return digest(self)
|
|
819
739
|
|
|
820
740
|
@cached_property
|
|
821
|
-
def _get_virtNewCoord(self):
|
|
741
|
+
def _get_virtNewCoord(self): # noqa N802
|
|
822
742
|
return self._virtNewCoord_func(self.mics.mpos, self.mics_virtual.mpos, self.method, self.array_dimension)
|
|
823
743
|
|
|
824
744
|
def sinc_mic(self, r):
|
|
825
745
|
"""Modified Sinc function for Radial Basis function approximation."""
|
|
826
746
|
return sinc((r * self.mics_virtual.mpos.shape[1]) / (pi))
|
|
827
747
|
|
|
828
|
-
def _virtNewCoord_func(self,
|
|
829
|
-
"""Core functionality for getting the
|
|
748
|
+
def _virtNewCoord_func(self, mpos, mpos_virt, method, array_dimension): # noqa N802
|
|
749
|
+
"""Core functionality for getting the interpolation.
|
|
830
750
|
|
|
831
751
|
Parameters
|
|
832
752
|
----------
|
|
833
|
-
|
|
753
|
+
mpos : float[3, nPhysicalMics]
|
|
834
754
|
The mic positions of the physical (really existing) mics
|
|
835
|
-
|
|
755
|
+
mpos_virt : float[3, nVirtualMics]
|
|
836
756
|
The mic positions of the virtual mics
|
|
837
757
|
method : string
|
|
838
758
|
The Interpolation method to use
|
|
@@ -864,11 +784,11 @@ class SpatialInterpolator(TimeInOut):
|
|
|
864
784
|
|
|
865
785
|
"""
|
|
866
786
|
# init positions of virtual mics in cyl coordinates
|
|
867
|
-
nVirtMics =
|
|
787
|
+
nVirtMics = mpos_virt.shape[1]
|
|
868
788
|
virtNewCoord = zeros((3, nVirtMics))
|
|
869
789
|
virtNewCoord.fill(nan)
|
|
870
790
|
# init real positions in cyl coordinates
|
|
871
|
-
nMics =
|
|
791
|
+
nMics = mpos.shape[1]
|
|
872
792
|
newCoord = zeros((3, nMics))
|
|
873
793
|
newCoord.fill(nan)
|
|
874
794
|
# empty mesh object
|
|
@@ -876,24 +796,24 @@ class SpatialInterpolator(TimeInOut):
|
|
|
876
796
|
|
|
877
797
|
if self.array_dimension == '1D' or self.array_dimension == 'ring':
|
|
878
798
|
# get projections onto new coordinate, for real mics
|
|
879
|
-
projectionOnNewAxis = cartToCyl(
|
|
799
|
+
projectionOnNewAxis = cartToCyl(mpos, self.Q)[0]
|
|
880
800
|
indReorderHelp = argsort(projectionOnNewAxis)
|
|
881
801
|
mesh.append([projectionOnNewAxis[indReorderHelp], indReorderHelp])
|
|
882
802
|
|
|
883
803
|
# new coordinates of real mics
|
|
884
|
-
indReorderHelp = argsort(cartToCyl(
|
|
885
|
-
newCoord = (cartToCyl(
|
|
804
|
+
indReorderHelp = argsort(cartToCyl(mpos, self.Q)[0])
|
|
805
|
+
newCoord = (cartToCyl(mpos, self.Q).T)[indReorderHelp].T
|
|
886
806
|
|
|
887
807
|
# and for virtual mics
|
|
888
|
-
virtNewCoord = cartToCyl(
|
|
808
|
+
virtNewCoord = cartToCyl(mpos_virt)
|
|
889
809
|
|
|
890
810
|
elif self.array_dimension == '2D': # 2d case0
|
|
891
811
|
# get virtual mic projections on new coord system
|
|
892
|
-
virtNewCoord = cartToCyl(
|
|
812
|
+
virtNewCoord = cartToCyl(mpos_virt, self.Q)
|
|
893
813
|
|
|
894
814
|
# new coordinates of real mics
|
|
895
|
-
indReorderHelp = argsort(cartToCyl(
|
|
896
|
-
newCoord = cartToCyl(
|
|
815
|
+
indReorderHelp = argsort(cartToCyl(mpos, self.Q)[0])
|
|
816
|
+
newCoord = cartToCyl(mpos, self.Q)
|
|
897
817
|
|
|
898
818
|
# scipy delauney triangulation
|
|
899
819
|
# Delaunay
|
|
@@ -929,10 +849,10 @@ class SpatialInterpolator(TimeInOut):
|
|
|
929
849
|
|
|
930
850
|
elif self.array_dimension == '3D': # 3d case
|
|
931
851
|
# get virtual mic projections on new coord system
|
|
932
|
-
virtNewCoord = cartToCyl(
|
|
852
|
+
virtNewCoord = cartToCyl(mpos_virt, self.Q)
|
|
933
853
|
# get real mic projections on new coord system
|
|
934
|
-
indReorderHelp = argsort(cartToCyl(
|
|
935
|
-
newCoord = cartToCyl(
|
|
854
|
+
indReorderHelp = argsort(cartToCyl(mpos, self.Q)[0])
|
|
855
|
+
newCoord = cartToCyl(mpos, self.Q)
|
|
936
856
|
# Delaunay
|
|
937
857
|
tri = Delaunay(newCoord.T, incremental=True) # , incremental=True,qhull_options = "Qc QJ Q12"
|
|
938
858
|
|
|
@@ -966,14 +886,14 @@ class SpatialInterpolator(TimeInOut):
|
|
|
966
886
|
|
|
967
887
|
return mesh, virtNewCoord, newCoord
|
|
968
888
|
|
|
969
|
-
def _result_core_func(self, p,
|
|
889
|
+
def _result_core_func(self, p, phi_delay=None, period=None, Q=Q, interp_at_zero=False): # noqa: N803, ARG002 (see #226)
|
|
970
890
|
"""Performs the actual Interpolation.
|
|
971
891
|
|
|
972
892
|
Parameters
|
|
973
893
|
----------
|
|
974
|
-
p : float[
|
|
894
|
+
p : float[num, nMicsReal]
|
|
975
895
|
The pressure field of the yielded sample at real mics.
|
|
976
|
-
|
|
896
|
+
phi_delay : empty list (default) or float[num]
|
|
977
897
|
If passed (rotational case), this list contains the angular delay
|
|
978
898
|
of each sample in rad.
|
|
979
899
|
period : None (default) or float
|
|
@@ -982,12 +902,12 @@ class SpatialInterpolator(TimeInOut):
|
|
|
982
902
|
|
|
983
903
|
Returns
|
|
984
904
|
-------
|
|
985
|
-
pInterp : float[
|
|
905
|
+
pInterp : float[num, nMicsVirtual]
|
|
986
906
|
The interpolated time data at the virtual mics
|
|
987
907
|
|
|
988
908
|
"""
|
|
989
|
-
if
|
|
990
|
-
|
|
909
|
+
if phi_delay is None:
|
|
910
|
+
phi_delay = []
|
|
991
911
|
# number of time samples
|
|
992
912
|
nTime = p.shape[0]
|
|
993
913
|
# number of virtual mixcs
|
|
@@ -1015,13 +935,13 @@ class SpatialInterpolator(TimeInOut):
|
|
|
1015
935
|
|
|
1016
936
|
# Interpolation for 1D Arrays
|
|
1017
937
|
if self.array_dimension == '1D' or self.array_dimension == 'ring':
|
|
1018
|
-
# for rotation add
|
|
1019
|
-
if not array_equal(
|
|
1020
|
-
xInterpHelp =
|
|
938
|
+
# for rotation add phi_delay
|
|
939
|
+
if not array_equal(phi_delay, []):
|
|
940
|
+
xInterpHelp = tile(virtNewCoord[0, :], (nTime, 1)) + tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1021
941
|
xInterp = ((xInterpHelp + pi) % (2 * pi)) - pi # shifting phi cootrdinate into feasible area [-pi, pi]
|
|
1022
942
|
# if no rotation given
|
|
1023
943
|
else:
|
|
1024
|
-
xInterp =
|
|
944
|
+
xInterp = tile(virtNewCoord[0, :], (nTime, 1))
|
|
1025
945
|
# get ordered microphone posions in radiant
|
|
1026
946
|
x = newCoord[0]
|
|
1027
947
|
for cntTime in range(nTime):
|
|
@@ -1074,11 +994,11 @@ class SpatialInterpolator(TimeInOut):
|
|
|
1074
994
|
# Interpolation for arbitrary 2D Arrays
|
|
1075
995
|
elif self.array_dimension == '2D':
|
|
1076
996
|
# check rotation
|
|
1077
|
-
if not array_equal(
|
|
1078
|
-
xInterpHelp =
|
|
997
|
+
if not array_equal(phi_delay, []):
|
|
998
|
+
xInterpHelp = tile(virtNewCoord[0, :], (nTime, 1)) + tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1079
999
|
xInterp = ((xInterpHelp + pi) % (2 * pi)) - pi # shifting phi cootrdinate into feasible area [-pi, pi]
|
|
1080
1000
|
else:
|
|
1081
|
-
xInterp =
|
|
1001
|
+
xInterp = tile(virtNewCoord[0, :], (nTime, 1))
|
|
1082
1002
|
|
|
1083
1003
|
mesh = meshList[0][0]
|
|
1084
1004
|
for cntTime in range(nTime):
|
|
@@ -1137,7 +1057,7 @@ class SpatialInterpolator(TimeInOut):
|
|
|
1137
1057
|
newPoint3_M = append(newPoint2_M, zeros([1, self.numchannels]), axis=0)
|
|
1138
1058
|
newPointCart = cylToCart(newPoint3_M)
|
|
1139
1059
|
for ind in arange(len(newPoint[:, 0])):
|
|
1140
|
-
newPoint_Rep =
|
|
1060
|
+
newPoint_Rep = tile(newPointCart[:, ind], (len(newPoint[:, 0]), 1)).T
|
|
1141
1061
|
subtract = newPoint_Rep - newCoordCart
|
|
1142
1062
|
normDistance = norm(subtract, axis=0)
|
|
1143
1063
|
index_norm = argsort(normDistance)[: self.num_IDW]
|
|
@@ -1153,11 +1073,11 @@ class SpatialInterpolator(TimeInOut):
|
|
|
1153
1073
|
# Interpolation for arbitrary 3D Arrays
|
|
1154
1074
|
elif self.array_dimension == '3D':
|
|
1155
1075
|
# check rotation
|
|
1156
|
-
if not array_equal(
|
|
1157
|
-
xInterpHelp =
|
|
1076
|
+
if not array_equal(phi_delay, []):
|
|
1077
|
+
xInterpHelp = tile(virtNewCoord[0, :], (nTime, 1)) + tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1158
1078
|
xInterp = ((xInterpHelp + pi) % (2 * pi)) - pi # shifting phi cootrdinate into feasible area [-pi, pi]
|
|
1159
1079
|
else:
|
|
1160
|
-
xInterp =
|
|
1080
|
+
xInterp = tile(virtNewCoord[0, :], (nTime, 1))
|
|
1161
1081
|
|
|
1162
1082
|
mesh = meshList[0][0]
|
|
1163
1083
|
for cntTime in range(nTime):
|
|
@@ -1253,12 +1173,12 @@ class SpatialInterpolatorRotation(SpatialInterpolator):
|
|
|
1253
1173
|
# period for rotation
|
|
1254
1174
|
period = 2 * pi
|
|
1255
1175
|
# get angle
|
|
1256
|
-
angle = self.angle_source.
|
|
1176
|
+
angle = self.angle_source.angle()
|
|
1257
1177
|
# counter to track angle position in time for each block
|
|
1258
1178
|
count = 0
|
|
1259
1179
|
for timeData in self.source.result(num):
|
|
1260
|
-
|
|
1261
|
-
interpVal = self._result_core_func(timeData,
|
|
1180
|
+
phi_delay = angle[count : count + num]
|
|
1181
|
+
interpVal = self._result_core_func(timeData, phi_delay, period, self.Q, interp_at_zero=False)
|
|
1262
1182
|
yield interpVal
|
|
1263
1183
|
count += num
|
|
1264
1184
|
|
|
@@ -1311,19 +1231,19 @@ class SpatialInterpolatorConstantRotation(SpatialInterpolator):
|
|
|
1311
1231
|
phiOffset = 0.0
|
|
1312
1232
|
for timeData in self.source.result(num):
|
|
1313
1233
|
nTime = timeData.shape[0]
|
|
1314
|
-
|
|
1315
|
-
interpVal = self._result_core_func(timeData,
|
|
1316
|
-
phiOffset =
|
|
1234
|
+
phi_delay = phiOffset + linspace(0, nTime / self.sample_freq * omega, nTime, endpoint=False)
|
|
1235
|
+
interpVal = self._result_core_func(timeData, phi_delay, period, self.Q, interp_at_zero=False)
|
|
1236
|
+
phiOffset = phi_delay[-1] + omega / self.sample_freq
|
|
1317
1237
|
yield interpVal
|
|
1318
1238
|
|
|
1319
1239
|
|
|
1320
|
-
class Mixer(
|
|
1240
|
+
class Mixer(TimeOut):
|
|
1321
1241
|
"""Mixes the signals from several sources."""
|
|
1322
1242
|
|
|
1323
|
-
#: Data source; :class:`~acoular.
|
|
1243
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` object.
|
|
1324
1244
|
source = Trait(SamplesGenerator)
|
|
1325
1245
|
|
|
1326
|
-
#: List of additional :class:`~acoular.
|
|
1246
|
+
#: List of additional :class:`~acoular.base.SamplesGenerator` objects
|
|
1327
1247
|
#: to be mixed.
|
|
1328
1248
|
sources = List(Instance(SamplesGenerator, ()))
|
|
1329
1249
|
|
|
@@ -1340,7 +1260,7 @@ class Mixer(TimeInOut):
|
|
|
1340
1260
|
sdigest = Str()
|
|
1341
1261
|
|
|
1342
1262
|
@observe('sources.items.digest')
|
|
1343
|
-
def
|
|
1263
|
+
def _set_sourcesdigest(self, event): # noqa ARG002
|
|
1344
1264
|
self.sdigest = ldigest(self.sources)
|
|
1345
1265
|
|
|
1346
1266
|
# internal identifier
|
|
@@ -1395,9 +1315,12 @@ class Mixer(TimeInOut):
|
|
|
1395
1315
|
break
|
|
1396
1316
|
|
|
1397
1317
|
|
|
1398
|
-
class TimePower(
|
|
1318
|
+
class TimePower(TimeOut):
|
|
1399
1319
|
"""Calculates time-depended power of the signal."""
|
|
1400
1320
|
|
|
1321
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1322
|
+
source = Instance(SamplesGenerator)
|
|
1323
|
+
|
|
1401
1324
|
def result(self, num):
|
|
1402
1325
|
"""Python generator that yields the output block-wise.
|
|
1403
1326
|
|
|
@@ -1418,64 +1341,12 @@ class TimePower(TimeInOut):
|
|
|
1418
1341
|
yield temp * temp
|
|
1419
1342
|
|
|
1420
1343
|
|
|
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):
|
|
1344
|
+
class TimeCumAverage(TimeOut):
|
|
1477
1345
|
"""Calculates cumulative average of the signal, useful for Leq."""
|
|
1478
1346
|
|
|
1347
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1348
|
+
source = Instance(SamplesGenerator)
|
|
1349
|
+
|
|
1479
1350
|
def result(self, num):
|
|
1480
1351
|
"""Python generator that yields the output block-wise.
|
|
1481
1352
|
|
|
@@ -1503,9 +1374,12 @@ class TimeCumAverage(TimeInOut):
|
|
|
1503
1374
|
yield temp
|
|
1504
1375
|
|
|
1505
1376
|
|
|
1506
|
-
class TimeReverse(
|
|
1377
|
+
class TimeReverse(TimeOut):
|
|
1507
1378
|
"""Calculates the time-reversed signal of a source."""
|
|
1508
1379
|
|
|
1380
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1381
|
+
source = Instance(SamplesGenerator)
|
|
1382
|
+
|
|
1509
1383
|
def result(self, num):
|
|
1510
1384
|
"""Python generator that yields the output block-wise.
|
|
1511
1385
|
|
|
@@ -1522,20 +1396,20 @@ class TimeReverse(TimeInOut):
|
|
|
1522
1396
|
The last block may be shorter than num.
|
|
1523
1397
|
|
|
1524
1398
|
"""
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
temp = empty_like(
|
|
1528
|
-
h =
|
|
1399
|
+
result_list = []
|
|
1400
|
+
result_list.extend(self.source.result(num))
|
|
1401
|
+
temp = empty_like(result_list[0])
|
|
1402
|
+
h = result_list.pop()
|
|
1529
1403
|
nsh = h.shape[0]
|
|
1530
1404
|
temp[:nsh] = h[::-1]
|
|
1531
|
-
for h in
|
|
1405
|
+
for h in result_list[::-1]:
|
|
1532
1406
|
temp[nsh:] = h[: nsh - 1 : -1]
|
|
1533
1407
|
yield temp
|
|
1534
1408
|
temp[:nsh] = h[nsh - 1 :: -1]
|
|
1535
1409
|
yield temp[:nsh]
|
|
1536
1410
|
|
|
1537
1411
|
|
|
1538
|
-
class Filter(
|
|
1412
|
+
class Filter(TimeOut):
|
|
1539
1413
|
"""Abstract base class for IIR filters based on scipy lfilter
|
|
1540
1414
|
implements a filter with coefficients that may be changed
|
|
1541
1415
|
during processing.
|
|
@@ -1543,6 +1417,9 @@ class Filter(TimeInOut):
|
|
|
1543
1417
|
Should not be instanciated by itself
|
|
1544
1418
|
"""
|
|
1545
1419
|
|
|
1420
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1421
|
+
source = Instance(SamplesGenerator)
|
|
1422
|
+
|
|
1546
1423
|
#: Filter coefficients
|
|
1547
1424
|
sos = Property()
|
|
1548
1425
|
|
|
@@ -1751,13 +1628,16 @@ class FiltFreqWeight(Filter):
|
|
|
1751
1628
|
return tf2sos(b, a)
|
|
1752
1629
|
|
|
1753
1630
|
|
|
1754
|
-
class FilterBank(
|
|
1631
|
+
class FilterBank(TimeOut):
|
|
1755
1632
|
"""Abstract base class for IIR filter banks based on scipy lfilter
|
|
1756
1633
|
implements a bank of parallel filters.
|
|
1757
1634
|
|
|
1758
1635
|
Should not be instanciated by itself
|
|
1759
1636
|
"""
|
|
1760
1637
|
|
|
1638
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1639
|
+
source = Instance(SamplesGenerator)
|
|
1640
|
+
|
|
1761
1641
|
#: List of filter coefficients for all filters
|
|
1762
1642
|
sos = Property()
|
|
1763
1643
|
|
|
@@ -1856,136 +1736,14 @@ class OctaveFilterBank(FilterBank):
|
|
|
1856
1736
|
return sos
|
|
1857
1737
|
|
|
1858
1738
|
|
|
1859
|
-
class
|
|
1860
|
-
"""Caches time signal in cache file."""
|
|
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):
|
|
1739
|
+
class WriteWAV(TimeOut):
|
|
1985
1740
|
"""Saves time signal from one or more channels as mono/stereo/multi-channel
|
|
1986
1741
|
`*.wav` file.
|
|
1987
1742
|
"""
|
|
1988
1743
|
|
|
1744
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1745
|
+
source = Instance(SamplesGenerator)
|
|
1746
|
+
|
|
1989
1747
|
#: Name of the file to be saved. If none is given, the name will be
|
|
1990
1748
|
#: automatically generated from the sources.
|
|
1991
1749
|
name = File(filter=['*.wav'], desc='name of wave file')
|
|
@@ -2048,9 +1806,12 @@ class WriteWAV(TimeInOut):
|
|
|
2048
1806
|
wf.close()
|
|
2049
1807
|
|
|
2050
1808
|
|
|
2051
|
-
class WriteH5(
|
|
1809
|
+
class WriteH5(TimeOut):
|
|
2052
1810
|
"""Saves time signal as `*.h5` file."""
|
|
2053
1811
|
|
|
1812
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1813
|
+
source = Instance(SamplesGenerator)
|
|
1814
|
+
|
|
2054
1815
|
#: Name of the file to be saved. If none is given, the name will be
|
|
2055
1816
|
#: automatically generated from a time stamp.
|
|
2056
1817
|
name = File(filter=['*.h5'], desc='name of data file')
|
|
@@ -2142,165 +1903,23 @@ class WriteH5(TimeInOut):
|
|
|
2142
1903
|
break
|
|
2143
1904
|
try:
|
|
2144
1905
|
data = next(source_gen)
|
|
2145
|
-
except:
|
|
1906
|
+
except StopIteration:
|
|
2146
1907
|
break
|
|
2147
1908
|
f5h.append_data(ac, data[:anz])
|
|
2148
|
-
yield data
|
|
2149
1909
|
f5h.flush()
|
|
1910
|
+
yield data
|
|
2150
1911
|
scount += anz
|
|
2151
1912
|
f5h.close()
|
|
2152
1913
|
|
|
2153
1914
|
|
|
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()
|
|
2163
|
-
|
|
2164
|
-
def __next__(self): # this function implementation is not python 2 compatible!
|
|
2165
|
-
with self.lock:
|
|
2166
|
-
return self.it.__next__()
|
|
2167
|
-
|
|
1915
|
+
class TimeConvolve(TimeOut):
|
|
1916
|
+
"""Uniformly partitioned overlap-save method (UPOLS) for fast convolution in the frequency domain.
|
|
2168
1917
|
|
|
2169
|
-
|
|
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`) .
|
|
1918
|
+
See :cite:`Wefers2015` for details.
|
|
2173
1919
|
"""
|
|
2174
1920
|
|
|
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>`."""
|
|
1921
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
1922
|
+
source = Instance(SamplesGenerator)
|
|
2304
1923
|
|
|
2305
1924
|
#: Convolution kernel in the time domain.
|
|
2306
1925
|
#: The second dimension of the kernel array has to be either 1 or match :attr:`~SamplesGenerator.numchannels`.
|
|
@@ -2376,13 +1995,13 @@ class TimeConvolve(TimeInOut):
|
|
|
2376
1995
|
L = self.kernel.shape[0]
|
|
2377
1996
|
N = self.source.numchannels
|
|
2378
1997
|
M = self.source.numsamples
|
|
2379
|
-
|
|
1998
|
+
numblocks_kernel = int(ceil(L / num)) # number of kernel blocks
|
|
2380
1999
|
Q = int(ceil(M / num)) # number of signal blocks
|
|
2381
2000
|
R = int(ceil((L + M - 1) / num)) # number of output blocks
|
|
2382
2001
|
last_size = (L + M - 1) % num # size of final block
|
|
2383
2002
|
|
|
2384
2003
|
idx = 0
|
|
2385
|
-
|
|
2004
|
+
fdl = zeros([numblocks_kernel, num + 1, N], dtype='complex128')
|
|
2386
2005
|
buff = zeros([2 * num, N]) # time-domain input buffer
|
|
2387
2006
|
spec_sum = zeros([num + 1, N], dtype='complex128')
|
|
2388
2007
|
|
|
@@ -2392,16 +2011,16 @@ class TimeConvolve(TimeInOut):
|
|
|
2392
2011
|
|
|
2393
2012
|
# for very short signals, we are already done
|
|
2394
2013
|
if R == 1:
|
|
2395
|
-
|
|
2396
|
-
spec_sum = _spectral_sum(spec_sum,
|
|
2014
|
+
_append_to_fdl(fdl, idx, numblocks_kernel, rfft(buff, axis=0))
|
|
2015
|
+
spec_sum = _spectral_sum(spec_sum, fdl, self._kernel_blocks)
|
|
2397
2016
|
# truncate s.t. total length is L+M-1 (like numpy convolve w/ mode="full")
|
|
2398
2017
|
yield irfft(spec_sum, axis=0)[num : last_size + num]
|
|
2399
2018
|
return
|
|
2400
2019
|
|
|
2401
2020
|
# stream processing of source signal
|
|
2402
2021
|
for temp in signal_blocks:
|
|
2403
|
-
|
|
2404
|
-
spec_sum = _spectral_sum(spec_sum,
|
|
2022
|
+
_append_to_fdl(fdl, idx, numblocks_kernel, rfft(buff, axis=0))
|
|
2023
|
+
spec_sum = _spectral_sum(spec_sum, fdl, self._kernel_blocks)
|
|
2405
2024
|
yield irfft(spec_sum, axis=0)[num:]
|
|
2406
2025
|
buff = concatenate(
|
|
2407
2026
|
[buff[num:], zeros([num, N])],
|
|
@@ -2410,33 +2029,50 @@ class TimeConvolve(TimeInOut):
|
|
|
2410
2029
|
buff[num : num + temp.shape[0]] = temp # append new time-data
|
|
2411
2030
|
|
|
2412
2031
|
for _ in range(R - Q):
|
|
2413
|
-
|
|
2414
|
-
spec_sum = _spectral_sum(spec_sum,
|
|
2032
|
+
_append_to_fdl(fdl, idx, numblocks_kernel, rfft(buff, axis=0))
|
|
2033
|
+
spec_sum = _spectral_sum(spec_sum, fdl, self._kernel_blocks)
|
|
2415
2034
|
yield irfft(spec_sum, axis=0)[num:]
|
|
2416
2035
|
buff = concatenate(
|
|
2417
2036
|
[buff[num:], zeros([num, N])],
|
|
2418
2037
|
axis=0,
|
|
2419
2038
|
) # shift input buffer to the left
|
|
2420
2039
|
|
|
2421
|
-
|
|
2422
|
-
spec_sum = _spectral_sum(spec_sum,
|
|
2040
|
+
_append_to_fdl(fdl, idx, numblocks_kernel, rfft(buff, axis=0))
|
|
2041
|
+
spec_sum = _spectral_sum(spec_sum, fdl, self._kernel_blocks)
|
|
2423
2042
|
# truncate s.t. total length is L+M-1 (like numpy convolve w/ mode="full")
|
|
2424
2043
|
yield irfft(spec_sum, axis=0)[num : last_size + num]
|
|
2425
2044
|
|
|
2426
2045
|
|
|
2427
2046
|
@nb.jit(nopython=True, cache=True)
|
|
2428
|
-
def
|
|
2429
|
-
|
|
2430
|
-
idx = int(idx + 1 %
|
|
2047
|
+
def _append_to_fdl(fdl, idx, numblocks_kernel, buff):
|
|
2048
|
+
fdl[idx] = buff
|
|
2049
|
+
idx = int(idx + 1 % numblocks_kernel)
|
|
2431
2050
|
|
|
2432
2051
|
|
|
2433
2052
|
@nb.jit(nopython=True, cache=True)
|
|
2434
|
-
def _spectral_sum(out,
|
|
2435
|
-
P, B, N =
|
|
2053
|
+
def _spectral_sum(out, fdl, kb):
|
|
2054
|
+
P, B, N = kb.shape
|
|
2436
2055
|
for n in range(N):
|
|
2437
2056
|
for b in range(B):
|
|
2438
2057
|
out[b, n] = 0
|
|
2439
2058
|
for i in range(P):
|
|
2440
|
-
out[b, n] +=
|
|
2059
|
+
out[b, n] += fdl[i, b, n] * kb[i, b, n]
|
|
2441
2060
|
|
|
2442
2061
|
return out
|
|
2062
|
+
|
|
2063
|
+
|
|
2064
|
+
class MaskedTimeInOut(MaskedTimeOut):
|
|
2065
|
+
"""Signal processing block for channel and sample selection (alias for :class:`~acoular.tprocess.MaskedTimeOut`.).
|
|
2066
|
+
|
|
2067
|
+
.. deprecated:: 24.10
|
|
2068
|
+
Using :class:`~acoular.tprocess.MaskedTimeInOut` is deprecated and will be removed in Acoular
|
|
2069
|
+
version 25.07. Use :class:`~acoular.tprocess.MaskedTimeOut` instead.
|
|
2070
|
+
"""
|
|
2071
|
+
|
|
2072
|
+
def __init__(self, *args, **kwargs):
|
|
2073
|
+
super().__init__(*args, **kwargs)
|
|
2074
|
+
warn(
|
|
2075
|
+
'Using MaskedTimeInOut is deprecated and will be removed in Acoular version 25.07. Use class MaskedTimeOut instead.',
|
|
2076
|
+
DeprecationWarning,
|
|
2077
|
+
stacklevel=2,
|
|
2078
|
+
)
|