acoular 25.4__py3-none-any.whl → 25.7__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,8 @@
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.
5
6
 
6
7
  .. autosummary::
7
8
  :toctree: generated/
@@ -27,7 +28,6 @@
27
28
  WriteWAV
28
29
  WriteH5
29
30
  TimeConvolve
30
- MaskedTimeInOut
31
31
  """
32
32
 
33
33
  # imports from other packages
@@ -38,6 +38,7 @@ from os import path
38
38
  from warnings import warn
39
39
 
40
40
  import numba as nb
41
+ import numpy as np
41
42
  from numpy import (
42
43
  append,
43
44
  arange,
@@ -58,7 +59,6 @@ from numpy import (
58
59
  float64,
59
60
  identity,
60
61
  inf,
61
- int16,
62
62
  interp,
63
63
  linspace,
64
64
  mean,
@@ -88,6 +88,7 @@ from traits.api import (
88
88
  Constant,
89
89
  Delegate,
90
90
  Dict,
91
+ Either,
91
92
  Enum,
92
93
  File,
93
94
  Float,
@@ -111,55 +112,70 @@ from .environments import cartToCyl, cylToCart
111
112
  from .h5files import _get_h5file_class
112
113
  from .internal import digest, ldigest
113
114
  from .microphones import MicGeom
115
+ from .process import Cache
114
116
  from .tools.utils import find_basename
115
117
 
116
118
 
117
- @deprecated_alias({'numchannels_total': 'num_channels_total', 'numsamples_total': 'num_samples_total'})
119
+ @deprecated_alias(
120
+ {'numchannels_total': 'num_channels_total', 'numsamples_total': 'num_samples_total'}, removal_version='25.10'
121
+ )
118
122
  class MaskedTimeOut(TimeOut):
119
- """Signal processing block for channel and sample selection.
123
+ """
124
+ A signal processing block that allows for the selection of specific channels and time samples.
125
+
126
+ The :class:`MaskedTimeOut` class is designed to filter data from a given
127
+ :class:`~acoular.sources.SamplesGenerator` (or a derived object) by defining valid time samples
128
+ and excluding specific channels. It acts as an intermediary between the data source and
129
+ subsequent processing steps, ensuring that only the selected portion of the data is passed
130
+ along.
120
131
 
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`.
132
+ This class is useful for selecting specific portions of data for analysis. The processed data is
133
+ accessed through the generator method :meth:`result`, which returns data in block-wise fashion
134
+ for efficient streaming.
126
135
  """
127
136
 
128
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
137
+ #: The input data source. It must be an instance of a
138
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
139
+ #: This object provides the raw time-domain signals that will be filtered based on the
140
+ #: :attr:`start`, :attr:`stop`, and :attr:`invalid_channels` attributes.
129
141
  source = Instance(SamplesGenerator)
130
142
 
131
- # Index of the first sample to be considered valid.
143
+ #: The index of the first valid sample. Default is ``0``.
132
144
  start = CInt(0, desc='start of valid samples')
133
145
 
134
- # Index of the last sample to be considered valid.
146
+ #: The index of the last valid sample (exclusive).
147
+ #: If set to :obj:`None`, the selection continues until the end of the available data.
135
148
  stop = Union(None, CInt, desc='stop of valid samples')
136
149
 
137
- # Channels that are to be treated as invalid.
150
+ #: List of channel indices to be excluded from processing.
138
151
  invalid_channels = List(int, desc='list of invalid channels')
139
152
 
140
- # Channel mask to serve as an index for all valid channels, is set automatically.
153
+ #: A mask or index array representing valid channels. (automatically updated)
141
154
  channels = Property(depends_on=['invalid_channels', 'source.num_channels'], desc='channel mask')
142
155
 
143
- # Number of channels in input, as given by :attr:`~acoular.base.TimeOut.source`.
156
+ #: Total number of input channels, including invalid channels, as given by
157
+ #: :attr:`~acoular.base.TimeOut.source`. (read-only).
144
158
  num_channels_total = Delegate('source', 'num_channels')
145
159
 
146
- # Number of samples in input, as given by :attr:`~acoular.base.TimeOut.source`.
160
+ #: Total number of input channels, including invalid channels. (read-only).
147
161
  num_samples_total = Delegate('source', 'num_samples')
148
162
 
149
- # Number of valid channels, is set automatically.
163
+ #: Number of valid input channels after excluding :attr:`invalid_channels`. (read-only)
150
164
  num_channels = Property(
151
165
  depends_on=['invalid_channels', 'source.num_channels'], desc='number of valid input channels'
152
166
  )
153
167
 
154
- # Number of valid time samples, is set automatically.
168
+ #: Number of valid time-domain samples, based on :attr:`start` and :attr:`stop` indices.
169
+ #: (read-only)
155
170
  num_samples = Property(
156
171
  depends_on=['start', 'stop', 'source.num_samples'], desc='number of valid samples per channel'
157
172
  )
158
173
 
159
- # Name of the cache file without extension, readonly.
174
+ #: The name of the cache file (without extension). It serves as an internal reference for data
175
+ #: caching and tracking processed files. (automatically generated)
160
176
  basename = Property(depends_on=['source.digest'], desc='basename for cache file')
161
177
 
162
- # internal identifier
178
+ #: A unique identifier for the object, based on its properties. (read-only)
163
179
  digest = Property(depends_on=['source.digest', 'start', 'stop', 'invalid_channels'])
164
180
 
165
181
  @cached_property
@@ -171,7 +187,7 @@ class MaskedTimeOut(TimeOut):
171
187
  warn(
172
188
  (
173
189
  f'The basename attribute of a {self.__class__.__name__} object is deprecated'
174
- ' and will be removed in a future release!'
190
+ ' and will be removed in Acoular 26.01!'
175
191
  ),
176
192
  DeprecationWarning,
177
193
  stacklevel=2,
@@ -197,19 +213,32 @@ class MaskedTimeOut(TimeOut):
197
213
  return sli[1] - sli[0]
198
214
 
199
215
  def result(self, num):
200
- """Python generator that yields the output block-wise.
216
+ """
217
+ Generate blocks of processed data, selecting only valid samples and channels.
218
+
219
+ This method fetches data from the :attr:`source` object, applies the defined :attr:`start`
220
+ and :attr:`stop` constraints on time samples, and filters out :attr:`invalid_channels`. The
221
+ data is then yielded in block-wise fashion to facilitate efficient streaming.
201
222
 
202
223
  Parameters
203
224
  ----------
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
-
225
+ num : :obj:`int`
226
+ Number of samples per block.
227
+
228
+ Yields
229
+ ------
230
+ :class:`numpy.ndarray`
231
+ An array of shape (``num``, :attr:`MaskedTimeOut.num_channels`), contatining blocks of
232
+ a filtered time-domain signal. The last block may contain fewer samples if the total
233
+ number of samples is not a multiple of ``num``. `MaskedTimeOut.num_channels` is not
234
+ inherited directly and may be smaller than the :attr:`source`'s number of channels.
235
+
236
+ Raises
237
+ ------
238
+ :obj:`OSError`
239
+ If no valid samples are available within the defined :attr:`start` and :attr:`stop`
240
+ range. This can occur if :attr:`start` is greater than or equal to :attr:`stop` or if
241
+ the :attr:`source` is not containing any valid samples in the given range.
213
242
  """
214
243
  sli = slice(self.start, self.stop).indices(self.num_samples_total)
215
244
  start = sli[0]
@@ -259,20 +288,35 @@ class MaskedTimeOut(TimeOut):
259
288
 
260
289
 
261
290
  class ChannelMixer(TimeOut):
262
- """Class for directly mixing the channels of a multi-channel source.
263
- Outputs a single channel.
264
291
  """
292
+ A signal processing block that mixes multiple input channels into a single output channel.
265
293
 
266
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
294
+ The :class:`ChannelMixer` class takes a multi-channel signal from a
295
+ :class:`~acoular.sources.SamplesGenerator` (or a derived object) and applies an optional set of
296
+ amplitude weights to each channel. The resulting weighted sum is then output as a single-channel
297
+ signal.
298
+
299
+ This class is particularly useful for cases where a combined signal representation is needed,
300
+ such as beamforming, array signal processing, or for reducing the dimensionality of
301
+ multi-channel time signal data.
302
+ """
303
+
304
+ #: The input data source. It must be an instance of a
305
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
306
+ #: It provides the multi-channel time-domain signals that will be mixed.
267
307
  source = Instance(SamplesGenerator)
268
308
 
269
- # Amplitude weight(s) for the channels as array. If not set, all channels are equally weighted.
309
+ #: An array of amplitude weight factors applied to each input channel before summation.
310
+ #: If not explicitly set, all channels are weighted equally (delault is ``1``).
311
+ #: The shape of :attr:`weights` must match the :attr:`number of input channels<num_channels>`.
312
+ #: If an incompatible shape is provided, a :obj:`ValueError` will be raised.
270
313
  weights = CArray(desc='channel weights')
271
314
 
272
- # Number of channels is always one here.
315
+ #: The number of output channels, which is always ``1`` for this class since it produces a
316
+ #: single mixed output. (read-only)
273
317
  num_channels = Constant(1)
274
318
 
275
- # internal identifier
319
+ #: A unique identifier for the object, based on its properties. (read-only)
276
320
  digest = Property(depends_on=['source.digest', 'weights'])
277
321
 
278
322
  @cached_property
@@ -280,19 +324,31 @@ class ChannelMixer(TimeOut):
280
324
  return digest(self)
281
325
 
282
326
  def result(self, num):
283
- """Python generator that yields the output block-wise.
327
+ """
328
+ Generate the mixed output signal in blocks.
329
+
330
+ This method retrieves data from the :attr:`source` object, applies the specified amplitude
331
+ :attr:`weights` to each channel, and sums them to produce a single-channel output. The data
332
+ is processed and yielded in block-wise fashion for efficient memory handling.
284
333
 
285
334
  Parameters
286
335
  ----------
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
-
336
+ num : :obj:`int`
337
+ Number of samples per block.
338
+
339
+ Yields
340
+ ------
341
+ :class:`numpy.ndarray`
342
+ An array of shape ``(num, 1)`` containing blocks a of single-channel mixed signal.
343
+ The last block may contain fewer samples if the total number of samples is not
344
+ a multiple of ``num``.
345
+
346
+ Raises
347
+ ------
348
+ :obj:`ValueError`
349
+ If the :attr:`weights` array is provided but its shape does not match the expected shape
350
+ (:attr:`num_channels`,) or (``1``,), a :obj:`ValueError` is raised indicating that the
351
+ weights cannot be broadcasted properly.
296
352
  """
297
353
  if self.weights.size:
298
354
  if self.weights.shape in {(self.source.num_channels,), (1,)}:
@@ -308,69 +364,79 @@ class ChannelMixer(TimeOut):
308
364
 
309
365
 
310
366
  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.
367
+ """
368
+ A signal processing class for detecting and analyzing trigger signals in time-series data.
369
+
370
+ The :class:`Trigger` class identifies trigger events in a single-channel signal provided by a
371
+ :class:`~acoular.base.SamplesGenerator` source. The detection process involves:
372
+
373
+ 1. Identifying peaks that exceed a specified positive or negative threshold.
374
+ 2. Estimating the approximate duration of one revolution based on the largest
375
+ sample distance between consecutive peaks.
376
+ 3. Dividing the estimated revolution duration into segments called "hunks,"
377
+ allowing only one peak per hunk.
378
+ 4. Selecting the most appropriate peak per hunk based on a chosen criterion
379
+ (e.g., first occurrence or extremum value).
380
+ 5. Validating the consistency of the detected peaks by ensuring the revolutions
381
+ have a stable duration with minimal variation.
382
+
383
+ This class is typically used for rotational speed analysis, where trigger events
384
+ correspond to periodic markers in a signal (e.g., TDC signals in engine diagnostics).
322
385
  """
323
386
 
324
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
387
+ #: The input data source. It must be an instance of a
388
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
389
+ #: The signal must be single-channel.
325
390
  source = Instance(SamplesGenerator)
326
391
 
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.
392
+ #: The threshold value for detecting trigger peaks. The meaning of this threshold depends
393
+ #: on the trigger type (:attr;`trigger_type`). The sign is relevant:
394
+ #:
395
+ #: - A positive threshold detects peaks above this value.
396
+ #: - A negative threshold detects peaks below this value.
397
+ #:
398
+ #: If :obj:`None`, an estimated threshold is used, calculated as 75% of the extreme deviation
399
+ #: from the mean signal value. Default is :obj:`None`.
400
+ #:
401
+ #: E.g: If the mean value is :math:`0` and there are positive extrema at :math:`400` and
402
+ #: negative extrema at :math:`-800`. Then the estimated threshold would be
403
+ #: :math:`0.75 \cdot (-800) = -600`.
336
404
  threshold = Union(None, Float)
337
405
 
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
406
+ #: The maximum allowable variation in duration between two trigger instances. If any revolution
407
+ #: exceeds this variation threshold, a warning is issued. Default is ``0.02``.
341
408
  max_variation_of_duration = Float(0.02)
342
409
 
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.
410
+ #: Defines the length of "hunks" as a fraction of the estimated duration between two trigger
411
+ #: instances. If multiple peaks occur within a hunk, only one is retained based on
412
+ #: :attr:`multiple_peaks_in_hunk`. Default is ``0.1``.
347
413
  hunk_length = Float(0.1)
348
414
 
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'.
415
+ #: Specifies the type of trigger detection:
416
+ #:
417
+ #: - ``'dirac'``: A single impulse is considered a trigger. The sign of :attr:`threshold`
418
+ #: determines whether positive or negative peaks are detected.
419
+ #: - ``'rect'``: A repeating rectangular waveform is assumed. Only every second edge is
420
+ #: considered a trigger. The sign of :attr:`threshold` determines whether rising (``+``) or
421
+ #: falling (``-``) edges are used.
422
+ #:
423
+ #: Default is ``'dirac'``.
360
424
  trigger_type = Enum('dirac', 'rect')
361
425
 
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.
426
+ #: Defines the criterion for selecting a peak when multiple occur within a hunk (see
427
+ #: :attr:`hunk_length`):
428
+ #:
429
+ #: - ``'extremum'``: Selects the most extreme peak.
430
+ #: - ``'first'``: Selects the first peak encountered.
431
+ #:
432
+ #: Default is ``'extremum'``.
365
433
  multiple_peaks_in_hunk = Enum('extremum', 'first')
366
434
 
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
435
+ #: A tuple containing:
436
+ #:
437
+ #: - A :class:`numpy.ndarray` of sample indices corresponding to detected trigger events.
438
+ #: - The maximum number of samples between consecutive trigger peaks.
439
+ #: - The minimum number of samples between consecutive trigger peaks.
374
440
  trigger_data = Property(
375
441
  depends_on=[
376
442
  'source.digest',
@@ -382,7 +448,7 @@ class Trigger(TimeOut): # pragma: no cover
382
448
  ],
383
449
  )
384
450
 
385
- # internal identifier
451
+ #: A unique identifier for the trigger, based on its properties. (read-only)
386
452
  digest = Property(
387
453
  depends_on=[
388
454
  'source.digest',
@@ -500,23 +566,52 @@ class Trigger(TimeOut): # pragma: no cover
500
566
  return 0
501
567
 
502
568
  def result(self, num):
569
+ """
570
+ Generate signal data from the source without modification.
571
+
572
+ This method acts as a pass-through, providing data blocks directly from the :attr:`source`
573
+ generator. It is included for interface consistency but does not apply trigger-based
574
+ transformations to the data.
575
+
576
+ Parameters
577
+ ----------
578
+ num : :obj:`int`
579
+ Number of samples per block.
580
+
581
+ Yields
582
+ ------
583
+ :class:`numpy.ndarray`
584
+ An array containing ``num`` samples from the source signal.
585
+ The last block may contain fewer samples if the total number of samples is not
586
+ a multiple of ``num``.
587
+
588
+ Warnings
589
+ --------
590
+ This method is not implemented for trigger-based transformations.
591
+ A warning is issued, indicating that data is passed unprocessed.
592
+ """
503
593
  msg = 'result method not implemented yet! Data from source will be passed without transformation.'
504
594
  warn(msg, Warning, stacklevel=2)
505
595
  yield from self.source.result(num)
506
596
 
507
597
 
508
598
  class AngleTracker(MaskedTimeOut):
509
- """Calculates rotation angle and rpm per sample from a trigger signal
510
- using spline interpolation in the time domain.
599
+ """
600
+ Compute the rotational angle and RPM per sample from a trigger signal in the time domain.
511
601
 
512
- Gets samples from :attr:`trigger` and stores the angle and rpm samples in :meth:`angle` and
513
- :meth:`rpm`.
602
+ This class retrieves samples from the specified :attr:`trigger` signal and interpolates angular
603
+ position and rotational speed. The results are stored in the properties :attr:`angle` and
604
+ :attr:`rpm`.
605
+
606
+ The algorithm assumes a periodic trigger signal marking rotational events (e.g., a tachometer
607
+ pulse or an encoder signal) and interpolates the angle and RPM using cubic splines. It is
608
+ capable of handling different rotational directions and numbers of triggers per revolution.
514
609
  """
515
610
 
516
- # Trigger data from :class:`acoular.tprocess.Trigger`.
611
+ #: Trigger data source, expected to be an instance of :class:`Trigger`.
517
612
  trigger = Instance(Trigger)
518
613
 
519
- # internal identifier
614
+ #: A unique identifier for the tracker, based on its properties. (read-only)
520
615
  digest = Property(
521
616
  depends_on=[
522
617
  'source.digest',
@@ -528,28 +623,35 @@ class AngleTracker(MaskedTimeOut):
528
623
  ],
529
624
  )
530
625
 
531
- # Trigger signals per revolution,
532
- # defaults to 1.
626
+ #: Number of trigger signals per revolution. This allows tracking scenarios where multiple
627
+ #: trigger pulses occur per rotation. Default is ``1``, meaning a single trigger per revolution.
533
628
  trigger_per_revo = Int(1, desc='trigger signals per revolution')
534
629
 
535
- # Flag to set counter-clockwise (1) or clockwise (-1) rotation,
536
- # defaults to -1.
630
+ #: Rotation direction flag:
631
+ #:
632
+ #: - ``1``: counter-clockwise rotation.
633
+ #: - ``-1``: clockwise rotation.
634
+ #:
635
+ #: Default is ``-1``.
537
636
  rot_direction = Int(-1, desc='mathematical direction of rotation')
538
637
 
539
- # Points of interpolation used for spline,
540
- # defaults to 4.
638
+ #: Number of points used for spline interpolation. Default is ``4``.
541
639
  interp_points = Int(4, desc='Points of interpolation used for spline')
542
640
 
543
- # rotation angle in radians for first trigger position
641
+ #: Initial rotation angle (in radians) corresponding to the first trigger event. This allows
642
+ #: defining a custom starting reference angle. Default is ``0``.
544
643
  start_angle = Float(0, desc='rotation angle for trigger position')
545
644
 
546
- # revolutions per minute for each sample, read-only
645
+ #: Revolutions per minute (RPM) computed for each sample.
646
+ #: It is based on the trigger data. (read-only)
547
647
  rpm = Property(depends_on=['digest'], desc='revolutions per minute for each sample')
548
648
 
549
- # average revolutions per minute, read-only
649
+ #: Average revolutions per minute over the entire dataset.
650
+ #: It is computed based on the trigger intervals. (read-only)
550
651
  average_rpm = Property(depends_on=['digest'], desc='average revolutions per minute')
551
652
 
552
- # rotation angle in radians for each sample, read-only
653
+ #: Computed rotation angle (in radians) for each sample.
654
+ #: It is interpolated from the trigger data. (read-only)
553
655
  angle = Property(depends_on=['digest'], desc='rotation angle for each sample')
554
656
 
555
657
  # Internal flag to determine whether rpm and angle calculation has been processed,
@@ -572,12 +674,12 @@ class AngleTracker(MaskedTimeOut):
572
674
  return (abs(peakarray - value)).argmin()
573
675
 
574
676
  def _to_rpm_and_angle(self):
575
- """Internal helper function
576
- Calculates angles in radians for one or more instants in time.
677
+ # Internal helper function.
678
+ # Calculates angles in radians for one or more instants in time.
679
+
680
+ # Current version supports only trigger and sources with the same samplefreq.
681
+ # This behaviour may change in future releases.
577
682
 
578
- Current version supports only trigger and sources with the same samplefreq.
579
- This behaviour may change in future releases
580
- """
581
683
  # init
582
684
  ind = 0
583
685
  # trigger data
@@ -646,14 +748,6 @@ class AngleTracker(MaskedTimeOut):
646
748
  # calc average rpm from trigger data
647
749
  @cached_property
648
750
  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
751
  # trigger indices data
658
752
  peakloc = self.trigger.trigger_data[0]
659
753
  # calculation of average rpm in 1/min
@@ -661,18 +755,33 @@ class AngleTracker(MaskedTimeOut):
661
755
 
662
756
 
663
757
  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`.
758
+ """
759
+ Base class for spatial interpolation of microphone data.
760
+
761
+ This class retrieves samples from a specified source and performs spatial interpolation to
762
+ generate output at virtual microphone positions. The interpolation is executed using various
763
+ methods such as linear, spline, radial basis function (RBF), and inverse distance weighting
764
+ (IDW).
765
+
766
+ See Also
767
+ --------
768
+ :class:`SpatialInterpolatorRotation` : Spatial interpolation class for rotating sound sources.
769
+ :class:`SpatialInterpolatorConstantRotation` :
770
+ Performs spatial linear interpolation for sources undergoing constant rotation.
667
771
  """
668
772
 
669
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
773
+ #: The input data source. It must be an instance of a
774
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
775
+ #: It provides the time-domain pressure samples from microphones.
670
776
  source = Instance(SamplesGenerator)
671
777
 
672
- # :class:`~acoular.microphones.MicGeom` object that provides the real microphone locations.
778
+ #: The physical microphone geometry. An instance of :class:`~acoular.microphones.MicGeom` that
779
+ #: defines the positions of the real microphones used for measurement.
673
780
  mics = Instance(MicGeom(), desc='microphone geometry')
674
781
 
675
- # :class:`~acoular.microphones.MicGeom` object that provides the virtual microphone locations.
782
+ #: The virtual microphone geometry. This property defines the positions
783
+ #: of virtual microphones where interpolated pressure values are computed.
784
+ #: Default is the physical microphone geometry (:attr:`mics`).
676
785
  mics_virtual = Property(desc='microphone geometry')
677
786
 
678
787
  _mics_virtual = Instance(MicGeom, desc='internal microphone geometry;internal usage, read only')
@@ -685,11 +794,18 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
685
794
  def _set_mics_virtual(self, mics_virtual):
686
795
  self._mics_virtual = mics_virtual
687
796
 
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
797
+ #: Interpolation method used for spatial data estimation.
798
+ #:
799
+ #: Options:
800
+ #:
801
+ #: - ``'linear'``: Uses NumPy linear interpolation.
802
+ #: - ``'spline'``: Uses SciPy's CubicSpline interpolator
803
+ #: - ``'rbf-multiquadric'``: Radial basis function (RBF) interpolation with a multiquadric
804
+ #: kernel.
805
+ #: - ``'rbf-cubic'``: RBF interpolation with a cubic kernel.
806
+ #: - ``'IDW'``: Inverse distance weighting interpolation.
807
+ #: - ``'custom'``: Allows user-defined interpolation methods.
808
+ #: - ``'sinc'``: Uses sinc-based interpolation for signal reconstruction.
693
809
  method = Enum(
694
810
  'linear',
695
811
  'spline',
@@ -701,30 +817,54 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
701
817
  desc='method for interpolation used',
702
818
  )
703
819
 
704
- # spatial dimensionality of the array geometry
820
+ #: Defines the spatial dimensionality of the microphone array.
821
+ #:
822
+ #: Possible values:
823
+ #:
824
+ #: - ``'1D'``: Linear microphone arrays.
825
+ #: - ``'2D'``: Planar microphone arrays.
826
+ #: - ``'ring'``: Circular arrays where rotation needs to be considered.
827
+ #: - ``'3D'``: Three-dimensional microphone distributions.
828
+ #: - ``'custom'``: User-defined microphone arrangements.
705
829
  array_dimension = Enum('1D', '2D', 'ring', '3D', 'custom', desc='spatial dimensionality of the array geometry')
706
830
 
707
- # Sampling frequency of output signal, as given by :attr:`source`.
831
+ #: Sampling frequency of the output signal, inherited from the :attr:`source`. This defines the
832
+ #: rate at which microphone pressure samples are acquired and processed.
708
833
  sample_freq = Delegate('source', 'sample_freq')
709
834
 
710
- # Number of channels in output.
835
+ #: Number of channels in the output data. This corresponds to the number of virtual microphone
836
+ #: positions where interpolated pressure values are computed. The value is ´determined based on
837
+ #: the :attr:`mics_virtual` geometry.
711
838
  num_channels = Property()
712
839
 
713
- # Number of samples in output, as given by :attr:`source`.
840
+ #: Number of time-domain samples in the output signal, inherited from the :attr:`source`.
714
841
  num_samples = Delegate('source', 'num_samples')
715
842
 
716
- # Interpolate a point at the origin of the Array geometry
843
+ #: Whether to interpolate a virtual microphone at the origin. If set to ``True``, an additional
844
+ #: virtual microphone position at the coordinate origin :math:`(0,0,0)` will be interpolated.
717
845
  interp_at_zero = Bool(False)
718
846
 
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).
847
+ #: Transformation matrix for coordinate system alignment.
848
+ #:
849
+ #: This 3x3 orthogonal matrix is used to align the microphone coordinates such that rotations
850
+ #: occur around the z-axis. If the original coordinates do not conform to the expected alignment
851
+ #: (where the x-axis transitions into the y-axis upon rotation), applying this matrix modifies
852
+ #: the coordinates accordingly. The transformation is defined as
853
+ #:
854
+ #: .. math::
855
+ #: \begin{bmatrix}x'\\y'\\z'\end{bmatrix} = Q \cdot \begin{bmatrix}x\\y\\z\end{bmatrix}
856
+ #:
857
+ #: where :math:`Q` is the transformation matrix and :math:`(x', y', z')` are the modified
858
+ #: coordinates. If no transformation is needed, :math:`Q` defaults to the identity matrix.
724
859
  Q = CArray(dtype=float64, shape=(3, 3), value=identity(3))
725
860
 
861
+ #: Number of neighboring microphones used in IDW interpolation. This parameter determines how
862
+ #: many physical microphones contribute to the weighted sum in inverse distance weighting (IDW)
863
+ #: interpolation.
726
864
  num_IDW = Int(3, desc='number of neighboring microphones, DEFAULT=3') # noqa: N815
727
865
 
866
+ #: Weighting exponent for IDW interpolation. This parameter controls the influence of distance
867
+ #: in inverse distance weighting (IDW). A higher value gives more weight to closer microphones.
728
868
  p_weight = Float(
729
869
  2,
730
870
  desc='used in interpolation for virtual microphone, weighting power exponent for IDW',
@@ -735,7 +875,7 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
735
875
  depends_on=['mics.digest', 'mics_virtual.digest', 'method', 'array_dimension', 'interp_at_zero'],
736
876
  )
737
877
 
738
- # internal identifier
878
+ #: Unique identifier for the current configuration of the interpolator. (read-only)
739
879
  digest = Property(
740
880
  depends_on=[
741
881
  'mics.digest',
@@ -760,51 +900,70 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
760
900
  return self._virtNewCoord_func(self.mics.mpos, self.mics_virtual.mpos, self.method, self.array_dimension)
761
901
 
762
902
  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))
903
+ """
904
+ Compute a modified sinc function for use in Radial Basis Function (RBF) approximation.
765
905
 
766
- def _virtNewCoord_func(self, mpos, mpos_virt, method, array_dimension): # noqa N802
767
- """Core functionality for getting the interpolation.
906
+ This function is used as a kernel in sinc-based interpolation methods, where the sinc
907
+ function serves as a basis function for reconstructing signals based on spatially
908
+ distributed microphone data. The function is scaled according to the number of virtual
909
+ microphone positions, ensuring accurate signal approximation.
768
910
 
769
911
  Parameters
770
912
  ----------
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
913
+ r : :obj:`float` or :obj:`list` of :obj:`floats<float>`
914
+ The radial distance(s) at which to evaluate the sinc function, typically representing
915
+ the spatial separation between real and virtual microphone positions.
779
916
 
780
917
  Returns
781
918
  -------
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].
919
+ :class:`numpy.ndarray`
920
+ Evaluated sinc function values at the given radial distances.
807
921
  """
922
+ return sinc((r * self.mics_virtual.mpos.shape[1]) / (pi))
923
+
924
+ def _virtNewCoord_func(self, mpos, mpos_virt, method, array_dimension): # noqa N802
925
+ # Core functionality for getting the interpolation.
926
+ #
927
+ # Parameters
928
+ # ----------
929
+ # mpos : float[3, nPhysicalMics]
930
+ # The mic positions of the physical (really existing) mics
931
+ # mpos_virt : float[3, nVirtualMics]
932
+ # The mic positions of the virtual mics
933
+ # method : string
934
+ # The Interpolation method to use
935
+ # array_dimension : string
936
+ # The Array Dimensions in cylinder coordinates
937
+ #
938
+ # Returns
939
+ # -------
940
+ # mesh : List[]
941
+ # The items of these lists depend on the reduced interpolation dimension of each
942
+ # subarray.
943
+ # If the Array is 1D the list items are:
944
+ # 1. item : float64[nMicsInSpecificSubarray]
945
+ # Ordered positions of the real mics on the new 1d axis,
946
+ # to be used as inputs for numpys interp.
947
+ # 2. item : int64[nMicsInArray]
948
+ # Indices identifying how the measured pressures must be evaluated, s.t. the
949
+ # entries of the previous item (see last line) correspond to their initial
950
+ # pressure values.
951
+ # If the Array is 2D or 3d the list items are:
952
+ # 1. item : Delaunay mesh object
953
+ # Delaunay mesh (see scipy.spatial.Delaunay) for the specific Array
954
+ # 2. item : int64[nMicsInArray]
955
+ # same as 1d case, BUT with the difference, that here the rotational periodicity
956
+ # is handled, when constructing the mesh. Therefore, the mesh could have more
957
+ # vertices than the actual Array mics.
958
+ #
959
+ # virtNewCoord : float64[3, nVirtualMics]
960
+ # Projection of each virtual mic onto its new coordinates. The columns of virtNewCoord
961
+ # correspond to [phi, rho, z].
962
+ #
963
+ # newCoord : float64[3, nMics]
964
+ # Projection of each mic onto its new coordinates. The columns of newCoordinates
965
+ # correspond to [phi, rho, z].
966
+
808
967
  # init positions of virtual mics in cyl coordinates
809
968
  nVirtMics = mpos_virt.shape[1]
810
969
  virtNewCoord = zeros((3, nVirtMics))
@@ -909,25 +1068,23 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
909
1068
  return mesh, virtNewCoord, newCoord
910
1069
 
911
1070
  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
- """
1071
+ # Performs the actual Interpolation.
1072
+ #
1073
+ # Parameters
1074
+ # ----------
1075
+ # p : float[num, nMicsReal]
1076
+ # The pressure field of the yielded sample at real mics.
1077
+ # phi_delay : empty list (default) or float[num]
1078
+ # If passed (rotational case), this list contains the angular delay
1079
+ # of each sample in rad.
1080
+ # period : None (default) or float
1081
+ # If periodicity can be assumed (rotational case)
1082
+ # this parameter contains the periodicity length
1083
+ #
1084
+ # Returns
1085
+ # -------
1086
+ # pInterp : float[num, nMicsVirtual]
1087
+ # The interpolated time data at the virtual mics
931
1088
  if phi_delay is None:
932
1089
  phi_delay = []
933
1090
  # number of time samples
@@ -1150,21 +1307,50 @@ class SpatialInterpolator(TimeOut): # pragma: no cover
1150
1307
  return pInterp
1151
1308
 
1152
1309
  def result(self, num):
1310
+ """
1311
+ Generate interpolated microphone data over time.
1312
+
1313
+ This method retrieves pressure samples from the physical microphones and applies spatial
1314
+ interpolation to estimate the pressure at virtual microphone locations.
1315
+ The interpolation method is determined by :attr:`method`.
1316
+
1317
+ Parameters
1318
+ ----------
1319
+ num : :obj:`int`
1320
+ Number of samples per block.
1321
+
1322
+ Yields
1323
+ ------
1324
+ :class:`numpy.ndarray`
1325
+ An array of shape (``num``, `n`), where `n` is the number of virtual microphones,
1326
+ containing interpolated pressure values for the virtual microphones at each time step.
1327
+ The last block may contain fewer samples if the total number of samples is not
1328
+ a multiple of ``num``.
1329
+ """
1153
1330
  msg = 'result method not implemented yet! Data from source will be passed without transformation.'
1154
1331
  warn(msg, Warning, stacklevel=2)
1155
1332
  yield from self.source.result(num)
1156
1333
 
1157
1334
 
1158
1335
  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`.
1336
+ """
1337
+ Spatial interpolation class for rotating sound sources.
1338
+
1339
+ This class extends :attr:`SpatialInterpolator` to handle sources that undergo rotational
1340
+ movement. It retrieves samples from the :attr:`source` attribute and angle data from the
1341
+ :attr:`AngleTracker` instance (:attr:`angle_source`). Using these inputs, it computes
1342
+ interpolated outputs through the :meth:`result` generator method.
1161
1343
 
1344
+ See Also
1345
+ --------
1346
+ :class:`SpatialInterpolator`: Base class for spatial interpolation of microphone data.
1162
1347
  """
1163
1348
 
1164
- # Angle data from AngleTracker class
1349
+ #: Provides real-time tracking of the source's rotation angles,
1350
+ #: instance of :attr:`AngleTracker`.
1165
1351
  angle_source = Instance(AngleTracker)
1166
1352
 
1167
- # Internal identifier
1353
+ #: Unique identifier for the current configuration of the interpolator. (read-only)
1168
1354
  digest = Property(
1169
1355
  depends_on=[
1170
1356
  'source.digest',
@@ -1183,19 +1369,26 @@ class SpatialInterpolatorRotation(SpatialInterpolator): # pragma: no cover
1183
1369
  return digest(self)
1184
1370
 
1185
1371
  def result(self, num=128):
1186
- """Python generator that yields the output block-wise.
1372
+ """
1373
+ Generate interpolated output samples in block-wise fashion.
1374
+
1375
+ This method acts as a generator, yielding time-domain time signal samples that have been
1376
+ spatially interpolated based on rotational movement.
1187
1377
 
1188
1378
  Parameters
1189
1379
  ----------
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
-
1380
+ num : :obj:`int`, optional
1381
+ Number of samples per block. Default is ``128``.
1382
+
1383
+ Yields
1384
+ ------
1385
+ :class:`numpy.ndarray`
1386
+ Interpolated time signal samples in blocks of shape
1387
+ (``num``, :attr:`~SpatialInterpolator.num_channels`), where
1388
+ :attr:`~SpatialInterpolator.num_channels` is inherited from the
1389
+ :class:`SpatialInterpolator` base class.
1390
+ The last block may contain fewer samples if the total number of samples is not
1391
+ a multiple of ``num``.
1199
1392
  """
1200
1393
  # period for rotation
1201
1394
  period = 2 * pi
@@ -1211,16 +1404,25 @@ class SpatialInterpolatorRotation(SpatialInterpolator): # pragma: no cover
1211
1404
 
1212
1405
 
1213
1406
  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
1407
  """
1408
+ Performs spatial linear interpolation for sources undergoing constant rotation.
1218
1409
 
1219
- # Rotational speed in rps. Positive, if rotation is around positive z-axis sense,
1220
- # which means from x to y axis.
1410
+ This class interpolates signals from a rotating sound source based on a constant rotational
1411
+ speed. It retrieves samples from the :attr:`source` and applies interpolation before
1412
+ generating output through the :meth:`result` generator.
1413
+
1414
+ See Also
1415
+ --------
1416
+ :class:`SpatialInterpolator` : Base class for spatial interpolation of microphone data.
1417
+ :class:`SpatialInterpolatorRotation` : Spatial interpolation class for rotating sound sources.
1418
+ """
1419
+
1420
+ #: Rotational speed of the source in revolutions per second (rps). A positive value indicates
1421
+ #: counterclockwise rotation around the positive z-axis, meaning motion from the x-axis toward
1422
+ #: the y-axis.
1221
1423
  rotational_speed = Float(0.0)
1222
1424
 
1223
- # internal identifier
1425
+ #: Unique identifier for the current configuration of the interpolator. (read-only)
1224
1426
  digest = Property(
1225
1427
  depends_on=[
1226
1428
  'source.digest',
@@ -1239,19 +1441,28 @@ class SpatialInterpolatorConstantRotation(SpatialInterpolator): # pragma: no co
1239
1441
  return digest(self)
1240
1442
 
1241
1443
  def result(self, num=1):
1242
- """Python generator that yields the output block-wise.
1444
+ """
1445
+ Generate interpolated time signal data in blocks of size ``num``.
1446
+
1447
+ This generator method continuously processes incoming time signal data while applying
1448
+ rotational interpolation. The phase delay is computed based on the rotational speed and
1449
+ applied to the signal.
1243
1450
 
1244
1451
  Parameters
1245
1452
  ----------
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
-
1453
+ num : :obj:`int`, optional
1454
+ Number of samples per block.
1455
+ Default is ``1``.
1456
+
1457
+ Yields
1458
+ ------
1459
+ :class:`numpy.ndarray`
1460
+ An array containing the interpolated time signal samples in blocks of shape
1461
+ (``num``, :attr:`~SpatialInterpolator.num_channels`), where
1462
+ :attr:`~SpatialInterpolator.num_channels` is inherited from the
1463
+ :class:`SpatialInterpolator` base class.
1464
+ The last block may contain fewer samples if the total number of samples is not
1465
+ a multiple of ``num``.
1255
1466
  """
1256
1467
  omega = 2 * pi * self.rotational_speed
1257
1468
  period = 2 * pi
@@ -1265,32 +1476,44 @@ class SpatialInterpolatorConstantRotation(SpatialInterpolator): # pragma: no co
1265
1476
 
1266
1477
 
1267
1478
  class Mixer(TimeOut):
1268
- """Mixes the signals from several sources."""
1479
+ """
1480
+ Mix signals from multiple sources into a single output.
1481
+
1482
+ This class takes a :attr:`primary time signal source<source>` and a list of
1483
+ :attr:`additional sources<sources>` with the same sampling rates and channel counts across all
1484
+ :attr:`primary time signal source<source>`, and outputs a mixed signal.
1485
+ The mixing process is performed block-wise using a generator.
1486
+
1487
+ If one of the :attr:`additional sources<sources>` holds a shorter signal than the other
1488
+ sources the :meth:`result` method will stop yielding mixed time signal at that point.
1489
+ """
1269
1490
 
1270
- # Data source; :class:`~acoular.base.SamplesGenerator` object.
1491
+ #: The primary time signal source. It must be an instance of a
1492
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
1271
1493
  source = Instance(SamplesGenerator)
1272
1494
 
1273
- # List of additional :class:`~acoular.base.SamplesGenerator` objects
1274
- # to be mixed.
1495
+ #: A list of additional time signal sources to be mixed with the primary source, each must be an
1496
+ #: instance of :class:`~acoular.base.SamplesGenerator`.
1275
1497
  sources = List(Instance(SamplesGenerator, ()))
1276
1498
 
1277
- # Sampling frequency of the signal as given by :attr:`source`.
1499
+ #: The sampling frequency of the primary time signal, delegated from :attr:`source`.
1278
1500
  sample_freq = Delegate('source')
1279
1501
 
1280
- # Number of channels in output as given by :attr:`source`.
1502
+ #: The number of channels in the output, delegated from :attr:`source`.
1281
1503
  num_channels = Delegate('source')
1282
1504
 
1283
- # Number of samples in output as given by :attr:`source`.
1505
+ #: The number of samples in the output, delegated from :attr:`source`.
1284
1506
  num_samples = Delegate('source')
1285
1507
 
1286
- # internal identifier
1508
+ #: Internal identifier that tracks changes in the :attr:`sources` list.
1287
1509
  sdigest = Str()
1288
1510
 
1289
1511
  @observe('sources.items.digest')
1290
1512
  def _set_sourcesdigest(self, event): # noqa ARG002
1291
1513
  self.sdigest = ldigest(self.sources)
1292
1514
 
1293
- # internal identifier
1515
+ #: A unique identifier for the Mixer instance, based on the :attr:`primary source<source>` and
1516
+ #: the :attr:`list of additional sources<sources>`.
1294
1517
  digest = Property(depends_on=['source.digest', 'sdigest'])
1295
1518
 
1296
1519
  @cached_property
@@ -1298,7 +1521,16 @@ class Mixer(TimeOut):
1298
1521
  return digest(self)
1299
1522
 
1300
1523
  def validate_sources(self):
1301
- """Validates if sources fit together."""
1524
+ # Validate whether the additional sources are compatible with the primary source.
1525
+ #
1526
+ # This method checks if all sources have the same sampling frequency and the same number of
1527
+ # channels. If a mismatch is detected, a :obj:`ValueError` is raised.
1528
+ #
1529
+ # Raises
1530
+ # ------
1531
+ # :obj:`ValueError`
1532
+ # If any source in :attr:`sources` has a different sampling frequency or
1533
+ # number of channels than :attr:`source`.
1302
1534
  if self.source:
1303
1535
  for s in self.sources:
1304
1536
  if self.sample_freq != s.sample_freq:
@@ -1309,21 +1541,34 @@ class Mixer(TimeOut):
1309
1541
  raise ValueError(msg)
1310
1542
 
1311
1543
  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.
1544
+ """
1545
+ Generate mixed time signal data in blocks of ``num`` samples.
1315
1546
 
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).
1547
+ This generator method retrieves time signal data from all sources and sums them together
1548
+ to produce a combined output. The data from each source is processed in blocks of the
1549
+ same size, ensuring synchronized mixing.
1321
1550
 
1322
- Returns
1323
- -------
1324
- Samples in blocks of shape (num, num_channels).
1325
- The last block may be shorter than num.
1551
+ .. note::
1326
1552
 
1553
+ Yielding stops when one of the additionally provied signals ends; i.e. if one of the
1554
+ additional sources holds a signal of shorter length than that of the
1555
+ :attr:`primary source<source>` that (shorter) signal forms the lower bound of the length
1556
+ of the mixed time signal yielded.
1557
+
1558
+ Parameters
1559
+ ----------
1560
+ num : :obj:`int`
1561
+ Number of samples per block.
1562
+
1563
+ Yields
1564
+ ------
1565
+ :class:`numpy.ndarray`
1566
+ An array containing the mixed time samples in blocks of shape
1567
+ (``num``, :attr:`~acoular.base.TimeOut.num_channels`), where
1568
+ :attr:`~acoular.base.TimeOut.num_channels` is inhereted from the
1569
+ :class:`~acoular.base.TimeOut` base class.
1570
+ The last block may contain fewer samples if the total number of samples is not
1571
+ a multiple of ``num``.
1327
1572
  """
1328
1573
  # check whether all sources fit together
1329
1574
  self.validate_sources()
@@ -1345,52 +1590,96 @@ class Mixer(TimeOut):
1345
1590
 
1346
1591
 
1347
1592
  class TimePower(TimeOut):
1348
- """Calculates time-depended power of the signal."""
1593
+ """
1594
+ Calculate the time-dependent power of a signal by squaring its samples.
1595
+
1596
+ This class computes the power of the input signal by squaring the value of each sample. It
1597
+ processes the signal in blocks, making it suitable for large datasets or real-time signal
1598
+ processing. The power is calculated on a per-block basis, and each block of the output is
1599
+ yielded as a NumPy array.
1600
+
1601
+ Attributes
1602
+ ----------
1603
+ source : SamplesGenerator
1604
+ The input data source, which provides the time signal or signal samples
1605
+ to be processed. It must be an instance of :class:`~acoular.base.SamplesGenerator`
1606
+ or any derived class that provides a `result()` method.
1607
+ """
1349
1608
 
1350
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
1609
+ #: The input data source. It must be an instance of a
1610
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
1351
1611
  source = Instance(SamplesGenerator)
1352
1612
 
1353
1613
  def result(self, num):
1354
- """Python generator that yields the output block-wise.
1614
+ """
1615
+ Generate the time-dependent power of the input signal in blocks.
1616
+
1617
+ This method iterates through the signal samples provided by the :attr:`source` and
1618
+ calculates the power by squaring each sample. The output is yielded block-wise to
1619
+ facilitate processing large signals in chunks.
1355
1620
 
1356
1621
  Parameters
1357
1622
  ----------
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
-
1623
+ num : :obj:`int`
1624
+ Number of samples per block.
1625
+
1626
+ Yields
1627
+ ------
1628
+ :class:`numpy.ndarray`
1629
+ An array containing the squared samples from the :attr:`source`. Each block will have
1630
+ the shape (``num``, :attr:`~acoular.base.TimeOut.num_channels`), where
1631
+ :attr:`~acoular.base.TimeOut.num_channels` is inhereted from the
1632
+ :class:`~acoular.base.TimeOut` base class.
1633
+ The last block may contain fewer samples if the total number of samples is not
1634
+ a multiple of ``num``.
1368
1635
  """
1369
1636
  for temp in self.source.result(num):
1370
1637
  yield temp * temp
1371
1638
 
1372
1639
 
1373
1640
  class TimeCumAverage(TimeOut):
1374
- """Calculates cumulative average of the signal, useful for Leq."""
1641
+ """
1642
+ Calculates the cumulative average of the signal.
1375
1643
 
1376
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
1644
+ This class computes the cumulative average of the input signal over time, which is useful for
1645
+ metrics like the Equivalent Continuous Sound Level (Leq). It processes the signal in blocks,
1646
+ maintaining a running average of the samples. The result is yielded in blocks, allowing for
1647
+ memory-efficient processing of large datasets.
1648
+ """
1649
+
1650
+ #: The input data source. It must be an instance of a
1651
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
1377
1652
  source = Instance(SamplesGenerator)
1378
1653
 
1379
1654
  def result(self, num):
1380
- """Python generator that yields the output block-wise.
1655
+ """
1656
+ Generate the cumulative average of the input signal in blocks.
1657
+
1658
+ This method iterates through the signal samples provided by the :attr:`source`, and for each
1659
+ block, it computes the cumulative average of the samples up to that point. The result is
1660
+ yielded in blocks, with each block containing the cumulative average of the signal up to
1661
+ that sample.
1381
1662
 
1382
1663
  Parameters
1383
1664
  ----------
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
-
1665
+ num : :obj:`int`
1666
+ Number of samples per block.
1667
+
1668
+ Yields
1669
+ ------
1670
+ :class:`numpy.ndarray`
1671
+ An array containing the cumulative average of the samples. Each block will have the
1672
+ shape (``num``, :attr:`~acoular.base.TimeOut.num_channels`), where
1673
+ :attr:`~acoular.base.TimeOut.num_channels` is inhereted from the :attr:`source`.
1674
+ The last block may contain fewer samples if the total number of samples is not
1675
+ a multiple of ``num``.
1676
+
1677
+ Notes
1678
+ -----
1679
+ The cumulative average is updated iteratively by considering the previously accumulated sum
1680
+ and the current block of samples. For each new sample, the cumulative average is
1681
+ recalculated by summing the previous cumulative value and the new samples, then dividing by
1682
+ the total number of samples up to that point.
1394
1683
  """
1395
1684
  count = (arange(num) + 1)[:, newaxis]
1396
1685
  for i, temp in enumerate(self.source.result(num)):
@@ -1404,26 +1693,49 @@ class TimeCumAverage(TimeOut):
1404
1693
 
1405
1694
 
1406
1695
  class TimeReverse(TimeOut):
1407
- """Calculates the time-reversed signal of a source."""
1696
+ """
1697
+ Calculates the time-reversed signal of a source.
1698
+
1699
+ This class takes the input signal from a source and computes the time-reversed version of the
1700
+ signal. It processes the signal in blocks, yielding the time-reversed signal block by block.
1701
+ This can be useful for various signal processing tasks, such as creating echoes or reversing
1702
+ the playback of time signal signals.
1703
+ """
1408
1704
 
1409
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
1705
+ #: The input data source. It must be an instance of a
1706
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
1410
1707
  source = Instance(SamplesGenerator)
1411
1708
 
1412
1709
  def result(self, num):
1413
- """Python generator that yields the output block-wise.
1710
+ """
1711
+ Generate the time-reversed version of the input signal block-wise.
1712
+
1713
+ This method processes the signal provided by the :attr:`source` in blocks, and for each
1714
+ block, it produces the time-reversed version of the signal. The result is yielded in blocks,
1715
+ with each block containing the time-reversed version of the signal for that segment.
1716
+ The signal is reversed in time by flipping the order of samples within each block.
1414
1717
 
1415
1718
  Parameters
1416
1719
  ----------
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
-
1720
+ num : :obj:`int`
1721
+ Number of samples per block.
1722
+
1723
+ Yields
1724
+ ------
1725
+ :class:`numpy.ndarray`
1726
+ An array containing the time-reversed version of the signal for the current block.
1727
+ Each block will have the shape (``num``, :attr:`acoular.base.TimeOut.num_channels`),
1728
+ where :attr:`~acoular.base.TimeOut.num_channels` is inherited from the :attr:`source`.
1729
+ The last block may contain fewer samples if the total number of samples is not
1730
+ a multiple of ``num``.
1731
+
1732
+ Notes
1733
+ -----
1734
+ The time-reversal is achieved by reversing the order of samples in each block of the signal.
1735
+ The :meth:`result` method first collects all the blocks from the source, then processes them
1736
+ in reverse order, yielding the time-reversed signal in blocks. The first block yielded
1737
+ corresponds to the last block of the source signal, and so on, until the entire signal has
1738
+ been processed in reverse.
1427
1739
  """
1428
1740
  result_list = []
1429
1741
  result_list.extend(self.source.result(num))
@@ -1439,37 +1751,56 @@ class TimeReverse(TimeOut):
1439
1751
 
1440
1752
 
1441
1753
  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.
1754
+ """
1755
+ Abstract base class for IIR filters using SciPy's :func:`~scipy.signal.lfilter`.
1756
+
1757
+ This class implements a digital Infinite Impulse Response (IIR) filter that applies filtering to
1758
+ a given signal in a block-wise manner. The filter coefficients can be dynamically changed during
1759
+ processing.
1760
+
1761
+ See Also
1762
+ --------
1763
+ :func:`scipy.signal.lfilter` :
1764
+ Filter data along one-dimension with an IIR or FIR (finite impulse response) filter.
1765
+ :func:`scipy.signal.sosfilt` :
1766
+ Filter data along one dimension using cascaded second-order sections.
1767
+ :class:`FiltOctave` :
1768
+ Octave or third-octave bandpass filter (causal, with non-zero phase delay).
1769
+ :class:`FiltFiltOctave` : Octave or third-octave bandpass filter with zero-phase distortion.
1447
1770
  """
1448
1771
 
1449
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
1772
+ #: The input data source. It must be an instance of a
1773
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
1450
1774
  source = Instance(SamplesGenerator)
1451
1775
 
1452
- # Filter coefficients
1776
+ #: Second-order sections representation of the filter coefficients.
1777
+ #: This property is dynamically updated and can change during signal processing.
1453
1778
  sos = Property()
1454
1779
 
1455
1780
  def _get_sos(self):
1456
1781
  return tf2sos([1], [1])
1457
1782
 
1458
1783
  def result(self, num):
1459
- """Python generator that yields the output block-wise.
1784
+ """
1785
+ Apply the IIR filter to the input signal and yields filtered data block-wise.
1786
+
1787
+ This method processes the signal provided by :attr:`source`, applying the defined filter
1788
+ coefficients (:attr:`sos`) using the :func:`scipy.signal.sosfilt` function. The filtering
1789
+ is performed in a streaming fashion, yielding blocks of filtered signal data.
1460
1790
 
1461
1791
  Parameters
1462
1792
  ----------
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
-
1793
+ num : :obj:`int`
1794
+ Number of samples per block.
1795
+
1796
+ Yields
1797
+ ------
1798
+ :class:`numpy.ndarray`
1799
+ An array containing the bandpass-filtered signal for the current block. Each block has
1800
+ the shape (``num``, :attr:`~acoular.base.TimeOut.num_channels`), where
1801
+ :attr:`~acoular.base.TimeOut.num_channels` is inherited from the :attr:`source`.
1802
+ The last block may contain fewer samples if the total number of samples is not
1803
+ a multiple of ``num``.
1473
1804
  """
1474
1805
  sos = self.sos
1475
1806
  zi = zeros((sos.shape[0], 2, self.source.num_channels))
@@ -1481,20 +1812,42 @@ class Filter(TimeOut):
1481
1812
 
1482
1813
 
1483
1814
  class FiltOctave(Filter):
1484
- """Octave or third-octave filter (causal, non-zero phase delay)."""
1815
+ """
1816
+ Octave or third-octave bandpass filter (causal, with non-zero phase delay).
1817
+
1818
+ This class implements a bandpass filter that conforms to octave or third-octave frequency band
1819
+ standards. The filter is designed using a second-order section (SOS) Infinite Impulse Response
1820
+ (IIR) approach.
1821
+
1822
+ The filtering process introduces a non-zero phase delay due to its causal nature. The center
1823
+ frequency and the octave fraction determine the frequency band characteristics.
1824
+
1825
+ See Also
1826
+ --------
1827
+ :class:`Filter` : The base class implementing a general IIR filter.
1828
+ :class:`FiltFiltOctave` : Octave or third-octave bandpass filter with zero-phase distortion.
1829
+ """
1485
1830
 
1486
- # Band center frequency; defaults to 1000.
1831
+ #: The center frequency of the octave or third-octave band. Default is ``1000``.
1487
1832
  band = Float(1000.0, desc='band center frequency')
1488
1833
 
1489
- # Octave fraction: 'Octave' or 'Third octave'; defaults to 'Octave'.
1834
+ #: Defines whether the filter is an octave-band or third-octave-band filter.
1835
+ #:
1836
+ #: - ``'Octave'``: Full octave band filter.
1837
+ #: - ``'Third octave'``: Third-octave band filter.
1838
+ #:
1839
+ #: Default is ``'Octave'``.
1490
1840
  fraction = Map({'Octave': 1, 'Third octave': 3}, default_value='Octave', desc='fraction of octave')
1491
1841
 
1492
- # Filter order
1842
+ #: The order of the IIR filter, which affects the steepness of the filter's roll-off.
1843
+ #: Default is ``3``.
1493
1844
  order = Int(3, desc='IIR filter order')
1494
1845
 
1846
+ #: Second-order sections representation of the filter coefficients. This property depends on
1847
+ #: :attr:`band`, :attr:`fraction`, :attr:`order`, and the source's digest.
1495
1848
  sos = Property(depends_on=['band', 'fraction', 'source.digest', 'order'])
1496
1849
 
1497
- # internal identifier
1850
+ #: A unique identifier for the filter, based on its properties. (read-only)
1498
1851
  digest = Property(depends_on=['source.digest', 'band', 'fraction', 'order'])
1499
1852
 
1500
1853
  @cached_property
@@ -1503,6 +1856,25 @@ class FiltOctave(Filter):
1503
1856
 
1504
1857
  @cached_property
1505
1858
  def _get_sos(self):
1859
+ # Compute the second-order section coefficients for the bandpass filter.
1860
+
1861
+ # The filter design follows ANSI S1.11-1987 standards and adjusts
1862
+ # filter edge frequencies to maintain correct power bandwidth.
1863
+
1864
+ # The filter is implemented using a Butterworth design, with
1865
+ # appropriate frequency scaling to match the desired octave band.
1866
+
1867
+ # Returns
1868
+ # -------
1869
+ # :class:`numpy.ndarray`
1870
+ # SOS (second-order section) coefficients for the filter.
1871
+
1872
+ # Raises
1873
+ # ------
1874
+ # :obj:`ValueError`
1875
+ # If the center frequency (:attr:`band`) is too high relative to
1876
+ # the sampling frequency.
1877
+
1506
1878
  # filter design
1507
1879
  fs = self.sample_freq
1508
1880
  # adjust filter edge frequencies for correct power bandwidth (see ANSI 1.11 1987
@@ -1522,16 +1894,31 @@ class FiltOctave(Filter):
1522
1894
 
1523
1895
 
1524
1896
  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!
1897
+ """
1898
+ Octave or third-octave bandpass filter with zero-phase distortion.
1899
+
1900
+ This filter applies an IIR bandpass filter in both forward and reverse directions, effectively
1901
+ eliminating phase distortion. It provides zero-phase filtering but requires significantly more
1902
+ memory compared to causal filtering.
1903
+
1904
+ See Also
1905
+ --------
1906
+ :class:`Filter` : The base class implementing a general IIR filter.
1907
+ :class:`FiltOctave` : The standard octave or third-octave filter with causal filtering.
1908
+
1909
+ Notes
1910
+ -----
1911
+ - Due to the double-pass filtering, additional bandwidth correction is applied to maintain
1912
+ accurate frequency response.
1913
+ - This approach requires storing the entire signal in memory before processing, making it
1914
+ unsuitable for real-time applications with large datasets.
1529
1915
  """
1530
1916
 
1531
- # Filter order (applied for forward filter and backward filter)
1917
+ #: The half-order of the IIR filter, applied twice (once forward and once backward). This
1918
+ #: results in a final filter order twice as large as the specified value. Default is ``2``.
1532
1919
  order = Int(2, desc='IIR filter half order')
1533
1920
 
1534
- # internal identifier
1921
+ #: A unique identifier for the filter, based on its properties. (read-only)
1535
1922
  digest = Property(depends_on=['source.digest', 'band', 'fraction', 'order'])
1536
1923
 
1537
1924
  @cached_property
@@ -1540,6 +1927,22 @@ class FiltFiltOctave(FiltOctave):
1540
1927
 
1541
1928
  @cached_property
1542
1929
  def _get_sos(self):
1930
+ # Compute the second-order section (SOS) coefficients for the filter.
1931
+ #
1932
+ # The filter design follows ANSI S1.11-1987 standards and incorporates additional bandwidth
1933
+ # correction to compensate for the double-pass filtering effect.
1934
+ #
1935
+ # Returns
1936
+ # -------
1937
+ # :class:`numpy.ndarray`
1938
+ # SOS (second-order section) coefficients for the filter.
1939
+ #
1940
+ # Raises
1941
+ # ------
1942
+ # :obj:`ValueError`
1943
+ # If the center frequency (:attr:`band`) is too high relative to the
1944
+ # sampling frequency.
1945
+
1543
1946
  # filter design
1544
1947
  fs = self.sample_freq
1545
1948
  # adjust filter edge frequencies for correct power bandwidth (see FiltOctave)
@@ -1560,20 +1963,31 @@ class FiltFiltOctave(FiltOctave):
1560
1963
  return butter(self.order, [om1, om2], 'bandpass', output='sos')
1561
1964
 
1562
1965
  def result(self, num):
1563
- """Python generator that yields the output block-wise.
1966
+ """
1967
+ Apply the filter to the input signal and yields filtered data block-wise.
1968
+
1969
+ The input signal is first stored in memory, then filtered in both forward and reverse
1970
+ directions to achieve zero-phase distortion. The processed signal is yielded in blocks.
1564
1971
 
1565
1972
  Parameters
1566
1973
  ----------
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
-
1974
+ num : :obj:`int`
1975
+ Number of samples per block.
1976
+
1977
+ Yields
1978
+ ------
1979
+ :class:`numpy.ndarray`
1980
+ An array containing the filtered signal for the current block. Each block has shape
1981
+ (``num``, :attr:`~acoular.base.TimeOut.num_channels`), where
1982
+ :attr:`~acoular.base.TimeOut.num_channels` is inherited from the :attr:`source`.
1983
+ The last block may contain fewer samples if the total number of samples is not
1984
+ a multiple of ``num``.
1985
+
1986
+ Notes
1987
+ -----
1988
+ - This method requires the entire signal to be stored in memory, making it unsuitable for
1989
+ streaming or real-time applications.
1990
+ - Filtering is performed separately for each channel to optimize memory usage.
1577
1991
  """
1578
1992
  sos = self.sos
1579
1993
  data = empty((self.source.num_samples, self.source.num_channels))
@@ -1593,17 +2007,36 @@ class FiltFiltOctave(FiltOctave):
1593
2007
 
1594
2008
 
1595
2009
  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
2010
  """
2011
+ Compute an exponentially weighted moving average of the input signal.
1600
2012
 
1601
- # time weighting
2013
+ This filter implements exponential averaging as defined in IEC 61672-1, which is commonly used
2014
+ for sound level measurements. The time weighting determines how quickly past values decay in
2015
+ significance.
2016
+
2017
+ See Also
2018
+ --------
2019
+ :class:`Filter` : Base class for implementing IIR filters.
2020
+
2021
+ Notes
2022
+ -----
2023
+ The `Impulse` (``'I'``) weighting is not part of IEC 61672-1 but is included for additional
2024
+ flexibility.
2025
+ """
2026
+
2027
+ #: Time weighting constant, determining the exponential decay rate.
2028
+ #:
2029
+ #: - ``'F'`` (Fast) → 0.125
2030
+ #: - ``'S'`` (Slow) → 1.0
2031
+ #: - ``'I'`` (Impulse) → 0.035 (non-standard)
2032
+ #:
2033
+ #: Default is ``'F'``.
1602
2034
  weight = Map({'F': 0.125, 'S': 1.0, 'I': 0.035}, default_value='F', desc='time weighting')
1603
2035
 
2036
+ #: Filter coefficients in second-order section (SOS) format.
1604
2037
  sos = Property(depends_on=['weight', 'source.digest'])
1605
2038
 
1606
- # internal identifier
2039
+ #: A unique identifier for the filter, based on its properties. (read-only)
1607
2040
  digest = Property(depends_on=['source.digest', 'weight'])
1608
2041
 
1609
2042
  @cached_property
@@ -1612,6 +2045,34 @@ class TimeExpAverage(Filter):
1612
2045
 
1613
2046
  @cached_property
1614
2047
  def _get_sos(self):
2048
+ # Compute the second-order section (SOS) coefficients for the exponential filter.
2049
+ #
2050
+ # The filter follows the form of a first-order IIR filter:
2051
+ #
2052
+ # .. math::
2053
+ # y[n] = \\alpha x[n] + (1 - \\alpha) y[n-1]
2054
+ #
2055
+ # where :math:`\\alpha` is determined by the selected time weighting.
2056
+ #
2057
+ # Returns
2058
+ # -------
2059
+ # :class:`numpy.ndarray`
2060
+ # SOS (second-order section) coefficients representing the filter.
2061
+ #
2062
+ # Notes
2063
+ # -----
2064
+ # The coefficient :math:`\\alpha` is calculated as:
2065
+ #
2066
+ # .. math::
2067
+ # \\alpha = 1 - e^{-1 / (\\tau f_s)}
2068
+ #
2069
+ # where:
2070
+ #
2071
+ # - :math:`\\tau` is the selected time constant (:attr:`weight`).
2072
+ # - :math:`f_s` is the sampling frequency of the source.
2073
+ #
2074
+ # This implementation ensures that the filter adapts dynamically
2075
+ # based on the source's sampling frequency.
1615
2076
  alpha = 1 - exp(-1 / self.weight_ / self.sample_freq)
1616
2077
  a = [1, alpha - 1]
1617
2078
  b = [alpha]
@@ -1619,14 +2080,37 @@ class TimeExpAverage(Filter):
1619
2080
 
1620
2081
 
1621
2082
  class FiltFreqWeight(Filter):
1622
- """Frequency weighting filter according to IEC 61672."""
2083
+ """
2084
+ Apply frequency weighting according to IEC 61672-1.
2085
+
2086
+ This filter implements frequency weighting curves commonly used in sound level meters for noise
2087
+ measurement. It provides A-weighting, C-weighting, and Z-weighting options.
2088
+
2089
+ See Also
2090
+ --------
2091
+ :class:`Filter` : Base class for implementing IIR filters.
1623
2092
 
1624
- # weighting characteristics
2093
+ Notes
2094
+ -----
2095
+ - The filter is designed following IEC 61672-1:2002, the standard for sound level meters.
2096
+ - The weighting curves are implemented using bilinear transformation of analog filter
2097
+ coefficients to the discrete domain.
2098
+ """
2099
+
2100
+ #: Defines the frequency weighting curve:
2101
+ #:
2102
+ #: - ``'A'``: Mimics human hearing sensitivity at low sound levels.
2103
+ #: - ``'C'``: Used for high-level sound measurements with less attenuation at low frequencies.
2104
+ #: - ``'Z'``: A flat response with no frequency weighting.
2105
+ #:
2106
+ #: Default is ``'A'``.
1625
2107
  weight = Enum('A', 'C', 'Z', desc='frequency weighting')
1626
2108
 
2109
+ #: Second-order sections (SOS) representation of the filter coefficients. This property is
2110
+ #: dynamically computed based on :attr:`weight` and the :attr:`Filter.source`'s digest.
1627
2111
  sos = Property(depends_on=['weight', 'source.digest'])
1628
2112
 
1629
- # internal identifier
2113
+ #: A unique identifier for the filter, based on its properties. (read-only)
1630
2114
  digest = Property(depends_on=['source.digest', 'weight'])
1631
2115
 
1632
2116
  @cached_property
@@ -1635,6 +2119,45 @@ class FiltFreqWeight(Filter):
1635
2119
 
1636
2120
  @cached_property
1637
2121
  def _get_sos(self):
2122
+ # Compute the second-order section (SOS) coefficients for the frequency weighting filter.
2123
+ #
2124
+ # The filter design is based on analog weighting functions defined in IEC 61672-1,
2125
+ # transformed into the discrete-time domain using the bilinear transformation.
2126
+ #
2127
+ # Returns
2128
+ # -------
2129
+ # :class:`numpy.ndarray`
2130
+ # SOS (second-order section) coefficients representing the filter.
2131
+ #
2132
+ # Notes
2133
+ # -----
2134
+ # The analog weighting functions are defined as:
2135
+ #
2136
+ # - **A-weighting**:
2137
+ #
2138
+ # .. math::
2139
+ # H(s) = \\frac{(2 \\pi f_4)^2 (s + 2 \\pi f_3) (s + 2 \\pi f_2)}
2140
+ # {(s + 2 \\pi f_4) (s + 2 \\pi f_1) (s^2 + 4 \\pi f_1 s + (2 \\pi f_1)^2)}
2141
+ #
2142
+ # where the parameters are:
2143
+ #
2144
+ # - :math:`f_1 = 20.598997` Hz
2145
+ # - :math:`f_2 = 107.65265` Hz
2146
+ # - :math:`f_3 = 737.86223` Hz
2147
+ # - :math:`f_4 = 12194.217` Hz
2148
+ #
2149
+ # - **C-weighting** follows a similar approach but without the low-frequency roll-off.
2150
+ #
2151
+ # - **Z-weighting** is implemented as a flat response (no filtering).
2152
+ #
2153
+ # The bilinear transformation is used to convert these analog functions into
2154
+ # the digital domain, preserving the frequency response characteristics.
2155
+ #
2156
+ # Raises
2157
+ # ------
2158
+ # :obj:`ValueError`
2159
+ # If an invalid weight type is provided.
2160
+
1638
2161
  # s domain coefficients
1639
2162
  f1 = 20.598997
1640
2163
  f2 = 107.65265
@@ -1657,59 +2180,82 @@ class FiltFreqWeight(Filter):
1657
2180
  return tf2sos(b, a)
1658
2181
 
1659
2182
 
1660
- @deprecated_alias({'numbands': 'num_bands'}, read_only=True)
2183
+ @deprecated_alias({'numbands': 'num_bands'}, read_only=True, removal_version='25.10')
1661
2184
  class FilterBank(TimeOut):
1662
- """Abstract base class for IIR filter banks based on scipy lfilter
1663
- implements a bank of parallel filters.
2185
+ """
2186
+ Abstract base class for IIR filter banks based on :mod:`scipy.signal.lfilter`.
1664
2187
 
1665
- Should not be instantiated by itself.
2188
+ Implements a bank of parallel filters. This class should not be instantiated by itself.
2189
+
2190
+ Inherits from :class:`acoular.base.TimeOut`, and defines the structure for working with filter
2191
+ banks for processing multi-channel time series data, such as time signal signals.
2192
+
2193
+ See Also
2194
+ --------
2195
+ :class:`acoular.base.TimeOut` :
2196
+ ABC for signal processing blocks that interact with data from a source.
2197
+ :class:`acoular.base.SamplesGenerator` :
2198
+ Interface for any generating multi-channel time domain signal processing block.
2199
+ :mod:`scipy.signal` :
2200
+ SciPy module for signal processing.
1666
2201
  """
1667
2202
 
1668
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
2203
+ #: The input data source. It must be an instance of a
2204
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
1669
2205
  source = Instance(SamplesGenerator)
1670
2206
 
1671
- # List of filter coefficients for all filters
2207
+ #: The list containing second order section (SOS) coefficients for the filters in the filter
2208
+ #: bank.
1672
2209
  sos = Property()
1673
2210
 
1674
- # List of labels for bands
2211
+ #: A list of labels describing the different frequency bands of the filter bank.
1675
2212
  bands = Property()
1676
2213
 
1677
- # Number of bands
2214
+ #: The total number of bands in the filter bank.
1678
2215
  num_bands = Property()
1679
2216
 
1680
- # Number of bands
2217
+ #: The total number of output channels resulting from the filter bank operation.
1681
2218
  num_channels = Property()
1682
2219
 
1683
2220
  @abstractmethod
1684
2221
  def _get_sos(self):
1685
- """Returns a list of second order section coefficients."""
2222
+ """Return a list of second order section coefficients."""
1686
2223
 
1687
2224
  @abstractmethod
1688
2225
  def _get_bands(self):
1689
- """Returns a list of labels for the bands."""
2226
+ """Return a list of labels for the bands."""
1690
2227
 
1691
2228
  @abstractmethod
1692
2229
  def _get_num_bands(self):
1693
- """Returns the number of bands."""
2230
+ """Return the number of bands."""
1694
2231
 
1695
2232
  def _get_num_channels(self):
1696
2233
  return self.num_bands * self.source.num_channels
1697
2234
 
1698
2235
  def result(self, num):
1699
- """Python generator that yields the output block-wise.
2236
+ """
2237
+ Yield the bandpass filtered output of the source in blocks of samples.
2238
+
2239
+ This method uses the second order section coefficients (:attr:`sos`) to filter the input
2240
+ samples provided by the source in blocks. The result is returned as a generator.
1700
2241
 
1701
2242
  Parameters
1702
2243
  ----------
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
-
2244
+ num : :obj:`int`
2245
+ Number of samples per block.
2246
+
2247
+ Yields
2248
+ ------
2249
+ :obj:`numpy.ndarray`
2250
+ An array of shape (``num``, :attr:`num_channels`), delivering the filtered
2251
+ samples for each band.
2252
+ The last block may contain fewer samples if the total number of samples is not
2253
+ a multiple of ``num``.
2254
+
2255
+ Notes
2256
+ -----
2257
+ The returned samples are bandpass filtered according to the coefficients in
2258
+ :attr:`sos`. Each block corresponds to the filtered samples for each frequency band.
1713
2259
  """
1714
2260
  numbands = self.num_bands
1715
2261
  snumch = self.source.num_channels
@@ -1723,27 +2269,49 @@ class FilterBank(TimeOut):
1723
2269
 
1724
2270
 
1725
2271
  class OctaveFilterBank(FilterBank):
1726
- """Octave or third-octave filter bank."""
2272
+ """
2273
+ Octave or third-octave filter bank.
2274
+
2275
+ Inherits from :class:`FilterBank` and implements an octave or third-octave filter bank.
2276
+ This class is used for filtering multi-channel time series data, such as time signal signals,
2277
+ using bandpass filters with center frequencies at octave or third-octave intervals.
2278
+
2279
+ See Also
2280
+ --------
2281
+ :class:`FilterBank` :
2282
+ The base class for implementing IIR filter banks.
2283
+ :class:`acoular.base.SamplesGenerator` :
2284
+ Interface for generating multi-channel time domain signal processing blocks.
2285
+ :mod:`scipy.signal` :
2286
+ SciPy module for signal processing.
2287
+ """
1727
2288
 
1728
- # Lowest band center frequency index; defaults to 21 (=125 Hz).
2289
+ #: The lowest band center frequency index. Default is ``21``.
2290
+ #: This index refers to the position in the scale of octave or third-octave bands.
1729
2291
  lband = Int(21, desc='lowest band center frequency index')
1730
2292
 
1731
- # Lowest band center frequency index + 1; defaults to 40 (=8000 Hz).
1732
- hband = Int(40, desc='lowest band center frequency index')
2293
+ #: The highest band center frequency index + 1. Default is ``40``.
2294
+ #: This is the position in the scale of octave or third-octave bands.
2295
+ hband = Int(40, desc='highest band center frequency index + 1')
1733
2296
 
1734
- # Octave fraction: 'Octave' or 'Third octave'; defaults to 'Octave'.
2297
+ #: The fraction of an octave, either ``'Octave'`` or ``'Third octave'``.
2298
+ #: Default is ``'Octave'``.
2299
+ #: Determines the width of the frequency bands. 'Octave' refers to full octaves,
2300
+ #: and ``'Third octave'`` refers to third-octave bands.
1735
2301
  fraction = Map({'Octave': 1, 'Third octave': 3}, default_value='Octave', desc='fraction of octave')
1736
2302
 
1737
- # List of filter coefficients for all filters
2303
+ #: The list of filter coefficients for all filters in the filter bank.
2304
+ #: The coefficients are computed based on the :attr:`lband`, :attr:`hband`,
2305
+ #: and :attr:`fraction` attributes.
1738
2306
  ba = Property(depends_on=['lband', 'hband', 'fraction', 'source.digest'])
1739
2307
 
1740
- # List of labels for bands
2308
+ #: The list of labels describing the frequency bands in the filter bank.
1741
2309
  bands = Property(depends_on=['lband', 'hband', 'fraction'])
1742
2310
 
1743
- # Number of bands
2311
+ #: The total number of bands in the filter bank.
1744
2312
  num_bands = Property(depends_on=['lband', 'hband', 'fraction'])
1745
2313
 
1746
- # internal identifier
2314
+ #: A unique identifier for the filter, based on its properties. (read-only)
1747
2315
  digest = Property(depends_on=['source.digest', 'lband', 'hband', 'fraction', 'order'])
1748
2316
 
1749
2317
  @cached_property
@@ -1760,6 +2328,16 @@ class OctaveFilterBank(FilterBank):
1760
2328
 
1761
2329
  @cached_property
1762
2330
  def _get_sos(self):
2331
+ # Generate and return the second-order section (SOS) coefficients for each filter.
2332
+ #
2333
+ # For each frequency band in the filter bank, the SOS coefficients are calculated using
2334
+ # the :class:`FiltOctave` object with the appropriate `fraction` setting. The coefficients
2335
+ # are then returned as a list.
2336
+ #
2337
+ # Returns
2338
+ # -------
2339
+ # :obj:`list` of :obj:`numpy.ndarray`
2340
+ # A list of SOS coefficients for each filter in the filter bank.
1763
2341
  of = FiltOctave(source=self.source, fraction=self.fraction)
1764
2342
  sos = []
1765
2343
  for i in range(self.lband, self.hband, 4 - self.fraction_):
@@ -1769,26 +2347,47 @@ class OctaveFilterBank(FilterBank):
1769
2347
  return sos
1770
2348
 
1771
2349
 
1772
- @deprecated_alias({'name': 'file'})
2350
+ @deprecated_alias({'name': 'file'}, removal_version='25.10')
1773
2351
  class WriteWAV(TimeOut):
1774
- """Saves time signal from one or more channels as mono/stereo/multi-channel
1775
- `*.wav` file.
2352
+ """
2353
+ Saves time signal from one or more channels as mono, stereo, or multi-channel ``.wav`` file.
2354
+
2355
+ Inherits from :class:`~acoular.base.TimeOut` and allows for exporting time-series data from one
2356
+ or more channels to a WAV file. Supports saving mono, stereo, or multi-channel signals to disk
2357
+ with automatic or user-defined file naming.
2358
+
2359
+ See Also
2360
+ --------
2361
+ :class:`acoular.base.TimeOut` :
2362
+ ABC for signal processing blocks that interact with data from a source.
2363
+ :class:`acoular.base.SamplesGenerator` :
2364
+ Interface for generating multi-channel time domain signal processing blocks.
2365
+ :mod:`wave` :
2366
+ Python module for handling WAV files.
1776
2367
  """
1777
2368
 
1778
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
2369
+ #: The input data source. It must be an instance of a
2370
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
1779
2371
  source = Instance(SamplesGenerator)
1780
2372
 
1781
- # Name of the file to be saved. If none is given, the name will be
1782
- # automatically generated from the sources.
2373
+ #: The name of the file to be saved. If none is given, the name will be automatically
2374
+ #: generated from the source.
1783
2375
  file = File(filter=['*.wav'], desc='name of wave file')
1784
2376
 
1785
- # Basename for cache, readonly.
2377
+ #: The name of the cache file (without extension). It serves as an internal reference for data
2378
+ #: caching and tracking processed files. (automatically generated)
1786
2379
  basename = Property(depends_on=['digest'])
1787
2380
 
1788
- # Channel(s) to save. List can only contain one or two channels.
1789
- channels = List(int, desc='channel to save')
2381
+ #: The list of channels to save. Can only contain one or two channels.
2382
+ channels = List(int, desc='channels to save')
1790
2383
 
1791
- # internal identifier
2384
+ # Bit depth of the output file.
2385
+ encoding = Enum('uint8', 'int16', 'int32', desc='bit depth of the output file')
2386
+
2387
+ # Maximum value to scale the output to. If `None`, the maximum value of the data is used.
2388
+ max_val = Either(None, Float, desc='Maximum value to scale the output to.')
2389
+
2390
+ #: A unique identifier for the filter, based on its properties. (read-only)
1792
2391
  digest = Property(depends_on=['source.digest', 'channels'])
1793
2392
 
1794
2393
  @cached_property
@@ -1800,21 +2399,80 @@ class WriteWAV(TimeOut):
1800
2399
  warn(
1801
2400
  (
1802
2401
  f'The basename attribute of a {self.__class__.__name__} object is deprecated'
1803
- ' and will be removed in a future release!'
2402
+ ' and will be removed in Acoular 26.01!'
1804
2403
  ),
1805
2404
  DeprecationWarning,
1806
2405
  stacklevel=2,
1807
2406
  )
1808
2407
  return find_basename(self.source)
1809
2408
 
1810
- def save(self):
1811
- """Saves source output to one- or multiple-channel `*.wav` file."""
2409
+ def _type_info(self):
2410
+ dtype = np.dtype(self.encoding)
2411
+ info = np.iinfo(dtype)
2412
+ return dtype, info.min, info.max, int(info.bits / 8)
2413
+
2414
+ def _encode(self, data):
2415
+ """Encodes the data according to self.encoding."""
2416
+ dtype, dmin, dmax, _ = self._type_info()
2417
+ if dtype == np.dtype('uint8'):
2418
+ data = (data + 1) / 2 * dmax
2419
+ else:
2420
+ data *= -dmin
2421
+ data = np.round(data)
2422
+ if data.min() < dmin or data.max() > dmax:
2423
+ warn(
2424
+ f'Clipping occurred in WAV export. Data type {dtype} cannot represent all values in data. \
2425
+ Consider raising max_val.',
2426
+ stacklevel=1,
2427
+ )
2428
+ return data.clip(dmin, dmax).astype(dtype).tobytes()
2429
+
2430
+ def result(self, num):
2431
+ """
2432
+ Generate and save time signal data as a WAV file in blocks.
2433
+
2434
+ This generator method retrieves time signal data from the :attr:`source` and writes it to a
2435
+ WAV file in blocks of size ``num``. The data is scaled and encoded according to the selected
2436
+ bit depth and channel configuration. If no file name is specified, a name is generated
2437
+ automatically. The method yields each block of data after it is written to the file,
2438
+ allowing for streaming or real-time processing.
2439
+
2440
+ Parameters
2441
+ ----------
2442
+ num : :class:`int`
2443
+ Number of samples per block to write and yield.
2444
+
2445
+ Yields
2446
+ ------
2447
+ :class:`numpy.ndarray`
2448
+ The block of time signal data that was written to the WAV file, with shape
2449
+ (``num``, number of channels).
2450
+
2451
+ Raises
2452
+ ------
2453
+ :class:`ValueError`
2454
+ If no channels are specified for output.
2455
+ :class:`Warning`
2456
+ If more than two channels are specified, or if the sample frequency is not an integer.
2457
+ Also warns if clipping occurs due to data range limitations.
2458
+
2459
+ See Also
2460
+ --------
2461
+ :meth:`save` : Save the entire source output to a WAV file in one call.
2462
+ """
1812
2463
  nc = len(self.channels)
1813
2464
  if nc == 0:
1814
2465
  msg = 'No channels given for output.'
1815
2466
  raise ValueError(msg)
1816
- if nc > 2:
2467
+ elif nc > 2:
1817
2468
  warn(f'More than two channels given for output, exported file will have {nc:d} channels', stacklevel=1)
2469
+ if self.sample_freq.is_integer():
2470
+ fs = self.sample_freq
2471
+ else:
2472
+ fs = int(round(self.sample_freq))
2473
+ msg = f'Sample frequency {self.sample_freq} is not a whole number. Proceeding with sampling frequency {fs}.'
2474
+ warn(msg, Warning, stacklevel=1)
2475
+ dtype, _, dmax, sw = self._type_info()
1818
2476
  if self.file == '':
1819
2477
  name = self.basename
1820
2478
  for nr in self.channels:
@@ -1822,51 +2480,98 @@ class WriteWAV(TimeOut):
1822
2480
  name += '.wav'
1823
2481
  else:
1824
2482
  name = self.file
2483
+
1825
2484
  with wave.open(name, 'w') as wf:
1826
2485
  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
2486
+ wf.setsampwidth(sw)
2487
+ wf.setframerate(fs)
1831
2488
  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())
2489
+ if self.max_val is None:
2490
+ # compute maximum and remember result to avoid calling source twice
2491
+ if not isinstance(self.source, Cache):
2492
+ self.source = Cache(source=self.source)
2493
+
2494
+ # distinguish cases to use full dynamic range of dtype
2495
+ if dtype == np.dtype('uint8'):
2496
+ mx = 0
2497
+ for data in self.source.result(num):
2498
+ mx = max(abs(data).max(), mx)
2499
+ elif dtype in (np.dtype('int16'), np.dtype('int32')):
2500
+ # for signed integers, we need special treatment because of asymmetry
2501
+ negmax, posmax = 0, 0
2502
+ for data in self.source.result(num):
2503
+ negmax, posmax = max(abs(data.min()), negmax), max(data.max(), posmax)
2504
+ mx = negmax if negmax > posmax else posmax + 1 / dmax # correction for asymmetry
2505
+ else:
2506
+ mx = self.max_val
1837
2507
 
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)
2508
+ # write scaled data to file
2509
+ for data in self.source.result(num):
2510
+ frames = self._encode(data[:, ind] / mx)
2511
+ wf.writeframes(frames)
2512
+ yield data
1842
2513
 
2514
+ def save(self):
2515
+ """
2516
+ Save the entire source output to a WAV file.
1843
2517
 
1844
- @deprecated_alias({'name': 'file', 'numsamples_write': 'num_samples_write', 'writeflag': 'write_flag'})
2518
+ This method writes all available time signal data from the :attr:`source` to the specified
2519
+ WAV file in blocks. It calls the :meth:`result` method internally and discards the yielded
2520
+ data. The file is written according to the current :attr:`channels`, :attr:`encoding`, and
2521
+ scaling settings. If no file name is specified, a name is generated automatically.
2522
+
2523
+ See Also
2524
+ --------
2525
+ :meth:`result` : Generator for writing and yielding data block-wise.
2526
+ """
2527
+ for _ in self.result(1024):
2528
+ pass
2529
+
2530
+
2531
+ @deprecated_alias(
2532
+ {'name': 'file', 'numsamples_write': 'num_samples_write', 'writeflag': 'write_flag'}, removal_version='25.10'
2533
+ )
1845
2534
  class WriteH5(TimeOut):
1846
- """Saves time signal as `*.h5` file."""
2535
+ """
2536
+ Saves time signal data as a ``.h5`` (HDF5) file.
2537
+
2538
+ Inherits from :class:`~acoular.base.TimeOut` and provides functionality for saving multi-channel
2539
+ time-domain signal data to an HDF5 file. The file can be written in blocks and supports
2540
+ metadata storage, precision control, and dynamic file generation based on timestamps.
2541
+
2542
+ See Also
2543
+ --------
2544
+ :class:`acoular.base.TimeOut` :
2545
+ ABC for signal processing blocks interacting with data from a source.
2546
+ :class:`acoular.base.SamplesGenerator` :
2547
+ Interface for generating multi-channel time-domain signal processing blocks.
2548
+ h5py :
2549
+ Python library for reading and writing HDF5 files.
2550
+ """
1847
2551
 
1848
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
2552
+ #: The input data source. It must be an instance of a
2553
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
1849
2554
  source = Instance(SamplesGenerator)
1850
2555
 
1851
- # Name of the file to be saved. If none is given, the name will be
1852
- # automatically generated from a time stamp.
2556
+ #: The name of the file to be saved. If none is given, the name is automatically
2557
+ #: generated based on the current timestamp.
1853
2558
  file = File(filter=['*.h5'], desc='name of data file')
1854
2559
 
1855
- # Number of samples to write to file by `result` method.
1856
- # defaults to -1 (write as long as source yields data).
2560
+ #: The number of samples to write to file per call to `result` method.
2561
+ #: Default is ``-1``, meaning all available data from the source will be written.
1857
2562
  num_samples_write = Int(-1)
1858
2563
 
1859
- # flag that can be raised to stop file writing
2564
+ #: A flag that can be set to stop file writing. Default is ``True``.
1860
2565
  write_flag = Bool(True)
1861
2566
 
1862
- # internal identifier
2567
+ #: A unique identifier for the object, based on its properties. (read-only)
1863
2568
  digest = Property(depends_on=['source.digest'])
1864
2569
 
1865
- # The floating-number-precision of entries of H5 File corresponding
1866
- # to numpy dtypes. Default is 32 bit.
2570
+ #: Precision of the entries in the HDF5 file, represented as numpy data types.
2571
+ #: Default is ``'float32'``.
1867
2572
  precision = Enum('float32', 'float64', desc='precision of H5 File')
1868
2573
 
1869
- # Metadata to be stored in HDF5 file object
2574
+ #: Metadata to be stored in the HDF5 file.
1870
2575
  metadata = Dict(desc='metadata to be stored in .h5 file')
1871
2576
 
1872
2577
  @cached_property
@@ -1874,11 +2579,28 @@ class WriteH5(TimeOut):
1874
2579
  return digest(self)
1875
2580
 
1876
2581
  def create_filename(self):
2582
+ """
2583
+ Generate a filename for the HDF5 file if needed.
2584
+
2585
+ Generate a filename for the HDF5 file based on the current timestamp if no filename is
2586
+ provided. If a filename is provided, it is used as the file name.
2587
+ """
1877
2588
  if self.file == '':
1878
2589
  name = datetime.now(tz=timezone.utc).isoformat('_').replace(':', '-').replace('.', '_')
1879
2590
  self.file = path.join(config.td_dir, name + '.h5')
1880
2591
 
1881
2592
  def get_initialized_file(self):
2593
+ """
2594
+ Initialize the HDF5 file and prepare the necessary datasets and metadata.
2595
+
2596
+ This method creates the file (if it doesn't exist), sets up the main data array,
2597
+ and appends metadata to the file.
2598
+
2599
+ Returns
2600
+ -------
2601
+ :class:`h5py.File`
2602
+ The initialized HDF5 file object ready for data insertion.
2603
+ """
1882
2604
  file = _get_h5file_class()
1883
2605
  self.create_filename()
1884
2606
  f5h = file(self.file, mode='w')
@@ -1889,7 +2611,18 @@ class WriteH5(TimeOut):
1889
2611
  return f5h
1890
2612
 
1891
2613
  def save(self):
1892
- """Saves source output to `*.h5` file."""
2614
+ """
2615
+ Save the source output to a HDF5 file.
2616
+
2617
+ This method writes the processed time-domain signal data from the source to the
2618
+ specified HDF5 file. Data is written in blocks and appended to the extendable
2619
+ ``'time_data'`` array.
2620
+
2621
+ Notes
2622
+ -----
2623
+ - If no file is specified, a file name is automatically generated.
2624
+ - Metadata defined in the :attr:`metadata` attribute is stored in the file.
2625
+ """
1893
2626
  f5h = self.get_initialized_file()
1894
2627
  ac = f5h.get_data_by_reference('time_data')
1895
2628
  for data in self.source.result(4096):
@@ -1897,7 +2630,17 @@ class WriteH5(TimeOut):
1897
2630
  f5h.close()
1898
2631
 
1899
2632
  def add_metadata(self, f5h):
1900
- """Adds metadata to .h5 file."""
2633
+ """
2634
+ Add metadata to the HDF5 file.
2635
+
2636
+ Metadata is stored in a separate 'metadata' group within the HDF5 file. The metadata
2637
+ is stored as arrays with each key-value pair corresponding to a separate array.
2638
+
2639
+ Parameters
2640
+ ----------
2641
+ f5h : :obj:`h5py.File`
2642
+ The HDF5 file object to which metadata will be added.
2643
+ """
1901
2644
  nitems = len(self.metadata.items())
1902
2645
  if nitems > 0:
1903
2646
  f5h.create_new_group('metadata', '/')
@@ -1907,23 +2650,31 @@ class WriteH5(TimeOut):
1907
2650
  f5h.create_array('/metadata', key, value)
1908
2651
 
1909
2652
  def result(self, num):
1910
- """Python generator that saves source output to `*.h5` file and
1911
- yields the source output block-wise.
2653
+ """
2654
+ Python generator that saves source output to an HDF5 file.
1912
2655
 
2656
+ This method processes data from the source in blocks and writes the data to the HDF5 file.
2657
+ It yields the processed blocks while the data is being written.
1913
2658
 
1914
2659
  Parameters
1915
2660
  ----------
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
-
2661
+ num : :obj:`int`
2662
+ Number of samples per block.
2663
+
2664
+ Yields
2665
+ ------
2666
+ :obj:`numpy.ndarray`
2667
+ A numpy array of shape (``num``, :attr:`~acoular.base.SamplesGenerator.num_channels`),
2668
+ where :attr:`~acoular.base.SamplesGenerator.num_channels` is inhereted from the
2669
+ :attr:`source`, delivering the processed time-domain signal data.
2670
+ The last block may contain fewer samples if the total number of samples is not
2671
+ a multiple of ``num``.
2672
+
2673
+ Notes
2674
+ -----
2675
+ - If :attr:`num_samples_write` is set to a value other than ``-1``, only that number of
2676
+ samples will be written to the file.
2677
+ - The data is echoed as it is yielded, after being written to the file.
1927
2678
  """
1928
2679
  self.write_flag = True
1929
2680
  f5h = self.get_initialized_file()
@@ -1951,27 +2702,47 @@ class WriteH5(TimeOut):
1951
2702
 
1952
2703
 
1953
2704
  class TimeConvolve(TimeOut):
1954
- """Fast frequency domain convolution with the Uniformly partitioned overlap-save method (UPOLS).
1955
-
1956
- See :cite:`Wefers2015` for details.
2705
+ """
2706
+ Perform frequency domain convolution with the uniformly partitioned overlap-save (UPOLS) method.
2707
+
2708
+ This class convolves a source signal with a kernel in the frequency domain. It uses the UPOLS
2709
+ method, which efficiently computes convolutions by processing signal blocks and kernel blocks
2710
+ separately in the frequency domain. For detailed theoretical background,
2711
+ refer to :cite:`Wefers2015`.
2712
+
2713
+ Inherits from :class:`~acoular.base.TimeOut`, which allows the class to process signals
2714
+ generated by a source object. The kernel used for convolution can be one-dimensional or
2715
+ two-dimensional, and it can be applied across one or more channels of the source signal.
2716
+
2717
+ See Also
2718
+ --------
2719
+ :class:`acoular.base.TimeOut` :
2720
+ The parent class for signal processing blocks.
2721
+ :class:`acoular.base.SamplesGenerator` :
2722
+ The interface for generating multi-channel time-domain signals.
1957
2723
  """
1958
2724
 
1959
- # Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
2725
+ #: The input data source. It must be an instance of a
2726
+ #: :class:`~acoular.base.SamplesGenerator`-derived class.
1960
2727
  source = Instance(SamplesGenerator)
1961
2728
 
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.
2729
+ #: Convolution kernel in the time domain.
2730
+ #: The second dimension of the kernel array has to be either ``1`` or match
2731
+ #: the :attr:`source`'s :attr:`~acoular.base.SamplesGenerator.num_channels` attribute.
2732
+ #: If only a single kernel is supplied, it is applied to all channels.
1965
2733
  kernel = CArray(dtype=float, desc='Convolution kernel.')
1966
2734
 
2735
+ # Internal block size for partitioning signals into smaller segments during processing.
1967
2736
  _block_size = Int(desc='Block size')
1968
2737
 
2738
+ # Blocks of the convolution kernel in the frequency domain.
2739
+ # Computed using Fast Fourier Transform (FFT).
1969
2740
  _kernel_blocks = Property(
1970
2741
  depends_on=['kernel', '_block_size'],
1971
2742
  desc='Frequency domain Kernel blocks',
1972
2743
  )
1973
2744
 
1974
- # internal identifier
2745
+ #: A unique identifier for the object, based on its properties. (read-only)
1975
2746
  digest = Property(depends_on=['source.digest', 'kernel'])
1976
2747
 
1977
2748
  @cached_property
@@ -1979,7 +2750,16 @@ class TimeConvolve(TimeOut):
1979
2750
  return digest(self)
1980
2751
 
1981
2752
  def _validate_kernel(self):
1982
- # reshape kernel for broadcasting
2753
+ # Validate the dimensions of the convolution kernel.
2754
+ #
2755
+ # Reshapes the kernel to match the required dimensions for broadcasting. Checks if the
2756
+ # kernel is either one-dimensional or two-dimensional, and ensures that the second dimension
2757
+ # matches the number of channels in the source signal.
2758
+ #
2759
+ # Raises
2760
+ # ------
2761
+ # ValueError
2762
+ # If the kernel's shape is invalid or incompatible with the source signal.
1983
2763
  if self.kernel.ndim == 1:
1984
2764
  self.kernel = self.kernel.reshape([-1, 1])
1985
2765
  return
@@ -1995,6 +2775,15 @@ class TimeConvolve(TimeOut):
1995
2775
  # compute the rfft of the kernel blockwise
1996
2776
  @cached_property
1997
2777
  def _get__kernel_blocks(self):
2778
+ # Compute the frequency-domain blocks of the kernel using the FFT.
2779
+ #
2780
+ # This method splits the kernel into blocks and applies the Fast Fourier Transform (FFT)
2781
+ # to each block. The result is used in the convolution process for efficient computation.
2782
+ #
2783
+ # Returns
2784
+ # -------
2785
+ # :class:`numpy.ndarray`
2786
+ # A 3D array of complex values representing the frequency-domain blocks of the kernel.
1998
2787
  [L, N] = self.kernel.shape
1999
2788
  num = self._block_size
2000
2789
  P = int(ceil(L / num))
@@ -2012,20 +2801,30 @@ class TimeConvolve(TimeOut):
2012
2801
  return blocks
2013
2802
 
2014
2803
  def result(self, num=128):
2015
- """Python generator that yields the output block-wise.
2016
- The source output is convolved with the kernel.
2804
+ """
2805
+ Convolve the source signal with the kernel and yield the result in blocks.
2806
+
2807
+ The method generates the convolution of the source signal with the kernel by processing the
2808
+ signal in small blocks, performing the convolution in the frequency domain, and yielding the
2809
+ results block by block.
2017
2810
 
2018
2811
  Parameters
2019
2812
  ----------
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
-
2813
+ num : :obj:`int`, optional
2814
+ Number of samples per block.
2815
+ Default is ``128``.
2816
+
2817
+ Yields
2818
+ ------
2819
+ :obj:`numpy.ndarray`
2820
+ A array of shape (``num``, :attr:`~acoular.base.SamplesGenerator.num_channels`),
2821
+ where :attr:`~acoular.base.SamplesGenerator.num_channels` is inhereted from the
2822
+ :attr:`source`, representing the convolution result in blocks.
2823
+
2824
+ Notes
2825
+ -----
2826
+ - The kernel is first validated and reshaped if necessary.
2827
+ - The convolution is computed efficiently using the FFT in the frequency domain.
2029
2828
  """
2030
2829
  self._validate_kernel()
2031
2830
  # initialize variables
@@ -2097,21 +2896,3 @@ def _spectral_sum(out, fdl, kb): # pragma: no cover
2097
2896
  out[b, n] += fdl[i, b, n] * kb[i, b, n]
2098
2897
 
2099
2898
  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
- )