acoular 25.7__py3-none-any.whl → 26.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/aiaa/aiaa.py +8 -10
- acoular/base.py +13 -16
- acoular/calib.py +25 -24
- acoular/configuration.py +2 -2
- acoular/demo/__init__.py +97 -9
- acoular/demo/__main__.py +37 -0
- acoular/environments.py +119 -130
- acoular/fbeamform.py +438 -440
- acoular/fprocess.py +18 -13
- acoular/grids.py +122 -301
- acoular/h5cache.py +5 -1
- acoular/h5files.py +96 -9
- acoular/microphones.py +30 -35
- acoular/process.py +14 -25
- acoular/sdinput.py +9 -14
- acoular/signals.py +36 -34
- acoular/sources.py +263 -380
- acoular/spectra.py +60 -80
- acoular/tbeamform.py +242 -224
- acoular/tools/helpers.py +25 -33
- acoular/tools/metrics.py +5 -10
- acoular/tools/utils.py +168 -0
- acoular/tprocess.py +248 -271
- acoular/trajectory.py +5 -6
- acoular/version.py +2 -2
- {acoular-25.7.dist-info → acoular-26.1.dist-info}/METADATA +54 -105
- acoular-26.1.dist-info/RECORD +56 -0
- {acoular-25.7.dist-info → acoular-26.1.dist-info}/WHEEL +1 -1
- acoular/demo/acoular_demo.py +0 -135
- acoular-25.7.dist-info/RECORD +0 -56
- {acoular-25.7.dist-info → acoular-26.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {acoular-25.7.dist-info → acoular-26.1.dist-info}/licenses/LICENSE +0 -0
acoular/tprocess.py
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
"""
|
|
5
5
|
Implement blockwise processing in the time domain.
|
|
6
6
|
|
|
7
|
+
.. inheritance-diagram::
|
|
8
|
+
acoular.tprocess
|
|
9
|
+
:top-classes:
|
|
10
|
+
acoular.base.TimeOut
|
|
11
|
+
:parts: 1
|
|
12
|
+
|
|
7
13
|
.. autosummary::
|
|
8
14
|
:toctree: generated/
|
|
9
15
|
|
|
@@ -39,46 +45,9 @@ from warnings import warn
|
|
|
39
45
|
|
|
40
46
|
import numba as nb
|
|
41
47
|
import numpy as np
|
|
42
|
-
|
|
43
|
-
append,
|
|
44
|
-
arange,
|
|
45
|
-
argmax,
|
|
46
|
-
argmin,
|
|
47
|
-
argsort,
|
|
48
|
-
array,
|
|
49
|
-
array_equal,
|
|
50
|
-
asarray,
|
|
51
|
-
ceil,
|
|
52
|
-
concatenate,
|
|
53
|
-
cumsum,
|
|
54
|
-
delete,
|
|
55
|
-
empty,
|
|
56
|
-
empty_like,
|
|
57
|
-
exp,
|
|
58
|
-
flatnonzero,
|
|
59
|
-
float64,
|
|
60
|
-
identity,
|
|
61
|
-
inf,
|
|
62
|
-
interp,
|
|
63
|
-
linspace,
|
|
64
|
-
mean,
|
|
65
|
-
nan,
|
|
66
|
-
newaxis,
|
|
67
|
-
pi,
|
|
68
|
-
polymul,
|
|
69
|
-
sin,
|
|
70
|
-
sinc,
|
|
71
|
-
split,
|
|
72
|
-
sqrt,
|
|
73
|
-
stack,
|
|
74
|
-
sum, # noqa: A004
|
|
75
|
-
tile,
|
|
76
|
-
unique,
|
|
77
|
-
zeros,
|
|
78
|
-
)
|
|
48
|
+
import scipy.linalg as spla
|
|
79
49
|
from scipy.fft import irfft, rfft
|
|
80
50
|
from scipy.interpolate import CloughTocher2DInterpolator, CubicSpline, LinearNDInterpolator, Rbf, splev, splrep
|
|
81
|
-
from scipy.linalg import norm
|
|
82
51
|
from scipy.signal import bilinear, butter, sosfilt, sosfiltfilt, tf2sos
|
|
83
52
|
from scipy.spatial import Delaunay
|
|
84
53
|
from traits.api import (
|
|
@@ -101,13 +70,11 @@ from traits.api import (
|
|
|
101
70
|
Union,
|
|
102
71
|
cached_property,
|
|
103
72
|
observe,
|
|
104
|
-
on_trait_change,
|
|
105
73
|
)
|
|
106
74
|
|
|
107
75
|
# acoular imports
|
|
108
76
|
from .base import SamplesGenerator, TimeOut
|
|
109
77
|
from .configuration import config
|
|
110
|
-
from .deprecation import deprecated_alias
|
|
111
78
|
from .environments import cartToCyl, cylToCart
|
|
112
79
|
from .h5files import _get_h5file_class
|
|
113
80
|
from .internal import digest, ldigest
|
|
@@ -116,9 +83,6 @@ from .process import Cache
|
|
|
116
83
|
from .tools.utils import find_basename
|
|
117
84
|
|
|
118
85
|
|
|
119
|
-
@deprecated_alias(
|
|
120
|
-
{'numchannels_total': 'num_channels_total', 'numsamples_total': 'num_samples_total'}, removal_version='25.10'
|
|
121
|
-
)
|
|
122
86
|
class MaskedTimeOut(TimeOut):
|
|
123
87
|
"""
|
|
124
88
|
A signal processing block that allows for the selection of specific channels and time samples.
|
|
@@ -141,17 +105,17 @@ class MaskedTimeOut(TimeOut):
|
|
|
141
105
|
source = Instance(SamplesGenerator)
|
|
142
106
|
|
|
143
107
|
#: The index of the first valid sample. Default is ``0``.
|
|
144
|
-
start = CInt(0
|
|
108
|
+
start = CInt(0)
|
|
145
109
|
|
|
146
110
|
#: The index of the last valid sample (exclusive).
|
|
147
111
|
#: If set to :obj:`None`, the selection continues until the end of the available data.
|
|
148
|
-
stop = Union(None, CInt
|
|
112
|
+
stop = Union(None, CInt)
|
|
149
113
|
|
|
150
114
|
#: List of channel indices to be excluded from processing.
|
|
151
|
-
invalid_channels = List(int
|
|
115
|
+
invalid_channels = List(int)
|
|
152
116
|
|
|
153
117
|
#: A mask or index array representing valid channels. (automatically updated)
|
|
154
|
-
channels = Property(depends_on=['invalid_channels', 'source.num_channels']
|
|
118
|
+
channels = Property(depends_on=['invalid_channels', 'source.num_channels'])
|
|
155
119
|
|
|
156
120
|
#: Total number of input channels, including invalid channels, as given by
|
|
157
121
|
#: :attr:`~acoular.base.TimeOut.source`. (read-only).
|
|
@@ -161,19 +125,15 @@ class MaskedTimeOut(TimeOut):
|
|
|
161
125
|
num_samples_total = Delegate('source', 'num_samples')
|
|
162
126
|
|
|
163
127
|
#: Number of valid input channels after excluding :attr:`invalid_channels`. (read-only)
|
|
164
|
-
num_channels = Property(
|
|
165
|
-
depends_on=['invalid_channels', 'source.num_channels'], desc='number of valid input channels'
|
|
166
|
-
)
|
|
128
|
+
num_channels = Property(depends_on=['invalid_channels', 'source.num_channels'])
|
|
167
129
|
|
|
168
130
|
#: Number of valid time-domain samples, based on :attr:`start` and :attr:`stop` indices.
|
|
169
131
|
#: (read-only)
|
|
170
|
-
num_samples = Property(
|
|
171
|
-
depends_on=['start', 'stop', 'source.num_samples'], desc='number of valid samples per channel'
|
|
172
|
-
)
|
|
132
|
+
num_samples = Property(depends_on=['start', 'stop', 'source.num_samples'])
|
|
173
133
|
|
|
174
134
|
#: The name of the cache file (without extension). It serves as an internal reference for data
|
|
175
135
|
#: caching and tracking processed files. (automatically generated)
|
|
176
|
-
basename = Property(depends_on=['source.digest']
|
|
136
|
+
basename = Property(depends_on=['source.digest'])
|
|
177
137
|
|
|
178
138
|
#: A unique identifier for the object, based on its properties. (read-only)
|
|
179
139
|
digest = Property(depends_on=['source.digest', 'start', 'stop', 'invalid_channels'])
|
|
@@ -199,7 +159,7 @@ class MaskedTimeOut(TimeOut):
|
|
|
199
159
|
if len(self.invalid_channels) == 0:
|
|
200
160
|
return slice(0, None, None)
|
|
201
161
|
allr = [i for i in range(self.num_channels_total) if i not in self.invalid_channels]
|
|
202
|
-
return array(allr)
|
|
162
|
+
return np.array(allr)
|
|
203
163
|
|
|
204
164
|
@cached_property
|
|
205
165
|
def _get_num_channels(self):
|
|
@@ -251,7 +211,7 @@ class MaskedTimeOut(TimeOut):
|
|
|
251
211
|
offset = -start % num
|
|
252
212
|
if offset == 0:
|
|
253
213
|
offset = num
|
|
254
|
-
buf = empty((num + offset, self.num_channels), dtype=float)
|
|
214
|
+
buf = np.empty((num + offset, self.num_channels), dtype=float)
|
|
255
215
|
bsize = 0
|
|
256
216
|
i = 0
|
|
257
217
|
fblock = True
|
|
@@ -310,7 +270,7 @@ class ChannelMixer(TimeOut):
|
|
|
310
270
|
#: If not explicitly set, all channels are weighted equally (delault is ``1``).
|
|
311
271
|
#: The shape of :attr:`weights` must match the :attr:`number of input channels<num_channels>`.
|
|
312
272
|
#: If an incompatible shape is provided, a :obj:`ValueError` will be raised.
|
|
313
|
-
weights = CArray(
|
|
273
|
+
weights = CArray()
|
|
314
274
|
|
|
315
275
|
#: The number of output channels, which is always ``1`` for this class since it produces a
|
|
316
276
|
#: single mixed output. (read-only)
|
|
@@ -360,7 +320,7 @@ class ChannelMixer(TimeOut):
|
|
|
360
320
|
weights = 1
|
|
361
321
|
|
|
362
322
|
for block in self.source.result(num):
|
|
363
|
-
yield sum(weights * block, 1, keepdims=True)
|
|
323
|
+
yield np.sum(weights * block, 1, keepdims=True)
|
|
364
324
|
|
|
365
325
|
|
|
366
326
|
class Trigger(TimeOut): # pragma: no cover
|
|
@@ -472,15 +432,15 @@ class Trigger(TimeOut): # pragma: no cover
|
|
|
472
432
|
threshold = self._threshold(num)
|
|
473
433
|
|
|
474
434
|
# get all samples which surpasse the threshold
|
|
475
|
-
peakLoc = array([], dtype='int') # all indices which surpasse the threshold
|
|
476
|
-
trigger_data = array([])
|
|
435
|
+
peakLoc = np.array([], dtype='int') # all indices which surpasse the threshold
|
|
436
|
+
trigger_data = np.array([])
|
|
477
437
|
x0 = []
|
|
478
438
|
dSamples = 0
|
|
479
439
|
for triggerSignal in self.source.result(num):
|
|
480
|
-
localTrigger = flatnonzero(triggerFunc(x0, triggerSignal, threshold))
|
|
440
|
+
localTrigger = np.flatnonzero(triggerFunc(x0, triggerSignal, threshold))
|
|
481
441
|
if len(localTrigger) != 0:
|
|
482
|
-
peakLoc = append(peakLoc, localTrigger + dSamples)
|
|
483
|
-
trigger_data = append(trigger_data, triggerSignal[localTrigger])
|
|
442
|
+
peakLoc = np.append(peakLoc, localTrigger + dSamples)
|
|
443
|
+
trigger_data = np.append(trigger_data, triggerSignal[localTrigger])
|
|
484
444
|
dSamples += num
|
|
485
445
|
x0 = triggerSignal[-1]
|
|
486
446
|
if len(peakLoc) <= 1:
|
|
@@ -494,24 +454,24 @@ class Trigger(TimeOut): # pragma: no cover
|
|
|
494
454
|
# which peak is the correct one -> delete the other one.
|
|
495
455
|
# if there are no multiple peaks in any hunk left -> leave the while
|
|
496
456
|
# loop and continue with program
|
|
497
|
-
multiplePeaksWithinHunk = flatnonzero(peakDist < self.hunk_length * maxPeakDist)
|
|
457
|
+
multiplePeaksWithinHunk = np.flatnonzero(peakDist < self.hunk_length * maxPeakDist)
|
|
498
458
|
while len(multiplePeaksWithinHunk) > 0:
|
|
499
459
|
peakLocHelp = multiplePeaksWithinHunk[0]
|
|
500
460
|
indHelp = [peakLocHelp, peakLocHelp + 1]
|
|
501
461
|
if self.multiple_peaks_in_hunk == 'extremum':
|
|
502
462
|
values = trigger_data[indHelp]
|
|
503
|
-
deleteInd = indHelp[argmin(abs(values))]
|
|
463
|
+
deleteInd = indHelp[np.argmin(abs(values))]
|
|
504
464
|
elif self.multiple_peaks_in_hunk == 'first':
|
|
505
465
|
deleteInd = indHelp[1]
|
|
506
|
-
peakLoc = delete(peakLoc, deleteInd)
|
|
507
|
-
trigger_data = delete(trigger_data, deleteInd)
|
|
466
|
+
peakLoc = np.delete(peakLoc, deleteInd)
|
|
467
|
+
trigger_data = np.delete(trigger_data, deleteInd)
|
|
508
468
|
peakDist = peakLoc[1:] - peakLoc[:-1]
|
|
509
|
-
multiplePeaksWithinHunk = flatnonzero(peakDist < self.hunk_length * maxPeakDist)
|
|
469
|
+
multiplePeaksWithinHunk = np.flatnonzero(peakDist < self.hunk_length * maxPeakDist)
|
|
510
470
|
|
|
511
471
|
# check whether distances between peaks are evenly distributed
|
|
512
|
-
meanDist = mean(peakDist)
|
|
472
|
+
meanDist = np.mean(peakDist)
|
|
513
473
|
diffDist = abs(peakDist - meanDist)
|
|
514
|
-
faultyInd = flatnonzero(diffDist > self.max_variation_of_duration * meanDist)
|
|
474
|
+
faultyInd = np.flatnonzero(diffDist > self.max_variation_of_duration * meanDist)
|
|
515
475
|
if faultyInd.size != 0:
|
|
516
476
|
warn(
|
|
517
477
|
f'In Trigger-Identification: The distances between the peaks (and therefore the lengths of the \
|
|
@@ -527,7 +487,7 @@ class Trigger(TimeOut): # pragma: no cover
|
|
|
527
487
|
|
|
528
488
|
def _trigger_rect(self, x0, x, threshold):
|
|
529
489
|
# x0 stores the last value of the the last generator cycle
|
|
530
|
-
xNew = append(x0, x)
|
|
490
|
+
xNew = np.append(x0, x)
|
|
531
491
|
# indPeakHunk = abs(xNew[1:] - xNew[:-1]) > abs(threshold)
|
|
532
492
|
# with above line, every edge would be located
|
|
533
493
|
return self._trigger_value_comp(xNew[1:] - xNew[:-1], threshold)
|
|
@@ -538,8 +498,8 @@ class Trigger(TimeOut): # pragma: no cover
|
|
|
538
498
|
def _threshold(self, num):
|
|
539
499
|
if self.threshold is None: # take a guessed threshold
|
|
540
500
|
# get max and min values of whole trigger signal
|
|
541
|
-
maxVal = -inf
|
|
542
|
-
minVal = inf
|
|
501
|
+
maxVal = -np.inf
|
|
502
|
+
minVal = np.inf
|
|
543
503
|
meanVal = 0
|
|
544
504
|
cntMean = 0
|
|
545
505
|
for trigger_data in self.source.result(num):
|
|
@@ -551,7 +511,7 @@ class Trigger(TimeOut): # pragma: no cover
|
|
|
551
511
|
|
|
552
512
|
# get 75% of maximum absolute value of trigger signal
|
|
553
513
|
maxTriggerHelp = [minVal, maxVal] - meanVal
|
|
554
|
-
argInd = argmax(abs(maxTriggerHelp))
|
|
514
|
+
argInd = np.argmax(abs(maxTriggerHelp))
|
|
555
515
|
thresh = maxTriggerHelp[argInd] * 0.75 # 0.75 for 75% of max trigger signal
|
|
556
516
|
warn(f'No threshold was passed. An estimated threshold of {thresh} is assumed.', Warning, stacklevel=2)
|
|
557
517
|
else: # take user defined threshold
|
|
@@ -625,7 +585,7 @@ class AngleTracker(MaskedTimeOut):
|
|
|
625
585
|
|
|
626
586
|
#: Number of trigger signals per revolution. This allows tracking scenarios where multiple
|
|
627
587
|
#: trigger pulses occur per rotation. Default is ``1``, meaning a single trigger per revolution.
|
|
628
|
-
trigger_per_revo = Int(1
|
|
588
|
+
trigger_per_revo = Int(1)
|
|
629
589
|
|
|
630
590
|
#: Rotation direction flag:
|
|
631
591
|
#:
|
|
@@ -633,26 +593,26 @@ class AngleTracker(MaskedTimeOut):
|
|
|
633
593
|
#: - ``-1``: clockwise rotation.
|
|
634
594
|
#:
|
|
635
595
|
#: Default is ``-1``.
|
|
636
|
-
rot_direction = Int(-1
|
|
596
|
+
rot_direction = Int(-1)
|
|
637
597
|
|
|
638
598
|
#: Number of points used for spline interpolation. Default is ``4``.
|
|
639
|
-
interp_points = Int(4
|
|
599
|
+
interp_points = Int(4)
|
|
640
600
|
|
|
641
601
|
#: Initial rotation angle (in radians) corresponding to the first trigger event. This allows
|
|
642
602
|
#: defining a custom starting reference angle. Default is ``0``.
|
|
643
|
-
start_angle = Float(0
|
|
603
|
+
start_angle = Float(0)
|
|
644
604
|
|
|
645
605
|
#: Revolutions per minute (RPM) computed for each sample.
|
|
646
606
|
#: It is based on the trigger data. (read-only)
|
|
647
|
-
rpm = Property(depends_on=['digest']
|
|
607
|
+
rpm = Property(depends_on=['digest'])
|
|
648
608
|
|
|
649
609
|
#: Average revolutions per minute over the entire dataset.
|
|
650
610
|
#: It is computed based on the trigger intervals. (read-only)
|
|
651
|
-
average_rpm = Property(depends_on=['digest']
|
|
611
|
+
average_rpm = Property(depends_on=['digest'])
|
|
652
612
|
|
|
653
613
|
#: Computed rotation angle (in radians) for each sample.
|
|
654
614
|
#: It is interpolated from the trigger data. (read-only)
|
|
655
|
-
angle = Property(depends_on=['digest']
|
|
615
|
+
angle = Property(depends_on=['digest'])
|
|
656
616
|
|
|
657
617
|
# Internal flag to determine whether rpm and angle calculation has been processed,
|
|
658
618
|
# prevents recalculation
|
|
@@ -670,7 +630,7 @@ class AngleTracker(MaskedTimeOut):
|
|
|
670
630
|
|
|
671
631
|
# helperfunction for trigger index detection
|
|
672
632
|
def _find_nearest_idx(self, peakarray, value):
|
|
673
|
-
peakarray = asarray(peakarray)
|
|
633
|
+
peakarray = np.asarray(peakarray)
|
|
674
634
|
return (abs(peakarray - value)).argmin()
|
|
675
635
|
|
|
676
636
|
def _to_rpm_and_angle(self):
|
|
@@ -688,8 +648,8 @@ class AngleTracker(MaskedTimeOut):
|
|
|
688
648
|
rotDirection = self.rot_direction
|
|
689
649
|
num = self.source.num_samples
|
|
690
650
|
samplerate = self.source.sample_freq
|
|
691
|
-
self._rpm = zeros(num)
|
|
692
|
-
self._angle = zeros(num)
|
|
651
|
+
self._rpm = np.zeros(num)
|
|
652
|
+
self._angle = np.zeros(num)
|
|
693
653
|
# number of spline points
|
|
694
654
|
InterpPoints = self.interp_points
|
|
695
655
|
|
|
@@ -701,7 +661,7 @@ class AngleTracker(MaskedTimeOut):
|
|
|
701
661
|
peakloc[self._find_nearest_idx(peakarray=peakloc, value=ind) + 1]
|
|
702
662
|
- peakloc[self._find_nearest_idx(peakarray=peakloc, value=ind)]
|
|
703
663
|
)
|
|
704
|
-
splineData = stack(
|
|
664
|
+
splineData = np.stack(
|
|
705
665
|
(range(InterpPoints), peakloc[ind // peakdist : ind // peakdist + InterpPoints]),
|
|
706
666
|
axis=0,
|
|
707
667
|
)
|
|
@@ -711,7 +671,7 @@ class AngleTracker(MaskedTimeOut):
|
|
|
711
671
|
peakloc[self._find_nearest_idx(peakarray=peakloc, value=ind)]
|
|
712
672
|
- peakloc[self._find_nearest_idx(peakarray=peakloc, value=ind) - 1]
|
|
713
673
|
)
|
|
714
|
-
splineData = stack(
|
|
674
|
+
splineData = np.stack(
|
|
715
675
|
(range(InterpPoints), peakloc[ind // peakdist - InterpPoints : ind // peakdist]),
|
|
716
676
|
axis=0,
|
|
717
677
|
)
|
|
@@ -719,16 +679,16 @@ class AngleTracker(MaskedTimeOut):
|
|
|
719
679
|
Spline = splrep(splineData[:, :][1], splineData[:, :][0], k=3)
|
|
720
680
|
self._rpm[ind] = splev(ind, Spline, der=1, ext=0) * 60 * samplerate
|
|
721
681
|
self._angle[ind] = (
|
|
722
|
-
splev(ind, Spline, der=0, ext=0) * 2 * pi * rotDirection / TriggerPerRevo + self.start_angle
|
|
723
|
-
) % (2 * pi)
|
|
682
|
+
splev(ind, Spline, der=0, ext=0) * 2 * np.pi * rotDirection / TriggerPerRevo + self.start_angle
|
|
683
|
+
) % (2 * np.pi)
|
|
724
684
|
# next sample
|
|
725
685
|
ind += 1
|
|
726
686
|
# calculation complete
|
|
727
687
|
self._calc_flag = True
|
|
728
688
|
|
|
729
689
|
# reset calc flag if something has changed
|
|
730
|
-
@
|
|
731
|
-
def _reset_calc_flag(self):
|
|
690
|
+
@observe('digest')
|
|
691
|
+
def _reset_calc_flag(self, event): # noqa ARG002
|
|
732
692
|
self._calc_flag = False
|
|
733
693
|
|
|
734
694
|
# calc rpm from trigger data
|
|
@@ -777,14 +737,15 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
777
737
|
|
|
778
738
|
#: The physical microphone geometry. An instance of :class:`~acoular.microphones.MicGeom` that
|
|
779
739
|
#: defines the positions of the real microphones used for measurement.
|
|
780
|
-
mics = Instance(MicGeom()
|
|
740
|
+
mics = Instance(MicGeom())
|
|
781
741
|
|
|
782
742
|
#: The virtual microphone geometry. This property defines the positions
|
|
783
743
|
#: of virtual microphones where interpolated pressure values are computed.
|
|
784
744
|
#: Default is the physical microphone geometry (:attr:`mics`).
|
|
785
|
-
mics_virtual = Property(
|
|
745
|
+
mics_virtual = Property()
|
|
786
746
|
|
|
787
|
-
|
|
747
|
+
#: internal microphone geometry;internal usage, read only
|
|
748
|
+
_mics_virtual = Instance(MicGeom)
|
|
788
749
|
|
|
789
750
|
def _get_mics_virtual(self):
|
|
790
751
|
if not self._mics_virtual and self.mics:
|
|
@@ -814,7 +775,6 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
814
775
|
'IDW',
|
|
815
776
|
'custom',
|
|
816
777
|
'sinc',
|
|
817
|
-
desc='method for interpolation used',
|
|
818
778
|
)
|
|
819
779
|
|
|
820
780
|
#: Defines the spatial dimensionality of the microphone array.
|
|
@@ -826,7 +786,7 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
826
786
|
#: - ``'ring'``: Circular arrays where rotation needs to be considered.
|
|
827
787
|
#: - ``'3D'``: Three-dimensional microphone distributions.
|
|
828
788
|
#: - ``'custom'``: User-defined microphone arrangements.
|
|
829
|
-
array_dimension = Enum('1D', '2D', 'ring', '3D', 'custom'
|
|
789
|
+
array_dimension = Enum('1D', '2D', 'ring', '3D', 'custom')
|
|
830
790
|
|
|
831
791
|
#: Sampling frequency of the output signal, inherited from the :attr:`source`. This defines the
|
|
832
792
|
#: rate at which microphone pressure samples are acquired and processed.
|
|
@@ -856,19 +816,16 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
856
816
|
#:
|
|
857
817
|
#: where :math:`Q` is the transformation matrix and :math:`(x', y', z')` are the modified
|
|
858
818
|
#: coordinates. If no transformation is needed, :math:`Q` defaults to the identity matrix.
|
|
859
|
-
Q = CArray(dtype=float64, shape=(3, 3), value=identity(3))
|
|
819
|
+
Q = CArray(dtype=np.float64, shape=(3, 3), value=np.identity(3))
|
|
860
820
|
|
|
861
821
|
#: Number of neighboring microphones used in IDW interpolation. This parameter determines how
|
|
862
822
|
#: many physical microphones contribute to the weighted sum in inverse distance weighting (IDW)
|
|
863
823
|
#: interpolation.
|
|
864
|
-
num_IDW = Int(3
|
|
824
|
+
num_IDW = Int(3) # noqa: N815
|
|
865
825
|
|
|
866
826
|
#: Weighting exponent for IDW interpolation. This parameter controls the influence of distance
|
|
867
827
|
#: in inverse distance weighting (IDW). A higher value gives more weight to closer microphones.
|
|
868
|
-
p_weight = Float(
|
|
869
|
-
2,
|
|
870
|
-
desc='used in interpolation for virtual microphone, weighting power exponent for IDW',
|
|
871
|
-
)
|
|
828
|
+
p_weight = Float(2)
|
|
872
829
|
|
|
873
830
|
# Stores the output of :meth:`_virtNewCoord_func`; Read-Only
|
|
874
831
|
_virtNewCoord_func = Property( # noqa: N815
|
|
@@ -897,7 +854,7 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
897
854
|
|
|
898
855
|
@cached_property
|
|
899
856
|
def _get_virtNewCoord(self): # noqa N802
|
|
900
|
-
return self._virtNewCoord_func(self.mics.
|
|
857
|
+
return self._virtNewCoord_func(self.mics.pos, self.mics_virtual.pos, self.method, self.array_dimension)
|
|
901
858
|
|
|
902
859
|
def sinc_mic(self, r):
|
|
903
860
|
"""
|
|
@@ -919,7 +876,7 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
919
876
|
:class:`numpy.ndarray`
|
|
920
877
|
Evaluated sinc function values at the given radial distances.
|
|
921
878
|
"""
|
|
922
|
-
return sinc((r * self.mics_virtual.mpos.shape[1]) / (pi))
|
|
879
|
+
return np.sinc((r * self.mics_virtual.mpos.shape[1]) / (np.pi))
|
|
923
880
|
|
|
924
881
|
def _virtNewCoord_func(self, mpos, mpos_virt, method, array_dimension): # noqa N802
|
|
925
882
|
# Core functionality for getting the interpolation.
|
|
@@ -966,23 +923,23 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
966
923
|
|
|
967
924
|
# init positions of virtual mics in cyl coordinates
|
|
968
925
|
nVirtMics = mpos_virt.shape[1]
|
|
969
|
-
virtNewCoord = zeros((3, nVirtMics))
|
|
970
|
-
virtNewCoord.fill(nan)
|
|
926
|
+
virtNewCoord = np.zeros((3, nVirtMics))
|
|
927
|
+
virtNewCoord.fill(np.nan)
|
|
971
928
|
# init real positions in cyl coordinates
|
|
972
929
|
nMics = mpos.shape[1]
|
|
973
|
-
newCoord = zeros((3, nMics))
|
|
974
|
-
newCoord.fill(nan)
|
|
930
|
+
newCoord = np.zeros((3, nMics))
|
|
931
|
+
newCoord.fill(np.nan)
|
|
975
932
|
# empty mesh object
|
|
976
933
|
mesh = []
|
|
977
934
|
|
|
978
935
|
if self.array_dimension == '1D' or self.array_dimension == 'ring':
|
|
979
936
|
# get projections onto new coordinate, for real mics
|
|
980
937
|
projectionOnNewAxis = cartToCyl(mpos, self.Q)[0]
|
|
981
|
-
indReorderHelp = argsort(projectionOnNewAxis)
|
|
938
|
+
indReorderHelp = np.argsort(projectionOnNewAxis)
|
|
982
939
|
mesh.append([projectionOnNewAxis[indReorderHelp], indReorderHelp])
|
|
983
940
|
|
|
984
941
|
# new coordinates of real mics
|
|
985
|
-
indReorderHelp = argsort(cartToCyl(mpos, self.Q)[0])
|
|
942
|
+
indReorderHelp = np.argsort(cartToCyl(mpos, self.Q)[0])
|
|
986
943
|
newCoord = (cartToCyl(mpos, self.Q).T)[indReorderHelp].T
|
|
987
944
|
|
|
988
945
|
# and for virtual mics
|
|
@@ -993,7 +950,7 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
993
950
|
virtNewCoord = cartToCyl(mpos_virt, self.Q)
|
|
994
951
|
|
|
995
952
|
# new coordinates of real mics
|
|
996
|
-
indReorderHelp = argsort(cartToCyl(mpos, self.Q)[0])
|
|
953
|
+
indReorderHelp = np.argsort(cartToCyl(mpos, self.Q)[0])
|
|
997
954
|
newCoord = cartToCyl(mpos, self.Q)
|
|
998
955
|
|
|
999
956
|
# scipy delauney triangulation
|
|
@@ -1002,29 +959,29 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
1002
959
|
|
|
1003
960
|
if self.interp_at_zero:
|
|
1004
961
|
# add a point at zero
|
|
1005
|
-
tri.add_points(array([[0], [0]]).T)
|
|
962
|
+
tri.add_points(np.array([[0], [0]]).T)
|
|
1006
963
|
|
|
1007
964
|
# extend mesh with closest boundary points of repeating mesh
|
|
1008
|
-
pointsOriginal = arange(tri.points.shape[0])
|
|
965
|
+
pointsOriginal = np.arange(tri.points.shape[0])
|
|
1009
966
|
hull = tri.convex_hull
|
|
1010
|
-
hullPoints = unique(hull)
|
|
967
|
+
hullPoints = np.unique(hull)
|
|
1011
968
|
|
|
1012
969
|
addRight = tri.points[hullPoints]
|
|
1013
|
-
addRight[:, 0] += 2 * pi
|
|
970
|
+
addRight[:, 0] += 2 * np.pi
|
|
1014
971
|
addLeft = tri.points[hullPoints]
|
|
1015
|
-
addLeft[:, 0] -= 2 * pi
|
|
972
|
+
addLeft[:, 0] -= 2 * np.pi
|
|
1016
973
|
|
|
1017
|
-
indOrigPoints = concatenate((pointsOriginal, pointsOriginal[hullPoints], pointsOriginal[hullPoints]))
|
|
974
|
+
indOrigPoints = np.concatenate((pointsOriginal, pointsOriginal[hullPoints], pointsOriginal[hullPoints]))
|
|
1018
975
|
# add all hull vertices to original mesh and check which of those
|
|
1019
976
|
# are actual neighbors of the original array. Cancel out all others.
|
|
1020
|
-
tri.add_points(concatenate([addLeft, addRight]))
|
|
977
|
+
tri.add_points(np.concatenate([addLeft, addRight]))
|
|
1021
978
|
indices, indptr = tri.vertex_neighbor_vertices
|
|
1022
|
-
hullNeighbor = empty((0), dtype='int32')
|
|
979
|
+
hullNeighbor = np.empty((0), dtype='int32')
|
|
1023
980
|
for currHull in hullPoints:
|
|
1024
981
|
neighborOfHull = indptr[indices[currHull] : indices[currHull + 1]]
|
|
1025
|
-
hullNeighbor = append(hullNeighbor, neighborOfHull)
|
|
1026
|
-
hullNeighborUnique = unique(hullNeighbor)
|
|
1027
|
-
pointsNew = unique(append(pointsOriginal, hullNeighborUnique))
|
|
982
|
+
hullNeighbor = np.append(hullNeighbor, neighborOfHull)
|
|
983
|
+
hullNeighborUnique = np.unique(hullNeighbor)
|
|
984
|
+
pointsNew = np.unique(np.append(pointsOriginal, hullNeighborUnique))
|
|
1028
985
|
tri = Delaunay(tri.points[pointsNew]) # re-meshing
|
|
1029
986
|
mesh.append([tri, indOrigPoints[pointsNew]])
|
|
1030
987
|
|
|
@@ -1032,36 +989,36 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
1032
989
|
# get virtual mic projections on new coord system
|
|
1033
990
|
virtNewCoord = cartToCyl(mpos_virt, self.Q)
|
|
1034
991
|
# get real mic projections on new coord system
|
|
1035
|
-
indReorderHelp = argsort(cartToCyl(mpos, self.Q)[0])
|
|
992
|
+
indReorderHelp = np.argsort(cartToCyl(mpos, self.Q)[0])
|
|
1036
993
|
newCoord = cartToCyl(mpos, self.Q)
|
|
1037
994
|
# Delaunay
|
|
1038
995
|
tri = Delaunay(newCoord.T, incremental=True) # , incremental=True,qhull_options = "Qc QJ Q12"
|
|
1039
996
|
|
|
1040
997
|
if self.interp_at_zero:
|
|
1041
998
|
# add a point at zero
|
|
1042
|
-
tri.add_points(array([[0], [0], [0]]).T)
|
|
999
|
+
tri.add_points(np.array([[0], [0], [0]]).T)
|
|
1043
1000
|
|
|
1044
1001
|
# extend mesh with closest boundary points of repeating mesh
|
|
1045
|
-
pointsOriginal = arange(tri.points.shape[0])
|
|
1002
|
+
pointsOriginal = np.arange(tri.points.shape[0])
|
|
1046
1003
|
hull = tri.convex_hull
|
|
1047
|
-
hullPoints = unique(hull)
|
|
1004
|
+
hullPoints = np.unique(hull)
|
|
1048
1005
|
|
|
1049
1006
|
addRight = tri.points[hullPoints]
|
|
1050
|
-
addRight[:, 0] += 2 * pi
|
|
1007
|
+
addRight[:, 0] += 2 * np.pi
|
|
1051
1008
|
addLeft = tri.points[hullPoints]
|
|
1052
|
-
addLeft[:, 0] -= 2 * pi
|
|
1009
|
+
addLeft[:, 0] -= 2 * np.pi
|
|
1053
1010
|
|
|
1054
|
-
indOrigPoints = concatenate((pointsOriginal, pointsOriginal[hullPoints], pointsOriginal[hullPoints]))
|
|
1011
|
+
indOrigPoints = np.concatenate((pointsOriginal, pointsOriginal[hullPoints], pointsOriginal[hullPoints]))
|
|
1055
1012
|
# add all hull vertices to original mesh and check which of those
|
|
1056
1013
|
# are actual neighbors of the original array. Cancel out all others.
|
|
1057
|
-
tri.add_points(concatenate([addLeft, addRight]))
|
|
1014
|
+
tri.add_points(np.concatenate([addLeft, addRight]))
|
|
1058
1015
|
indices, indptr = tri.vertex_neighbor_vertices
|
|
1059
|
-
hullNeighbor = empty((0), dtype='int32')
|
|
1016
|
+
hullNeighbor = np.empty((0), dtype='int32')
|
|
1060
1017
|
for currHull in hullPoints:
|
|
1061
1018
|
neighborOfHull = indptr[indices[currHull] : indices[currHull + 1]]
|
|
1062
|
-
hullNeighbor = append(hullNeighbor, neighborOfHull)
|
|
1063
|
-
hullNeighborUnique = unique(hullNeighbor)
|
|
1064
|
-
pointsNew = unique(append(pointsOriginal, hullNeighborUnique))
|
|
1019
|
+
hullNeighbor = np.append(hullNeighbor, neighborOfHull)
|
|
1020
|
+
hullNeighborUnique = np.unique(hullNeighbor)
|
|
1021
|
+
pointsNew = np.unique(np.append(pointsOriginal, hullNeighborUnique))
|
|
1065
1022
|
tri = Delaunay(tri.points[pointsNew]) # re-meshing
|
|
1066
1023
|
mesh.append([tri, indOrigPoints[pointsNew]])
|
|
1067
1024
|
|
|
@@ -1090,24 +1047,24 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
1090
1047
|
# number of time samples
|
|
1091
1048
|
nTime = p.shape[0]
|
|
1092
1049
|
# number of virtual mixcs
|
|
1093
|
-
nVirtMics = self.mics_virtual.
|
|
1050
|
+
nVirtMics = self.mics_virtual.pos.shape[1]
|
|
1094
1051
|
# mesh and projection onto polar Coordinates
|
|
1095
1052
|
meshList, virtNewCoord, newCoord = self._get_virtNewCoord()
|
|
1096
1053
|
# pressure interpolation init
|
|
1097
|
-
pInterp = zeros((nTime, nVirtMics))
|
|
1054
|
+
pInterp = np.zeros((nTime, nVirtMics))
|
|
1098
1055
|
# Coordinates in cartesian CO - for IDW interpolation
|
|
1099
1056
|
newCoordCart = cylToCart(newCoord)
|
|
1100
1057
|
|
|
1101
1058
|
if self.interp_at_zero:
|
|
1102
1059
|
# interpolate point at 0 in Kartesian CO
|
|
1103
1060
|
interpolater = LinearNDInterpolator(
|
|
1104
|
-
cylToCart(newCoord[:, argsort(newCoord[0])])[:2, :].T,
|
|
1105
|
-
p[:, (argsort(newCoord[0]))].T,
|
|
1061
|
+
cylToCart(newCoord[:, np.argsort(newCoord[0])])[:2, :].T,
|
|
1062
|
+
p[:, (np.argsort(newCoord[0]))].T,
|
|
1106
1063
|
fill_value=0,
|
|
1107
1064
|
)
|
|
1108
1065
|
pZero = interpolater((0, 0))
|
|
1109
1066
|
# add the interpolated pressure at origin to pressure channels
|
|
1110
|
-
p = concatenate((p, pZero[:, newaxis]), axis=1)
|
|
1067
|
+
p = np.concatenate((p, pZero[:, np.newaxis]), axis=1)
|
|
1111
1068
|
|
|
1112
1069
|
# helpfunction reordered for reordered pressure values
|
|
1113
1070
|
pHelp = p[:, meshList[0][1]]
|
|
@@ -1115,31 +1072,31 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
1115
1072
|
# Interpolation for 1D Arrays
|
|
1116
1073
|
if self.array_dimension == '1D' or self.array_dimension == 'ring':
|
|
1117
1074
|
# for rotation add phi_delay
|
|
1118
|
-
if not array_equal(phi_delay, []):
|
|
1119
|
-
xInterpHelp = tile(virtNewCoord[0, :], (nTime, 1)) + tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1120
|
-
xInterp = ((xInterpHelp + pi) % (2 * pi)) - pi # shifting phi
|
|
1075
|
+
if not np.array_equal(phi_delay, []):
|
|
1076
|
+
xInterpHelp = np.tile(virtNewCoord[0, :], (nTime, 1)) + np.tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1077
|
+
xInterp = ((xInterpHelp + np.pi) % (2 * np.pi)) - np.pi # shifting phi into feasible area [-pi, pi]
|
|
1121
1078
|
# if no rotation given
|
|
1122
1079
|
else:
|
|
1123
|
-
xInterp = tile(virtNewCoord[0, :], (nTime, 1))
|
|
1080
|
+
xInterp = np.tile(virtNewCoord[0, :], (nTime, 1))
|
|
1124
1081
|
# get ordered microphone positions in radiant
|
|
1125
1082
|
x = newCoord[0]
|
|
1126
1083
|
for cntTime in range(nTime):
|
|
1127
1084
|
if self.method == 'linear':
|
|
1128
1085
|
# numpy 1-d interpolation
|
|
1129
|
-
pInterp[cntTime] = interp(
|
|
1086
|
+
pInterp[cntTime] = np.interp(
|
|
1130
1087
|
xInterp[cntTime, :],
|
|
1131
1088
|
x,
|
|
1132
1089
|
pHelp[cntTime, :],
|
|
1133
1090
|
period=period,
|
|
1134
|
-
left=nan,
|
|
1135
|
-
right=nan,
|
|
1091
|
+
left=np.nan,
|
|
1092
|
+
right=np.nan,
|
|
1136
1093
|
)
|
|
1137
1094
|
|
|
1138
1095
|
elif self.method == 'spline':
|
|
1139
1096
|
# scipy cubic spline interpolation
|
|
1140
1097
|
SplineInterp = CubicSpline(
|
|
1141
|
-
append(x, (2 * pi) + x[0]),
|
|
1142
|
-
append(pHelp[cntTime, :], pHelp[cntTime, :][0]),
|
|
1098
|
+
np.append(x, (2 * np.pi) + x[0]),
|
|
1099
|
+
np.append(pHelp[cntTime, :], pHelp[cntTime, :][0]),
|
|
1143
1100
|
axis=0,
|
|
1144
1101
|
bc_type='periodic',
|
|
1145
1102
|
extrapolate=None,
|
|
@@ -1173,16 +1130,18 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
1173
1130
|
# Interpolation for arbitrary 2D Arrays
|
|
1174
1131
|
elif self.array_dimension == '2D':
|
|
1175
1132
|
# check rotation
|
|
1176
|
-
if not array_equal(phi_delay, []):
|
|
1177
|
-
xInterpHelp = tile(virtNewCoord[0, :], (nTime, 1)) + tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1178
|
-
xInterp = ((xInterpHelp + pi) % (2 * pi)) - pi # shifting phi
|
|
1133
|
+
if not np.array_equal(phi_delay, []):
|
|
1134
|
+
xInterpHelp = np.tile(virtNewCoord[0, :], (nTime, 1)) + np.tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1135
|
+
xInterp = ((xInterpHelp + np.pi) % (2 * np.pi)) - np.pi # shifting phi into feasible area [-pi, pi]
|
|
1179
1136
|
else:
|
|
1180
|
-
xInterp = tile(virtNewCoord[0, :], (nTime, 1))
|
|
1137
|
+
xInterp = np.tile(virtNewCoord[0, :], (nTime, 1))
|
|
1181
1138
|
|
|
1182
1139
|
mesh = meshList[0][0]
|
|
1183
1140
|
for cntTime in range(nTime):
|
|
1184
1141
|
# points for interpolation
|
|
1185
|
-
newPoint = concatenate(
|
|
1142
|
+
newPoint = np.concatenate(
|
|
1143
|
+
(xInterp[cntTime, :][:, np.newaxis], virtNewCoord[1, :][:, np.newaxis]), axis=1
|
|
1144
|
+
)
|
|
1186
1145
|
# scipy 1D interpolation
|
|
1187
1146
|
if self.method == 'linear':
|
|
1188
1147
|
interpolater = LinearNDInterpolator(mesh, pHelp[cntTime, :], fill_value=0)
|
|
@@ -1215,7 +1174,7 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
1215
1174
|
function='cubic',
|
|
1216
1175
|
) # radial basis function interpolator instance
|
|
1217
1176
|
|
|
1218
|
-
virtshiftcoord = array([xInterp[cntTime, :], virtNewCoord[1], virtNewCoord[2]])
|
|
1177
|
+
virtshiftcoord = np.array([xInterp[cntTime, :], virtNewCoord[1], virtNewCoord[2]])
|
|
1219
1178
|
pInterp[cntTime] = rbfi(virtshiftcoord[0], virtshiftcoord[1], virtshiftcoord[2])
|
|
1220
1179
|
|
|
1221
1180
|
elif self.method == 'rbf-multiquadric':
|
|
@@ -1228,40 +1187,40 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
|
|
|
1228
1187
|
function='multiquadric',
|
|
1229
1188
|
) # radial basis function interpolator instance
|
|
1230
1189
|
|
|
1231
|
-
virtshiftcoord = array([xInterp[cntTime, :], virtNewCoord[1], virtNewCoord[2]])
|
|
1190
|
+
virtshiftcoord = np.array([xInterp[cntTime, :], virtNewCoord[1], virtNewCoord[2]])
|
|
1232
1191
|
pInterp[cntTime] = rbfi(virtshiftcoord[0], virtshiftcoord[1], virtshiftcoord[2])
|
|
1233
1192
|
# using inverse distance weighting
|
|
1234
1193
|
elif self.method == 'IDW':
|
|
1235
1194
|
newPoint2_M = newPoint.T
|
|
1236
|
-
newPoint3_M = append(newPoint2_M, zeros([1, self.num_channels]), axis=0)
|
|
1195
|
+
newPoint3_M = np.append(newPoint2_M, np.zeros([1, self.num_channels]), axis=0)
|
|
1237
1196
|
newPointCart = cylToCart(newPoint3_M)
|
|
1238
|
-
for ind in arange(len(newPoint[:, 0])):
|
|
1239
|
-
newPoint_Rep = tile(newPointCart[:, ind], (len(newPoint[:, 0]), 1)).T
|
|
1197
|
+
for ind in np.arange(len(newPoint[:, 0])):
|
|
1198
|
+
newPoint_Rep = np.tile(newPointCart[:, ind], (len(newPoint[:, 0]), 1)).T
|
|
1240
1199
|
subtract = newPoint_Rep - newCoordCart
|
|
1241
|
-
normDistance = norm(subtract, axis=0)
|
|
1242
|
-
index_norm = argsort(normDistance)[: self.num_IDW]
|
|
1200
|
+
normDistance = spla.norm(subtract, axis=0)
|
|
1201
|
+
index_norm = np.argsort(normDistance)[: self.num_IDW]
|
|
1243
1202
|
pHelpNew = pHelp[cntTime, index_norm]
|
|
1244
1203
|
normNew = normDistance[index_norm]
|
|
1245
1204
|
if normNew[0] < 1e-3:
|
|
1246
1205
|
pInterp[cntTime, ind] = pHelpNew[0]
|
|
1247
1206
|
else:
|
|
1248
|
-
wholeD = sum(1 / normNew**self.p_weight)
|
|
1207
|
+
wholeD = np.sum(1 / normNew**self.p_weight)
|
|
1249
1208
|
weight = (1 / normNew**self.p_weight) / wholeD
|
|
1250
|
-
pInterp[cntTime, ind] = sum(pHelpNew * weight)
|
|
1209
|
+
pInterp[cntTime, ind] = np.sum(pHelpNew * weight)
|
|
1251
1210
|
|
|
1252
1211
|
# Interpolation for arbitrary 3D Arrays
|
|
1253
1212
|
elif self.array_dimension == '3D':
|
|
1254
1213
|
# check rotation
|
|
1255
|
-
if not array_equal(phi_delay, []):
|
|
1256
|
-
xInterpHelp = tile(virtNewCoord[0, :], (nTime, 1)) + tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1257
|
-
xInterp = ((xInterpHelp + pi) % (2 * pi)) - pi # shifting phi
|
|
1214
|
+
if not np.array_equal(phi_delay, []):
|
|
1215
|
+
xInterpHelp = np.tile(virtNewCoord[0, :], (nTime, 1)) + np.tile(phi_delay, (virtNewCoord.shape[1], 1)).T
|
|
1216
|
+
xInterp = ((xInterpHelp + np.pi) % (2 * np.pi)) - np.pi # shifting phi into feasible area [-pi, pi]
|
|
1258
1217
|
else:
|
|
1259
|
-
xInterp = tile(virtNewCoord[0, :], (nTime, 1))
|
|
1218
|
+
xInterp = np.tile(virtNewCoord[0, :], (nTime, 1))
|
|
1260
1219
|
|
|
1261
1220
|
mesh = meshList[0][0]
|
|
1262
1221
|
for cntTime in range(nTime):
|
|
1263
1222
|
# points for interpolation
|
|
1264
|
-
newPoint = concatenate((xInterp[cntTime, :][:, newaxis], virtNewCoord[1:, :].T), axis=1)
|
|
1223
|
+
newPoint = np.concatenate((xInterp[cntTime, :][:, np.newaxis], virtNewCoord[1:, :].T), axis=1)
|
|
1265
1224
|
|
|
1266
1225
|
if self.method == 'linear':
|
|
1267
1226
|
interpolater = LinearNDInterpolator(mesh, pHelp[cntTime, :], fill_value=0)
|
|
@@ -1391,9 +1350,9 @@ class SpatialInterpolatorRotation(SpatialInterpolator): # pragma: no cover
|
|
|
1391
1350
|
a multiple of ``num``.
|
|
1392
1351
|
"""
|
|
1393
1352
|
# period for rotation
|
|
1394
|
-
period = 2 * pi
|
|
1353
|
+
period = 2 * np.pi
|
|
1395
1354
|
# get angle
|
|
1396
|
-
angle = self.angle_source.angle
|
|
1355
|
+
angle = self.angle_source.angle
|
|
1397
1356
|
# counter to track angle position in time for each block
|
|
1398
1357
|
count = 0
|
|
1399
1358
|
for timeData in self.source.result(num):
|
|
@@ -1464,12 +1423,12 @@ class SpatialInterpolatorConstantRotation(SpatialInterpolator): # pragma: no co
|
|
|
1464
1423
|
The last block may contain fewer samples if the total number of samples is not
|
|
1465
1424
|
a multiple of ``num``.
|
|
1466
1425
|
"""
|
|
1467
|
-
omega = 2 * pi * self.rotational_speed
|
|
1468
|
-
period = 2 * pi
|
|
1426
|
+
omega = 2 * np.pi * self.rotational_speed
|
|
1427
|
+
period = 2 * np.pi
|
|
1469
1428
|
phiOffset = 0.0
|
|
1470
1429
|
for timeData in self.source.result(num):
|
|
1471
1430
|
nTime = timeData.shape[0]
|
|
1472
|
-
phi_delay = phiOffset + linspace(0, nTime / self.sample_freq * omega, nTime, endpoint=False)
|
|
1431
|
+
phi_delay = phiOffset + np.linspace(0, nTime / self.sample_freq * omega, nTime, endpoint=False)
|
|
1473
1432
|
interpVal = self._result_core_func(timeData, phi_delay, period, self.Q, interp_at_zero=False)
|
|
1474
1433
|
phiOffset = phi_delay[-1] + omega / self.sample_freq
|
|
1475
1434
|
yield interpVal
|
|
@@ -1521,16 +1480,18 @@ class Mixer(TimeOut):
|
|
|
1521
1480
|
return digest(self)
|
|
1522
1481
|
|
|
1523
1482
|
def validate_sources(self):
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1483
|
+
"""
|
|
1484
|
+
Validate whether the additional sources are compatible with the primary source.
|
|
1485
|
+
|
|
1486
|
+
This method checks if all sources have the same sampling frequency and the same number of
|
|
1487
|
+
channels. If a mismatch is detected, a :obj:`ValueError` is raised.
|
|
1488
|
+
|
|
1489
|
+
Raises
|
|
1490
|
+
------
|
|
1491
|
+
:obj:`ValueError`
|
|
1492
|
+
If any source in :attr:`sources` has a different sampling frequency or
|
|
1493
|
+
number of channels than :attr:`source`.
|
|
1494
|
+
"""
|
|
1534
1495
|
if self.source:
|
|
1535
1496
|
for s in self.sources:
|
|
1536
1497
|
if self.sample_freq != s.sample_freq:
|
|
@@ -1681,12 +1642,12 @@ class TimeCumAverage(TimeOut):
|
|
|
1681
1642
|
recalculated by summing the previous cumulative value and the new samples, then dividing by
|
|
1682
1643
|
the total number of samples up to that point.
|
|
1683
1644
|
"""
|
|
1684
|
-
count = (arange(num) + 1)[:, newaxis]
|
|
1645
|
+
count = (np.arange(num) + 1)[:, np.newaxis]
|
|
1685
1646
|
for i, temp in enumerate(self.source.result(num)):
|
|
1686
1647
|
ns, nc = temp.shape
|
|
1687
1648
|
if not i:
|
|
1688
|
-
accu = zeros((1, nc))
|
|
1689
|
-
temp = (accu * (count[0] - 1) + cumsum(temp, axis=0)) / count[:ns]
|
|
1649
|
+
accu = np.zeros((1, nc))
|
|
1650
|
+
temp = (accu * (count[0] - 1) + np.cumsum(temp, axis=0)) / count[:ns]
|
|
1690
1651
|
accu = temp[-1]
|
|
1691
1652
|
count += ns
|
|
1692
1653
|
yield temp
|
|
@@ -1724,7 +1685,7 @@ class TimeReverse(TimeOut):
|
|
|
1724
1685
|
------
|
|
1725
1686
|
:class:`numpy.ndarray`
|
|
1726
1687
|
An array containing the time-reversed version of the signal for the current block.
|
|
1727
|
-
Each block will have the shape (``num``, :attr
|
|
1688
|
+
Each block will have the shape (``num``, :attr:`~acoular.base.TimeOut.num_channels`),
|
|
1728
1689
|
where :attr:`~acoular.base.TimeOut.num_channels` is inherited from the :attr:`source`.
|
|
1729
1690
|
The last block may contain fewer samples if the total number of samples is not
|
|
1730
1691
|
a multiple of ``num``.
|
|
@@ -1739,7 +1700,7 @@ class TimeReverse(TimeOut):
|
|
|
1739
1700
|
"""
|
|
1740
1701
|
result_list = []
|
|
1741
1702
|
result_list.extend(self.source.result(num))
|
|
1742
|
-
temp = empty_like(result_list[0])
|
|
1703
|
+
temp = np.empty_like(result_list[0])
|
|
1743
1704
|
h = result_list.pop()
|
|
1744
1705
|
nsh = h.shape[0]
|
|
1745
1706
|
temp[:nsh] = h[::-1]
|
|
@@ -1803,7 +1764,7 @@ class Filter(TimeOut):
|
|
|
1803
1764
|
a multiple of ``num``.
|
|
1804
1765
|
"""
|
|
1805
1766
|
sos = self.sos
|
|
1806
|
-
zi = zeros((sos.shape[0], 2, self.source.num_channels))
|
|
1767
|
+
zi = np.zeros((sos.shape[0], 2, self.source.num_channels))
|
|
1807
1768
|
for block in self.source.result(num):
|
|
1808
1769
|
sos = self.sos # this line is useful in case of changes
|
|
1809
1770
|
# to self.sos during generator lifetime
|
|
@@ -1829,7 +1790,7 @@ class FiltOctave(Filter):
|
|
|
1829
1790
|
"""
|
|
1830
1791
|
|
|
1831
1792
|
#: The center frequency of the octave or third-octave band. Default is ``1000``.
|
|
1832
|
-
band = Float(1000.0
|
|
1793
|
+
band = Float(1000.0)
|
|
1833
1794
|
|
|
1834
1795
|
#: Defines whether the filter is an octave-band or third-octave-band filter.
|
|
1835
1796
|
#:
|
|
@@ -1837,11 +1798,11 @@ class FiltOctave(Filter):
|
|
|
1837
1798
|
#: - ``'Third octave'``: Third-octave band filter.
|
|
1838
1799
|
#:
|
|
1839
1800
|
#: Default is ``'Octave'``.
|
|
1840
|
-
fraction = Map({'Octave': 1, 'Third octave': 3}, default_value='Octave'
|
|
1801
|
+
fraction = Map({'Octave': 1, 'Third octave': 3}, default_value='Octave')
|
|
1841
1802
|
|
|
1842
1803
|
#: The order of the IIR filter, which affects the steepness of the filter's roll-off.
|
|
1843
1804
|
#: Default is ``3``.
|
|
1844
|
-
order = Int(3
|
|
1805
|
+
order = Int(3)
|
|
1845
1806
|
|
|
1846
1807
|
#: Second-order sections representation of the filter coefficients. This property depends on
|
|
1847
1808
|
#: :attr:`band`, :attr:`fraction`, :attr:`order`, and the source's digest.
|
|
@@ -1880,12 +1841,12 @@ class FiltOctave(Filter):
|
|
|
1880
1841
|
# adjust filter edge frequencies for correct power bandwidth (see ANSI 1.11 1987
|
|
1881
1842
|
# and Kalb,J.T.: "A thirty channel real time audio analyzer and its applications",
|
|
1882
1843
|
# PhD Thesis: Georgia Inst. of Techn., 1975
|
|
1883
|
-
beta = pi / (2 * self.order)
|
|
1844
|
+
beta = np.pi / (2 * self.order)
|
|
1884
1845
|
alpha = pow(2.0, 1.0 / (2.0 * self.fraction_))
|
|
1885
|
-
beta = 2 * beta / sin(beta) / (alpha - 1 / alpha)
|
|
1886
|
-
alpha = (1 + sqrt(1 + beta * beta)) / beta
|
|
1846
|
+
beta = 2 * beta / np.sin(beta) / (alpha - 1 / alpha)
|
|
1847
|
+
alpha = (1 + np.sqrt(1 + beta * beta)) / beta
|
|
1887
1848
|
fr = 2 * self.band / fs
|
|
1888
|
-
if fr > 1 / sqrt(2):
|
|
1849
|
+
if fr > 1 / np.sqrt(2):
|
|
1889
1850
|
msg = f'band frequency too high:{self.band:f},{fs:f}'
|
|
1890
1851
|
raise ValueError(msg)
|
|
1891
1852
|
om1 = fr / alpha
|
|
@@ -1916,7 +1877,7 @@ class FiltFiltOctave(FiltOctave):
|
|
|
1916
1877
|
|
|
1917
1878
|
#: The half-order of the IIR filter, applied twice (once forward and once backward). This
|
|
1918
1879
|
#: results in a final filter order twice as large as the specified value. Default is ``2``.
|
|
1919
|
-
order = Int(2
|
|
1880
|
+
order = Int(2)
|
|
1920
1881
|
|
|
1921
1882
|
#: A unique identifier for the filter, based on its properties. (read-only)
|
|
1922
1883
|
digest = Property(depends_on=['source.digest', 'band', 'fraction', 'order'])
|
|
@@ -1946,16 +1907,16 @@ class FiltFiltOctave(FiltOctave):
|
|
|
1946
1907
|
# filter design
|
|
1947
1908
|
fs = self.sample_freq
|
|
1948
1909
|
# adjust filter edge frequencies for correct power bandwidth (see FiltOctave)
|
|
1949
|
-
beta = pi / (2 * self.order)
|
|
1910
|
+
beta = np.pi / (2 * self.order)
|
|
1950
1911
|
alpha = pow(2.0, 1.0 / (2.0 * self.fraction_))
|
|
1951
|
-
beta = 2 * beta / sin(beta) / (alpha - 1 / alpha)
|
|
1952
|
-
alpha = (1 + sqrt(1 + beta * beta)) / beta
|
|
1912
|
+
beta = 2 * beta / np.sin(beta) / (alpha - 1 / alpha)
|
|
1913
|
+
alpha = (1 + np.sqrt(1 + beta * beta)) / beta
|
|
1953
1914
|
# additional bandwidth correction for double-pass
|
|
1954
1915
|
alpha = alpha * {6: 1.01, 5: 1.012, 4: 1.016, 3: 1.022, 2: 1.036, 1: 1.083}.get(self.order, 1.0) ** (
|
|
1955
1916
|
3 / self.fraction_
|
|
1956
1917
|
)
|
|
1957
1918
|
fr = 2 * self.band / fs
|
|
1958
|
-
if fr > 1 / sqrt(2):
|
|
1919
|
+
if fr > 1 / np.sqrt(2):
|
|
1959
1920
|
msg = f'band frequency too high:{self.band:f},{fs:f}'
|
|
1960
1921
|
raise ValueError(msg)
|
|
1961
1922
|
om1 = fr / alpha
|
|
@@ -1990,7 +1951,7 @@ class FiltFiltOctave(FiltOctave):
|
|
|
1990
1951
|
- Filtering is performed separately for each channel to optimize memory usage.
|
|
1991
1952
|
"""
|
|
1992
1953
|
sos = self.sos
|
|
1993
|
-
data = empty((self.source.num_samples, self.source.num_channels))
|
|
1954
|
+
data = np.empty((self.source.num_samples, self.source.num_channels))
|
|
1994
1955
|
j = 0
|
|
1995
1956
|
for block in self.source.result(num):
|
|
1996
1957
|
ns, nc = block.shape
|
|
@@ -2031,7 +1992,7 @@ class TimeExpAverage(Filter):
|
|
|
2031
1992
|
#: - ``'I'`` (Impulse) → 0.035 (non-standard)
|
|
2032
1993
|
#:
|
|
2033
1994
|
#: Default is ``'F'``.
|
|
2034
|
-
weight = Map({'F': 0.125, 'S': 1.0, 'I': 0.035}, default_value='F'
|
|
1995
|
+
weight = Map({'F': 0.125, 'S': 1.0, 'I': 0.035}, default_value='F')
|
|
2035
1996
|
|
|
2036
1997
|
#: Filter coefficients in second-order section (SOS) format.
|
|
2037
1998
|
sos = Property(depends_on=['weight', 'source.digest'])
|
|
@@ -2073,7 +2034,7 @@ class TimeExpAverage(Filter):
|
|
|
2073
2034
|
#
|
|
2074
2035
|
# This implementation ensures that the filter adapts dynamically
|
|
2075
2036
|
# based on the source's sampling frequency.
|
|
2076
|
-
alpha = 1 - exp(-1 / self.weight_ / self.sample_freq)
|
|
2037
|
+
alpha = 1 - np.exp(-1 / self.weight_ / self.sample_freq)
|
|
2077
2038
|
a = [1, alpha - 1]
|
|
2078
2039
|
b = [alpha]
|
|
2079
2040
|
return tf2sos(b, a)
|
|
@@ -2104,7 +2065,7 @@ class FiltFreqWeight(Filter):
|
|
|
2104
2065
|
#: - ``'Z'``: A flat response with no frequency weighting.
|
|
2105
2066
|
#:
|
|
2106
2067
|
#: Default is ``'A'``.
|
|
2107
|
-
weight = Enum('A', 'C', 'Z'
|
|
2068
|
+
weight = Enum('A', 'C', 'Z')
|
|
2108
2069
|
|
|
2109
2070
|
#: Second-order sections (SOS) representation of the filter coefficients. This property is
|
|
2110
2071
|
#: dynamically computed based on :attr:`weight` and the :attr:`Filter.source`'s digest.
|
|
@@ -2163,38 +2124,37 @@ class FiltFreqWeight(Filter):
|
|
|
2163
2124
|
f2 = 107.65265
|
|
2164
2125
|
f3 = 737.86223
|
|
2165
2126
|
f4 = 12194.217
|
|
2166
|
-
a = polymul([1, 4 * pi * f4, (2 * pi * f4) ** 2], [1, 4 * pi * f1, (2 * pi * f1) ** 2])
|
|
2127
|
+
a = np.polymul([1, 4 * np.pi * f4, (2 * np.pi * f4) ** 2], [1, 4 * np.pi * f1, (2 * np.pi * f1) ** 2])
|
|
2167
2128
|
if self.weight == 'A':
|
|
2168
|
-
a = polymul(polymul(a, [1, 2 * pi * f3]), [1, 2 * pi * f2])
|
|
2169
|
-
b = [(2 * pi * f4) ** 2 * 10 ** (1.9997 / 20), 0, 0, 0, 0]
|
|
2129
|
+
a = np.polymul(np.polymul(a, [1, 2 * np.pi * f3]), [1, 2 * np.pi * f2])
|
|
2130
|
+
b = [(2 * np.pi * f4) ** 2 * 10 ** (1.9997 / 20), 0, 0, 0, 0]
|
|
2170
2131
|
b, a = bilinear(b, a, self.sample_freq)
|
|
2171
2132
|
elif self.weight == 'C':
|
|
2172
|
-
b = [(2 * pi * f4) ** 2 * 10 ** (0.0619 / 20), 0, 0]
|
|
2133
|
+
b = [(2 * np.pi * f4) ** 2 * 10 ** (0.0619 / 20), 0, 0]
|
|
2173
2134
|
b, a = bilinear(b, a, self.sample_freq)
|
|
2174
|
-
b = append(b, zeros(2)) # make 6th order
|
|
2175
|
-
a = append(a, zeros(2))
|
|
2135
|
+
b = np.append(b, np.zeros(2)) # make 6th order
|
|
2136
|
+
a = np.append(a, np.zeros(2))
|
|
2176
2137
|
else:
|
|
2177
|
-
b = zeros(7)
|
|
2138
|
+
b = np.zeros(7)
|
|
2178
2139
|
b[0] = 1.0
|
|
2179
2140
|
a = b # 6th order flat response
|
|
2180
2141
|
return tf2sos(b, a)
|
|
2181
2142
|
|
|
2182
2143
|
|
|
2183
|
-
@deprecated_alias({'numbands': 'num_bands'}, read_only=True, removal_version='25.10')
|
|
2184
2144
|
class FilterBank(TimeOut):
|
|
2185
2145
|
"""
|
|
2186
2146
|
Abstract base class for IIR filter banks based on :mod:`scipy.signal.lfilter`.
|
|
2187
2147
|
|
|
2188
2148
|
Implements a bank of parallel filters. This class should not be instantiated by itself.
|
|
2189
2149
|
|
|
2190
|
-
Inherits from :class
|
|
2150
|
+
Inherits from :class:`~acoular.base.TimeOut`, and defines the structure for working with filter
|
|
2191
2151
|
banks for processing multi-channel time series data, such as time signal signals.
|
|
2192
2152
|
|
|
2193
2153
|
See Also
|
|
2194
2154
|
--------
|
|
2195
|
-
:class
|
|
2155
|
+
:class:`~acoular.base.TimeOut` :
|
|
2196
2156
|
ABC for signal processing blocks that interact with data from a source.
|
|
2197
|
-
:class
|
|
2157
|
+
:class:`~acoular.base.SamplesGenerator` :
|
|
2198
2158
|
Interface for any generating multi-channel time domain signal processing block.
|
|
2199
2159
|
:mod:`scipy.signal` :
|
|
2200
2160
|
SciPy module for signal processing.
|
|
@@ -2260,12 +2220,13 @@ class FilterBank(TimeOut):
|
|
|
2260
2220
|
numbands = self.num_bands
|
|
2261
2221
|
snumch = self.source.num_channels
|
|
2262
2222
|
sos = self.sos
|
|
2263
|
-
zi = [zeros((sos[0].shape[0], 2, snumch)) for _ in range(numbands)]
|
|
2264
|
-
res = zeros((num, self.num_channels), dtype='float')
|
|
2223
|
+
zi = [np.zeros((sos[0].shape[0], 2, snumch)) for _ in range(numbands)]
|
|
2224
|
+
res = np.zeros((num, self.num_channels), dtype='float')
|
|
2265
2225
|
for block in self.source.result(num):
|
|
2226
|
+
len_block = block.shape[0]
|
|
2266
2227
|
for i in range(numbands):
|
|
2267
|
-
res[
|
|
2268
|
-
yield res
|
|
2228
|
+
res[:len_block, i * snumch : (i + 1) * snumch], zi[i] = sosfilt(sos[i], block, axis=0, zi=zi[i])
|
|
2229
|
+
yield res[:len_block]
|
|
2269
2230
|
|
|
2270
2231
|
|
|
2271
2232
|
class OctaveFilterBank(FilterBank):
|
|
@@ -2280,7 +2241,7 @@ class OctaveFilterBank(FilterBank):
|
|
|
2280
2241
|
--------
|
|
2281
2242
|
:class:`FilterBank` :
|
|
2282
2243
|
The base class for implementing IIR filter banks.
|
|
2283
|
-
:class
|
|
2244
|
+
:class:`~acoular.base.SamplesGenerator` :
|
|
2284
2245
|
Interface for generating multi-channel time domain signal processing blocks.
|
|
2285
2246
|
:mod:`scipy.signal` :
|
|
2286
2247
|
SciPy module for signal processing.
|
|
@@ -2288,17 +2249,17 @@ class OctaveFilterBank(FilterBank):
|
|
|
2288
2249
|
|
|
2289
2250
|
#: The lowest band center frequency index. Default is ``21``.
|
|
2290
2251
|
#: This index refers to the position in the scale of octave or third-octave bands.
|
|
2291
|
-
lband = Int(21
|
|
2252
|
+
lband = Int(21)
|
|
2292
2253
|
|
|
2293
2254
|
#: The highest band center frequency index + 1. Default is ``40``.
|
|
2294
2255
|
#: This is the position in the scale of octave or third-octave bands.
|
|
2295
|
-
hband = Int(40
|
|
2256
|
+
hband = Int(40)
|
|
2296
2257
|
|
|
2297
2258
|
#: The fraction of an octave, either ``'Octave'`` or ``'Third octave'``.
|
|
2298
2259
|
#: Default is ``'Octave'``.
|
|
2299
2260
|
#: Determines the width of the frequency bands. 'Octave' refers to full octaves,
|
|
2300
2261
|
#: and ``'Third octave'`` refers to third-octave bands.
|
|
2301
|
-
fraction = Map({'Octave': 1, 'Third octave': 3}, default_value='Octave'
|
|
2262
|
+
fraction = Map({'Octave': 1, 'Third octave': 3}, default_value='Octave')
|
|
2302
2263
|
|
|
2303
2264
|
#: The list of filter coefficients for all filters in the filter bank.
|
|
2304
2265
|
#: The coefficients are computed based on the :attr:`lband`, :attr:`hband`,
|
|
@@ -2347,7 +2308,6 @@ class OctaveFilterBank(FilterBank):
|
|
|
2347
2308
|
return sos
|
|
2348
2309
|
|
|
2349
2310
|
|
|
2350
|
-
@deprecated_alias({'name': 'file'}, removal_version='25.10')
|
|
2351
2311
|
class WriteWAV(TimeOut):
|
|
2352
2312
|
"""
|
|
2353
2313
|
Saves time signal from one or more channels as mono, stereo, or multi-channel ``.wav`` file.
|
|
@@ -2358,9 +2318,9 @@ class WriteWAV(TimeOut):
|
|
|
2358
2318
|
|
|
2359
2319
|
See Also
|
|
2360
2320
|
--------
|
|
2361
|
-
:class
|
|
2321
|
+
:class:`~acoular.base.TimeOut` :
|
|
2362
2322
|
ABC for signal processing blocks that interact with data from a source.
|
|
2363
|
-
:class
|
|
2323
|
+
:class:`~acoular.base.SamplesGenerator` :
|
|
2364
2324
|
Interface for generating multi-channel time domain signal processing blocks.
|
|
2365
2325
|
:mod:`wave` :
|
|
2366
2326
|
Python module for handling WAV files.
|
|
@@ -2372,20 +2332,22 @@ class WriteWAV(TimeOut):
|
|
|
2372
2332
|
|
|
2373
2333
|
#: The name of the file to be saved. If none is given, the name will be automatically
|
|
2374
2334
|
#: generated from the source.
|
|
2375
|
-
file = File(filter=['*.wav']
|
|
2335
|
+
file = File(filter=['*.wav'])
|
|
2376
2336
|
|
|
2377
2337
|
#: The name of the cache file (without extension). It serves as an internal reference for data
|
|
2378
2338
|
#: caching and tracking processed files. (automatically generated)
|
|
2379
2339
|
basename = Property(depends_on=['digest'])
|
|
2380
2340
|
|
|
2381
2341
|
#: The list of channels to save. Can only contain one or two channels.
|
|
2382
|
-
channels = List(int
|
|
2342
|
+
channels = List(int)
|
|
2383
2343
|
|
|
2384
2344
|
# Bit depth of the output file.
|
|
2385
|
-
|
|
2345
|
+
#: bit depth of the output file
|
|
2346
|
+
encoding = Enum('uint8', 'int16', 'int32')
|
|
2386
2347
|
|
|
2387
2348
|
# Maximum value to scale the output to. If `None`, the maximum value of the data is used.
|
|
2388
|
-
|
|
2349
|
+
#: Maximum value to scale the output to.
|
|
2350
|
+
max_val = Either(None, Float)
|
|
2389
2351
|
|
|
2390
2352
|
#: A unique identifier for the filter, based on its properties. (read-only)
|
|
2391
2353
|
digest = Property(depends_on=['source.digest', 'channels'])
|
|
@@ -2485,7 +2447,7 @@ class WriteWAV(TimeOut):
|
|
|
2485
2447
|
wf.setnchannels(nc)
|
|
2486
2448
|
wf.setsampwidth(sw)
|
|
2487
2449
|
wf.setframerate(fs)
|
|
2488
|
-
ind = array(self.channels)
|
|
2450
|
+
ind = np.array(self.channels)
|
|
2489
2451
|
if self.max_val is None:
|
|
2490
2452
|
# compute maximum and remember result to avoid calling source twice
|
|
2491
2453
|
if not isinstance(self.source, Cache):
|
|
@@ -2495,7 +2457,7 @@ class WriteWAV(TimeOut):
|
|
|
2495
2457
|
if dtype == np.dtype('uint8'):
|
|
2496
2458
|
mx = 0
|
|
2497
2459
|
for data in self.source.result(num):
|
|
2498
|
-
mx = max(abs(data).max(), mx)
|
|
2460
|
+
mx = max(np.abs(data).max(), mx)
|
|
2499
2461
|
elif dtype in (np.dtype('int16'), np.dtype('int32')):
|
|
2500
2462
|
# for signed integers, we need special treatment because of asymmetry
|
|
2501
2463
|
negmax, posmax = 0, 0
|
|
@@ -2528,9 +2490,6 @@ class WriteWAV(TimeOut):
|
|
|
2528
2490
|
pass
|
|
2529
2491
|
|
|
2530
2492
|
|
|
2531
|
-
@deprecated_alias(
|
|
2532
|
-
{'name': 'file', 'numsamples_write': 'num_samples_write', 'writeflag': 'write_flag'}, removal_version='25.10'
|
|
2533
|
-
)
|
|
2534
2493
|
class WriteH5(TimeOut):
|
|
2535
2494
|
"""
|
|
2536
2495
|
Saves time signal data as a ``.h5`` (HDF5) file.
|
|
@@ -2541,9 +2500,9 @@ class WriteH5(TimeOut):
|
|
|
2541
2500
|
|
|
2542
2501
|
See Also
|
|
2543
2502
|
--------
|
|
2544
|
-
:class
|
|
2503
|
+
:class:`~acoular.base.TimeOut` :
|
|
2545
2504
|
ABC for signal processing blocks interacting with data from a source.
|
|
2546
|
-
:class
|
|
2505
|
+
:class:`~acoular.base.SamplesGenerator` :
|
|
2547
2506
|
Interface for generating multi-channel time-domain signal processing blocks.
|
|
2548
2507
|
h5py :
|
|
2549
2508
|
Python library for reading and writing HDF5 files.
|
|
@@ -2555,7 +2514,7 @@ class WriteH5(TimeOut):
|
|
|
2555
2514
|
|
|
2556
2515
|
#: The name of the file to be saved. If none is given, the name is automatically
|
|
2557
2516
|
#: generated based on the current timestamp.
|
|
2558
|
-
file = File(filter=['*.h5']
|
|
2517
|
+
file = File(filter=['*.h5'])
|
|
2559
2518
|
|
|
2560
2519
|
#: The number of samples to write to file per call to `result` method.
|
|
2561
2520
|
#: Default is ``-1``, meaning all available data from the source will be written.
|
|
@@ -2569,10 +2528,10 @@ class WriteH5(TimeOut):
|
|
|
2569
2528
|
|
|
2570
2529
|
#: Precision of the entries in the HDF5 file, represented as numpy data types.
|
|
2571
2530
|
#: Default is ``'float32'``.
|
|
2572
|
-
precision = Enum('float32', 'float64'
|
|
2531
|
+
precision = Enum('float32', 'float64')
|
|
2573
2532
|
|
|
2574
2533
|
#: Metadata to be stored in the HDF5 file.
|
|
2575
|
-
metadata = Dict(
|
|
2534
|
+
metadata = Dict()
|
|
2576
2535
|
|
|
2577
2536
|
@cached_property
|
|
2578
2537
|
def _get_digest(self):
|
|
@@ -2646,7 +2605,7 @@ class WriteH5(TimeOut):
|
|
|
2646
2605
|
f5h.create_new_group('metadata', '/')
|
|
2647
2606
|
for key, value in self.metadata.items():
|
|
2648
2607
|
if isinstance(value, str):
|
|
2649
|
-
value = array(value, dtype='S')
|
|
2608
|
+
value = np.array(value, dtype='S')
|
|
2650
2609
|
f5h.create_array('/metadata', key, value)
|
|
2651
2610
|
|
|
2652
2611
|
def result(self, num):
|
|
@@ -2716,9 +2675,9 @@ class TimeConvolve(TimeOut):
|
|
|
2716
2675
|
|
|
2717
2676
|
See Also
|
|
2718
2677
|
--------
|
|
2719
|
-
:class
|
|
2678
|
+
:class:`~acoular.base.TimeOut` :
|
|
2720
2679
|
The parent class for signal processing blocks.
|
|
2721
|
-
:class
|
|
2680
|
+
:class:`~acoular.base.SamplesGenerator` :
|
|
2722
2681
|
The interface for generating multi-channel time-domain signals.
|
|
2723
2682
|
"""
|
|
2724
2683
|
|
|
@@ -2728,22 +2687,34 @@ class TimeConvolve(TimeOut):
|
|
|
2728
2687
|
|
|
2729
2688
|
#: Convolution kernel in the time domain.
|
|
2730
2689
|
#: The second dimension of the kernel array has to be either ``1`` or match
|
|
2731
|
-
#: the :attr:`source`'s :attr:`~acoular.base.
|
|
2690
|
+
#: the :attr:`source`'s :attr:`~acoular.base.Generator.num_channels` attribute.
|
|
2732
2691
|
#: If only a single kernel is supplied, it is applied to all channels.
|
|
2733
|
-
kernel = CArray(dtype=float
|
|
2692
|
+
kernel = CArray(dtype=float)
|
|
2693
|
+
|
|
2694
|
+
#: Controls whether to extend the output to include the full convolution result.
|
|
2695
|
+
#:
|
|
2696
|
+
#: - If ``False`` (default): Output length is :math:`\\max(L, M)`, where :math:`L` is the
|
|
2697
|
+
#: kernel length and :math:`M` is the signal length. This mode keeps the output length
|
|
2698
|
+
#: equal to the longest input (different from NumPy's ``mode='same'``, since it does not
|
|
2699
|
+
#: pad the output).
|
|
2700
|
+
#: - If ``True``: Output length is :math:`L + M - 1`, returning the full convolution at
|
|
2701
|
+
#: each overlap point (similar to NumPy's ``mode='full'``).
|
|
2702
|
+
#:
|
|
2703
|
+
#: Default is ``False``.
|
|
2704
|
+
extend_signal = Bool(False)
|
|
2734
2705
|
|
|
2735
2706
|
# Internal block size for partitioning signals into smaller segments during processing.
|
|
2736
|
-
|
|
2707
|
+
#: Block size
|
|
2708
|
+
_block_size = Int()
|
|
2737
2709
|
|
|
2738
2710
|
# Blocks of the convolution kernel in the frequency domain.
|
|
2739
2711
|
# Computed using Fast Fourier Transform (FFT).
|
|
2740
2712
|
_kernel_blocks = Property(
|
|
2741
2713
|
depends_on=['kernel', '_block_size'],
|
|
2742
|
-
desc='Frequency domain Kernel blocks',
|
|
2743
2714
|
)
|
|
2744
2715
|
|
|
2745
2716
|
#: A unique identifier for the object, based on its properties. (read-only)
|
|
2746
|
-
digest = Property(depends_on=['source.digest', 'kernel'])
|
|
2717
|
+
digest = Property(depends_on=['source.digest', 'kernel', 'extend_signal'])
|
|
2747
2718
|
|
|
2748
2719
|
@cached_property
|
|
2749
2720
|
def _get_digest(self):
|
|
@@ -2786,27 +2757,27 @@ class TimeConvolve(TimeOut):
|
|
|
2786
2757
|
# A 3D array of complex values representing the frequency-domain blocks of the kernel.
|
|
2787
2758
|
[L, N] = self.kernel.shape
|
|
2788
2759
|
num = self._block_size
|
|
2789
|
-
P = int(ceil(L / num))
|
|
2760
|
+
P = int(np.ceil(L / num))
|
|
2790
2761
|
trim = num * (P - 1)
|
|
2791
|
-
blocks = zeros([P, num + 1, N], dtype='complex128')
|
|
2762
|
+
blocks = np.zeros([P, num + 1, N], dtype='complex128')
|
|
2792
2763
|
|
|
2793
2764
|
if P > 1:
|
|
2794
|
-
for i, block in enumerate(split(self.kernel[:trim], P - 1, axis=0)):
|
|
2795
|
-
blocks[i] = rfft(concatenate([block, zeros([num, N])], axis=0), axis=0)
|
|
2765
|
+
for i, block in enumerate(np.split(self.kernel[:trim], P - 1, axis=0)):
|
|
2766
|
+
blocks[i] = rfft(np.concatenate([block, np.zeros([num, N])], axis=0), axis=0)
|
|
2796
2767
|
|
|
2797
2768
|
blocks[-1] = rfft(
|
|
2798
|
-
concatenate([self.kernel[trim:], zeros([2 * num - L + trim, N])], axis=0),
|
|
2769
|
+
np.concatenate([self.kernel[trim:], np.zeros([2 * num - L + trim, N])], axis=0),
|
|
2799
2770
|
axis=0,
|
|
2800
2771
|
)
|
|
2801
2772
|
return blocks
|
|
2802
2773
|
|
|
2803
2774
|
def result(self, num=128):
|
|
2804
|
-
"""
|
|
2775
|
+
r"""
|
|
2805
2776
|
Convolve the source signal with the kernel and yield the result in blocks.
|
|
2806
2777
|
|
|
2807
|
-
The method generates the convolution of the source signal with the kernel
|
|
2808
|
-
signal in small blocks, performing the convolution in
|
|
2809
|
-
results block by block.
|
|
2778
|
+
The method generates the convolution of the source signal (length :math:`M`) with the kernel
|
|
2779
|
+
(length :math:`L`) by processing the signal in small blocks, performing the convolution in
|
|
2780
|
+
the frequency domain, and yielding the results block by block.
|
|
2810
2781
|
|
|
2811
2782
|
Parameters
|
|
2812
2783
|
----------
|
|
@@ -2817,14 +2788,15 @@ class TimeConvolve(TimeOut):
|
|
|
2817
2788
|
Yields
|
|
2818
2789
|
------
|
|
2819
2790
|
:obj:`numpy.ndarray`
|
|
2820
|
-
|
|
2821
|
-
where :attr:`~acoular.base.
|
|
2791
|
+
An array of shape (``num``, :attr:`~acoular.base.Generator.num_channels`),
|
|
2792
|
+
where :attr:`~acoular.base.Generator.num_channels` is inherited from the
|
|
2822
2793
|
:attr:`source`, representing the convolution result in blocks.
|
|
2823
2794
|
|
|
2824
2795
|
Notes
|
|
2825
2796
|
-----
|
|
2826
2797
|
- The kernel is first validated and reshaped if necessary.
|
|
2827
2798
|
- The convolution is computed efficiently using the FFT in the frequency domain.
|
|
2799
|
+
- The output length is determined by the :attr:`extend_signal` property.
|
|
2828
2800
|
"""
|
|
2829
2801
|
self._validate_kernel()
|
|
2830
2802
|
# initialize variables
|
|
@@ -2832,15 +2804,18 @@ class TimeConvolve(TimeOut):
|
|
|
2832
2804
|
L = self.kernel.shape[0]
|
|
2833
2805
|
N = self.source.num_channels
|
|
2834
2806
|
M = self.source.num_samples
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2807
|
+
|
|
2808
|
+
output_size = max(L, M) if not self.extend_signal else L + M - 1
|
|
2809
|
+
|
|
2810
|
+
numblocks_kernel = int(np.ceil(L / num)) # number of kernel blocks
|
|
2811
|
+
Q = int(np.ceil(M / num)) # number of signal blocks
|
|
2812
|
+
R = int(np.ceil(output_size / num)) # number of output blocks
|
|
2813
|
+
last_size = output_size % num # size of final output block
|
|
2839
2814
|
|
|
2840
2815
|
idx = 0
|
|
2841
|
-
fdl = zeros([numblocks_kernel, num + 1, N], dtype='complex128')
|
|
2842
|
-
buff = zeros([2 * num, N]) # time-domain input buffer
|
|
2843
|
-
spec_sum = zeros([num + 1, N], dtype='complex128')
|
|
2816
|
+
fdl = np.zeros([numblocks_kernel, num + 1, N], dtype='complex128')
|
|
2817
|
+
buff = np.zeros([2 * num, N]) # time-domain input buffer
|
|
2818
|
+
spec_sum = np.zeros([num + 1, N], dtype='complex128')
|
|
2844
2819
|
|
|
2845
2820
|
signal_blocks = self.source.result(num)
|
|
2846
2821
|
temp = next(signal_blocks)
|
|
@@ -2851,7 +2826,8 @@ class TimeConvolve(TimeOut):
|
|
|
2851
2826
|
_append_to_fdl(fdl, idx, numblocks_kernel, rfft(buff, axis=0))
|
|
2852
2827
|
spec_sum = _spectral_sum(spec_sum, fdl, self._kernel_blocks)
|
|
2853
2828
|
# truncate s.t. total length is L+M-1 (like numpy convolve w/ mode="full")
|
|
2854
|
-
|
|
2829
|
+
final_len = last_size if last_size != 0 else num
|
|
2830
|
+
yield irfft(spec_sum, axis=0)[num : final_len + num]
|
|
2855
2831
|
return
|
|
2856
2832
|
|
|
2857
2833
|
# stream processing of source signal
|
|
@@ -2859,8 +2835,8 @@ class TimeConvolve(TimeOut):
|
|
|
2859
2835
|
_append_to_fdl(fdl, idx, numblocks_kernel, rfft(buff, axis=0))
|
|
2860
2836
|
spec_sum = _spectral_sum(spec_sum, fdl, self._kernel_blocks)
|
|
2861
2837
|
yield irfft(spec_sum, axis=0)[num:]
|
|
2862
|
-
buff = concatenate(
|
|
2863
|
-
[buff[num:], zeros([num, N])],
|
|
2838
|
+
buff = np.concatenate(
|
|
2839
|
+
[buff[num:], np.zeros([num, N])],
|
|
2864
2840
|
axis=0,
|
|
2865
2841
|
) # shift input buffer to the left
|
|
2866
2842
|
buff[num : num + temp.shape[0]] = temp # append new time-data
|
|
@@ -2869,15 +2845,16 @@ class TimeConvolve(TimeOut):
|
|
|
2869
2845
|
_append_to_fdl(fdl, idx, numblocks_kernel, rfft(buff, axis=0))
|
|
2870
2846
|
spec_sum = _spectral_sum(spec_sum, fdl, self._kernel_blocks)
|
|
2871
2847
|
yield irfft(spec_sum, axis=0)[num:]
|
|
2872
|
-
buff = concatenate(
|
|
2873
|
-
[buff[num:], zeros([num, N])],
|
|
2848
|
+
buff = np.concatenate(
|
|
2849
|
+
[buff[num:], np.zeros([num, N])],
|
|
2874
2850
|
axis=0,
|
|
2875
2851
|
) # shift input buffer to the left
|
|
2876
2852
|
|
|
2877
2853
|
_append_to_fdl(fdl, idx, numblocks_kernel, rfft(buff, axis=0))
|
|
2878
2854
|
spec_sum = _spectral_sum(spec_sum, fdl, self._kernel_blocks)
|
|
2879
2855
|
# truncate s.t. total length is L+M-1 (like numpy convolve w/ mode="full")
|
|
2880
|
-
|
|
2856
|
+
final_len = last_size if last_size != 0 else num
|
|
2857
|
+
yield irfft(spec_sum, axis=0)[num : final_len + num]
|
|
2881
2858
|
|
|
2882
2859
|
|
|
2883
2860
|
@nb.jit(nopython=True, cache=True)
|