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/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
- from numpy import (
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, desc='start of valid samples')
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, desc='stop of valid samples')
112
+ stop = Union(None, CInt)
149
113
 
150
114
  #: List of channel indices to be excluded from processing.
151
- invalid_channels = List(int, desc='list of invalid channels')
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'], desc='channel mask')
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'], desc='basename for cache file')
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(desc='channel weights')
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, desc='trigger signals per revolution')
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, desc='mathematical direction of rotation')
596
+ rot_direction = Int(-1)
637
597
 
638
598
  #: Number of points used for spline interpolation. Default is ``4``.
639
- interp_points = Int(4, desc='Points of interpolation used for spline')
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, desc='rotation angle for trigger position')
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'], desc='revolutions per minute for each sample')
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'], desc='average revolutions per minute')
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'], desc='rotation angle for each sample')
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
- @on_trait_change('digest')
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(), desc='microphone geometry')
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(desc='microphone geometry')
745
+ mics_virtual = Property()
786
746
 
787
- _mics_virtual = Instance(MicGeom, desc='internal microphone geometry;internal usage, read only')
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', desc='spatial dimensionality of the array geometry')
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, desc='number of neighboring microphones, DEFAULT=3') # noqa: N815
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.mpos, self.mics_virtual.mpos, self.method, self.array_dimension)
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.mpos.shape[1]
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 cootrdinate into feasible area [-pi, pi]
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 cootrdinate into feasible area [-pi, pi]
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((xInterp[cntTime, :][:, newaxis], virtNewCoord[1, :][:, newaxis]), axis=1)
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 cootrdinate into feasible area [-pi, pi]
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
- # Validate whether the additional sources are compatible with the primary source.
1525
- #
1526
- # This method checks if all sources have the same sampling frequency and the same number of
1527
- # channels. If a mismatch is detected, a :obj:`ValueError` is raised.
1528
- #
1529
- # Raises
1530
- # ------
1531
- # :obj:`ValueError`
1532
- # If any source in :attr:`sources` has a different sampling frequency or
1533
- # number of channels than :attr:`source`.
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:`acoular.base.TimeOut.num_channels`),
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, desc='band center frequency')
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', desc='fraction of 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, desc='IIR filter order')
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, desc='IIR filter half order')
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', desc='time weighting')
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', desc='frequency weighting')
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:`acoular.base.TimeOut`, and defines the structure for working with filter
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:`acoular.base.TimeOut` :
2155
+ :class:`~acoular.base.TimeOut` :
2196
2156
  ABC for signal processing blocks that interact with data from a source.
2197
- :class:`acoular.base.SamplesGenerator` :
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[:, i * snumch : (i + 1) * snumch], zi[i] = sosfilt(sos[i], block, axis=0, zi=zi[i])
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:`acoular.base.SamplesGenerator` :
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, desc='lowest band center frequency index')
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, desc='highest band center frequency index + 1')
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', desc='fraction of 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:`acoular.base.TimeOut` :
2321
+ :class:`~acoular.base.TimeOut` :
2362
2322
  ABC for signal processing blocks that interact with data from a source.
2363
- :class:`acoular.base.SamplesGenerator` :
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'], desc='name of wave file')
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, desc='channels to save')
2342
+ channels = List(int)
2383
2343
 
2384
2344
  # Bit depth of the output file.
2385
- encoding = Enum('uint8', 'int16', 'int32', desc='bit depth of the output file')
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
- max_val = Either(None, Float, desc='Maximum value to scale the output to.')
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:`acoular.base.TimeOut` :
2503
+ :class:`~acoular.base.TimeOut` :
2545
2504
  ABC for signal processing blocks interacting with data from a source.
2546
- :class:`acoular.base.SamplesGenerator` :
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'], desc='name of data file')
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', desc='precision of H5 File')
2531
+ precision = Enum('float32', 'float64')
2573
2532
 
2574
2533
  #: Metadata to be stored in the HDF5 file.
2575
- metadata = Dict(desc='metadata to be stored in .h5 file')
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:`acoular.base.TimeOut` :
2678
+ :class:`~acoular.base.TimeOut` :
2720
2679
  The parent class for signal processing blocks.
2721
- :class:`acoular.base.SamplesGenerator` :
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.SamplesGenerator.num_channels` attribute.
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, desc='Convolution kernel.')
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
- _block_size = Int(desc='Block size')
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 by processing the
2808
- signal in small blocks, performing the convolution in the frequency domain, and yielding the
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
- A array of shape (``num``, :attr:`~acoular.base.SamplesGenerator.num_channels`),
2821
- where :attr:`~acoular.base.SamplesGenerator.num_channels` is inhereted from the
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
- numblocks_kernel = int(ceil(L / num)) # number of kernel blocks
2836
- Q = int(ceil(M / num)) # number of signal blocks
2837
- R = int(ceil((L + M - 1) / num)) # number of output blocks
2838
- last_size = (L + M - 1) % num # size of final block
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
- yield irfft(spec_sum, axis=0)[num : last_size + num]
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
- yield irfft(spec_sum, axis=0)[num : last_size + num]
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)