acoular 25.4__py3-none-any.whl → 25.10__py3-none-any.whl

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