acoular 25.1__py3-none-any.whl → 25.3__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/sources.py CHANGED
@@ -1,7 +1,8 @@
1
1
  # ------------------------------------------------------------------------------
2
2
  # Copyright (c) Acoular Development Team.
3
3
  # ------------------------------------------------------------------------------
4
- """Measured multichannel data management and simulation of acoustic sources.
4
+ """
5
+ Measured multichannel data management and simulation of acoustic sources.
5
6
 
6
7
  .. autosummary::
7
8
  :toctree: generated/
@@ -18,6 +19,9 @@
18
19
  UncorrelatedNoiseSource
19
20
  SourceMixer
20
21
  PointSourceConvolve
22
+ spherical_hn1
23
+ get_radiation_angles
24
+ get_modes
21
25
  """
22
26
 
23
27
  # imports from other packages
@@ -109,27 +113,118 @@ def _fill_mic_signal_block(out, signal, rm, ind, blocksize, num_channels, up, pr
109
113
 
110
114
 
111
115
  def spherical_hn1(n, z):
112
- """Spherical Hankel Function of the First Kind."""
116
+ r"""
117
+ Compute the spherical Hankel function of the first kind.
118
+
119
+ The spherical Hankel function of the first kind, :math:`h_n^{(1)}(z)`, is defined as
120
+
121
+ .. math:: h_n^{(1)}(z) = j_n(z) + i \cdot y_n(z)
122
+
123
+ with the complex unit :math:`i`, the spherical Bessel function of the first kind as
124
+
125
+ .. math:: j_n(z) = \sqrt{\frac{\pi}{2z}} J_{n + 1/2}(z),
126
+
127
+ and the spherical Bessel function of the second kind as
128
+
129
+ .. math:: y_n(z) = \sqrt{\frac{\pi}{2z}} Y_{n + 1/2}(z),
130
+
131
+ where :math:`Y_n` is the Bessel function of the second kind.
132
+
133
+ Parameters
134
+ ----------
135
+ n : :class:`int`, array_like
136
+ Order of the spherical Hankel function. Must be a non-negative integer.
137
+ z : complex or :class:`float`, array_like
138
+ Argument of the spherical Hankel function. Can be real or complex.
139
+
140
+ Returns
141
+ -------
142
+ complex or :class:`numpy.ndarray`
143
+ Value of the spherical Hankel function of the first kind for the
144
+ given order ``n`` and argument ``z``. If ``z`` is array-like, an array
145
+ of the same shape is returned.
146
+
147
+ See Also
148
+ --------
149
+ :func:`scipy.special.spherical_jn` : Computes the spherical Bessel function of the first kind.
150
+ :func:`scipy.special.spherical_yn` : Computes the spherical Bessel function of the second kind.
151
+
152
+ Notes
153
+ -----
154
+ - The function relies on :func:`scipy.special.spherical_jn` for the spherical Bessel function of
155
+ the first kind and :func:`scipy.special.spherical_yn` for the spherical Bessel function of the
156
+ second kind.
157
+ - The input ``n`` must be a non-negative integer; otherwise, the behavior is undefined.
158
+
159
+ Examples
160
+ --------
161
+ >>> import acoular as ac
162
+ >>>
163
+ >>> ac.sources.spherical_hn1(0, 1.0)
164
+ np.complex128(0.8414709848078965-0.5403023058681398j)
165
+ >>> ac.sources.spherical_hn1(1, [1.0, 2.0])
166
+ array([0.30116868-1.38177329j, 0.43539777-0.350612j ])
167
+ """
113
168
  return spherical_jn(n, z, derivative=False) + 1j * spherical_yn(n, z, derivative=False)
114
169
 
115
170
 
116
171
  def get_radiation_angles(direction, mpos, sourceposition):
117
- """Returns azimuthal and elevation angles between the mics and the source.
172
+ r"""
173
+ Calculate the azimuthal and elevation angles between the microphones and the source.
174
+
175
+ The function computes the azimuth (``azi``) and elevation (``ele``) angles between each
176
+ microphone position and the source position, taking into account the orientation of the
177
+ spherical harmonics provided by the parameter ``direction``.
118
178
 
119
179
  Parameters
120
180
  ----------
121
- direction : array of floats
122
- Spherical Harmonic orientation
123
- mpos : array of floats
124
- x, y, z position of microphones
125
- sourceposition : array of floats
126
- position of the source
181
+ direction : :class:`numpy.ndarray` of shape ``(3,)``
182
+ Unit vector representing the spherical harmonic orientation. It should be a 3-element array
183
+ corresponding to the ``x``, ``y``, and ``z`` components of the direction.
184
+ mpos : :class:`numpy.ndarray` of shape ``(3, N)``
185
+ Microphone positions in a 3D Cartesian coordinate system. The array should have 3 rows (the
186
+ ``x``, ``y`` and ``z`` coordinates) and ``N`` columns (one for each microphone).
187
+ sourceposition : :class:`numpy.ndarray` of shape ``(3,)``
188
+ Position of the source in a 3D Cartesian coordinate system. It should be a 3-element array
189
+ corresponding to the ``x``, ``y``, and ``z`` coordinates of the source.
127
190
 
128
191
  Returns
129
192
  -------
130
- azi, ele : array of floats
131
- the angle between the mics and the source
193
+ azi : :class:`numpy.ndarray` of shape ``(N,)``
194
+ Azimuth angles in radians between the microphones and the source. The range of the values is
195
+ :math:`[0, 2\pi)`.
196
+ ele : :class:`numpy.ndarray` of shape ``(N,)``
197
+ Elevation angles in radians between the microphones and the source. The range of the values
198
+ is :math:`[0, \pi]`.
199
+
200
+ See Also
201
+ --------
202
+ :func:`numpy.linalg.norm` :
203
+ Computes the norm of a vector.
204
+ :func:`numpy.arctan2` :
205
+ Computes the arctangent of two variables, preserving quadrant information.
206
+
207
+ Notes
208
+ -----
209
+ - The function accounts for a coordinate system transformation where the ``z``-axis in Acoular
210
+ corresponds to the ``y``-axis in spherical coordinates, and the ``y``-axis in Acoular
211
+ corresponds to the ``z``-axis in spherical coordinates.
212
+ - The elevation angle (``ele``) is adjusted to the range :math:`[0, \pi]` by adding
213
+ :math:`\pi/2` after the initial calculation.
132
214
 
215
+ Examples
216
+ --------
217
+ >>> import acoular as ac
218
+ >>> import numpy as np
219
+ >>>
220
+ >>> direction = [1, 0, 0]
221
+ >>> mpos = np.array([[1, 2], [0, 0], [0, 1]]) # Two microphones
222
+ >>> sourceposition = [0, 0, 0]
223
+ >>> azi, ele = ac.sources.get_radiation_angles(direction, mpos, sourceposition)
224
+ >>> azi
225
+ array([0. , 5.8195377])
226
+ >>> ele
227
+ array([4.71238898, 4.71238898])
133
228
  """
134
229
  # direction of the Spherical Harmonics
135
230
  direc = array(direction, dtype=float)
@@ -151,24 +246,63 @@ def get_radiation_angles(direction, mpos, sourceposition):
151
246
 
152
247
 
153
248
  def get_modes(lOrder, direction, mpos, sourceposition=None): # noqa: N803
154
- """Returns Spherical Harmonic Radiation Pattern at the Microphones.
249
+ """
250
+ Calculate the spherical harmonic radiation pattern at microphone positions.
251
+
252
+ This function computes the spherical harmonic radiation pattern values at each
253
+ microphone position for a given maximum spherical harmonic order (``lOrder``),
254
+ orientation (``direction``), and optional source position (``sourceposition``).
155
255
 
156
256
  Parameters
157
257
  ----------
158
- lOrder : int
159
- Maximal order of spherical harmonic
160
- direction : array of floats
161
- Spherical Harmonic orientation
162
- mpos : array of floats
163
- x, y, z position of microphones
164
- sourceposition : array of floats
165
- position of the source
258
+ lOrder : :class:`int`
259
+ The maximum order of spherical harmonics to compute. The resulting modes will include all
260
+ orders up to and including ``lOrder``.
261
+ direction : :class:`numpy.ndarray` of shape ``(3,)``
262
+ Unit vector representing the orientation of the spherical harmonics. Should contain the
263
+ ``x``, ``y``, and ``z`` components of the direction.
264
+ mpos : :class:`numpy.ndarray` of shape ``(3, N)``
265
+ Microphone positions in a 3D Cartesian coordinate system. The array should have 3 rows (the
266
+ ``x``, ``y`` and ``z`` coordinates) and ``N`` columns (one for each microphone).
267
+ sourceposition : :class:`numpy.ndarray` of shape ``(3,)``, optional
268
+ Position of the source in a 3D Cartesian coordinate system. If not provided, it defaults to
269
+ the origin ``[0, 0, 0]``.
166
270
 
167
271
  Returns
168
272
  -------
169
- modes : array of floats
170
- the radiation values at each microphone for each mode
273
+ :class:`numpy.ndarray` of shape ``(N, (lOrder+1) ** 2)``
274
+ Complex values representing the spherical harmonic radiation pattern at each microphone
275
+ position (``N`` microphones) for each spherical harmonic mode.
276
+
277
+ See Also
278
+ --------
279
+ :func:`get_radiation_angles` :
280
+ Computes azimuth and elevation angles between microphones and the source.
281
+ :obj:`scipy.special.sph_harm` : Computes spherical harmonic values.
282
+
283
+ Notes
284
+ -----
285
+ - The azimuth (``azi``) and elevation (``ele``) angles between the microphones and the source
286
+ are calculated using the :func:`get_radiation_angles` function.
287
+ - Spherical harmonics (``sph_harm``) are computed for each mode ``(l, m)``, where ``l`` is the
288
+ degree (ranging from ``0`` to ``lOrder``) and ``m`` is the order
289
+ (ranging from ``-l`` to ``+l``).
290
+ - For negative orders (`m < 0`), the conjugate of the spherical harmonic is computed and scaled
291
+ by the imaginary unit ``1j``.
171
292
 
293
+ Examples
294
+ --------
295
+ >>> import acoular as ac
296
+ >>> import numpy as np
297
+ >>>
298
+ >>> lOrder = 2
299
+ >>> direction = [0, 0, 1] # Orientation along z-axis
300
+ >>> mpos = np.array([[1, -1], [1, -1], [0, 0]]) # Two microphones
301
+ >>> sourcepos = [0, 0, 0] # Source at origin
302
+ >>>
303
+ >>> modes = ac.sources.get_modes(lOrder, direction, mpos, sourcepos)
304
+ >>> modes.shape
305
+ (2, 9)
172
306
  """
173
307
  sourceposition = sourceposition if sourceposition is not None else array([0, 0, 0])
174
308
  azi, ele = get_radiation_angles(direction, mpos, sourceposition) # angles between source and mics
@@ -185,11 +319,25 @@ def get_modes(lOrder, direction, mpos, sourceposition=None): # noqa: N803
185
319
 
186
320
  @deprecated_alias({'name': 'file'})
187
321
  class TimeSamples(SamplesGenerator):
188
- """Container for processing time data in `*.h5` or NumPy array format.
322
+ """
323
+ Container for processing time data in ``*.h5`` or NumPy array format.
324
+
325
+ The :class:`TimeSamples` class provides functionality for loading, managing, and accessing
326
+ time-domain data stored in HDF5 files or directly provided as a NumPy array. This data can be
327
+ accessed iteratively through the :meth:`result` method, which returns chunks of the time data
328
+ for further processing.
189
329
 
190
- This class loads measured data from HDF5 files and provides information about this data. It also
191
- serves as an interface where the data can be accessed (e.g. for use in a block chain) via the
192
- :meth:`result` generator.
330
+ See Also
331
+ --------
332
+ :class:`acoular.sources.MaskedTimeSamples` :
333
+ Extends the functionality of class :class:`TimeSamples` by enabling the definition of start
334
+ and stop samples as well as the specification of invalid channels.
335
+
336
+ Notes
337
+ -----
338
+ - If a calibration object is provided, calibrated time-domain data will be returned.
339
+ - Metadata from the :attr:`HDF5 file<file>` can be accessed through the :attr:`metadata`
340
+ attribute.
193
341
 
194
342
  Examples
195
343
  --------
@@ -201,8 +349,8 @@ class TimeSamples(SamplesGenerator):
201
349
  >>> print(f'number of channels: {ts.num_channels}') # doctest: +SKIP
202
350
  number of channels: 56 # doctest: +SKIP
203
351
 
204
- Alternatively, the time data can be specified directly as a numpy array.
205
- In this case, the :attr:`data` and :attr:`sample_freq` attributes must be set manually.
352
+ Alternatively, the time data can be specified directly as a NumPy array. In this case, the
353
+ :attr:`data` and :attr:`~acoular.base.Generator.sample_freq` attributes must be set manually.
206
354
 
207
355
  >>> import numpy as np
208
356
  >>> data = np.random.rand(1000, 4)
@@ -218,42 +366,40 @@ class TimeSamples(SamplesGenerator):
218
366
  ... print(block.shape)
219
367
  (512, 4)
220
368
  (488, 4)
221
-
222
- See Also
223
- --------
224
- acoular.sources.MaskedTimeSamples:
225
- Extends the functionality of class :class:`TimeSamples` by enabling the definition of start
226
- and stop samples as well as the specification of invalid channels.
227
369
  """
228
370
 
229
- #: Full name of the .h5 file with data.
371
+ #: Full path to the ``.h5`` file containing time-domain data.
230
372
  file = File(filter=['*.h5'], exists=True, desc='name of data file')
231
373
 
232
- #: Basename of the .h5 file with data, is set automatically.
374
+ #: Basename of the ``.h5`` file, set automatically from the :attr:`file` attribute.
233
375
  basename = Property(depends_on=['file'], desc='basename of data file')
234
376
 
235
- #: Calibration data, instance of :class:`~acoular.calib.Calib` class, optional .
377
+ #: Calibration data, an instance of the :class:`~acoular.calib.Calib` class.
378
+ #: (optional; if provided, the time data will be calibrated.)
236
379
  calib = Instance(Calib, desc='Calibration data')
237
380
 
238
- #: Number of channels, is set automatically / read from file.
381
+ #: Number of input channels in the time data, set automatically based on the
382
+ #: :attr:`loaded data<file>` or :attr:`specified array<data>`.
239
383
  num_channels = CInt(0, desc='number of input channels')
240
384
 
241
- #: Number of time data samples, is set automatically / read from file.
385
+ #: Total number of time-domain samples, set automatically based on the :attr:`loaded data<file>`
386
+ #: or :attr:`specified array<data>`.
242
387
  num_samples = CInt(0, desc='number of samples')
243
388
 
244
- #: The time data as array of floats with dimension (num_samples, num_channels).
389
+ #: A 2D NumPy array containing the time-domain data, shape (:attr:`num_samples`,
390
+ #: :attr:`num_channels`).
245
391
  data = Any(transient=True, desc='the actual time data array')
246
392
 
247
- #: HDF5 file object
393
+ #: HDF5 file object.
248
394
  h5f = Instance(H5FileBase, transient=True)
249
395
 
250
- #: Provides metadata stored in HDF5 file object
396
+ #: Metadata loaded from the HDF5 file, if available.
251
397
  metadata = Dict(desc='metadata contained in .h5 file')
252
398
 
253
399
  # Checksum over first data entries of all channels
254
400
  _datachecksum = Property()
255
401
 
256
- # internal identifier
402
+ #: A unique identifier for the samples, based on its properties. (read-only)
257
403
  digest = Property(
258
404
  depends_on=['basename', 'calib.digest', '_datachecksum', 'sample_freq', 'num_channels', 'num_samples']
259
405
  )
@@ -271,7 +417,7 @@ class TimeSamples(SamplesGenerator):
271
417
 
272
418
  @on_trait_change('basename')
273
419
  def _load_data(self):
274
- """Open the .h5 file and set attributes."""
420
+ # Open the .h5 file and set attributes.
275
421
  if self.h5f is not None:
276
422
  with contextlib.suppress(OSError):
277
423
  self.h5f.close()
@@ -282,40 +428,72 @@ class TimeSamples(SamplesGenerator):
282
428
 
283
429
  @on_trait_change('data')
284
430
  def _load_shapes(self):
285
- """Set num_channels and num_samples from data."""
431
+ # Set :attr:`num_channels` and :attr:`num_samples` from data.
286
432
  if self.data is not None:
287
433
  self.num_samples, self.num_channels = self.data.shape
288
434
 
289
435
  def _load_timedata(self):
290
- """Loads timedata from .h5 file. Only for internal use."""
436
+ # Loads timedata from :attr:`.h5 file<file>`. Only for internal use.
291
437
  self.data = self.h5f.get_data_by_reference('time_data')
292
438
  self.sample_freq = self.h5f.get_node_attribute(self.data, 'sample_freq')
293
439
 
294
440
  def _load_metadata(self):
295
- """Loads metadata from .h5 file. Only for internal use."""
441
+ # Loads :attr:`metadata` from :attr:`.h5 file<file>`. Only for internal use.
296
442
  self.metadata = {}
297
443
  if '/metadata' in self.h5f:
298
444
  self.metadata = self.h5f.node_to_dict('/metadata')
299
445
 
300
446
  def result(self, num=128):
301
- """Python generator that yields the output block-wise.
447
+ """
448
+ Generate blocks of time-domain data iteratively.
302
449
 
303
- Reads the time data either from a HDF5 file or from a numpy array given
304
- by :attr:`data` and iteratively returns a block of size `num` samples.
305
- Calibrated data is returned if a calibration object is given by :attr:`calib`.
450
+ The :meth:`result` method is a Python generator that yields blocks of time-domain data
451
+ of the specified size. Data is either read from an HDF5 file (if :attr:`file` is set)
452
+ or from a NumPy array (if :attr:`data` is directly provided). If a calibration object
453
+ is specified, the returned data is calibrated.
306
454
 
307
455
  Parameters
308
456
  ----------
309
- num : integer, defaults to 128
310
- This parameter defines the size of the blocks to be yielded
311
- (i.e. the number of samples per block).
457
+ num : :class:`int`, optional
458
+ The size of each block to be yielded, representing the number of time-domain
459
+ samples per block.
312
460
 
313
461
  Yields
314
462
  ------
315
- numpy.ndarray
316
- Samples in blocks of shape (num, num_channels).
317
- The last block may be shorter than num.
463
+ :class:`numpy.ndarray`
464
+ A 2D array of shape (``num``, :attr:`num_channels`) representing a block of
465
+ time-domain data. The last block may have fewer than ``num`` samples if the total number
466
+ of samples is not a multiple of ``num``.
318
467
 
468
+ Raises
469
+ ------
470
+ :obj:`OSError`
471
+ If no samples are available (i.e., :attr:`num_samples` is ``0``).
472
+ :obj:`ValueError`
473
+ If the calibration data does not match the number of channels.
474
+
475
+ Warnings
476
+ --------
477
+ A deprecation warning is raised if the calibration functionality is used directly in
478
+ :class:`TimeSamples`. Instead, the :class:`~acoular.calib.Calib` class should be used as a
479
+ separate processing block.
480
+
481
+ Examples
482
+ --------
483
+ Create a generator and access blocks of data:
484
+
485
+ >>> import numpy as np
486
+ >>> from acoular.sources import TimeSamples
487
+ >>> ts = TimeSamples(data=np.random.rand(1000, 4), sample_freq=51200)
488
+ >>> generator = ts.result(num=256)
489
+ >>> for block in generator:
490
+ ... print(block.shape)
491
+ (256, 4)
492
+ (256, 4)
493
+ (256, 4)
494
+ (232, 4)
495
+
496
+ Note that the last block may have fewer that ``num`` samples.
319
497
  """
320
498
  if self.num_samples == 0:
321
499
  msg = 'no samples available'
@@ -353,12 +531,23 @@ class TimeSamples(SamplesGenerator):
353
531
  read_only=['numchannels', 'numsamples'],
354
532
  )
355
533
  class MaskedTimeSamples(TimeSamples):
356
- """Container for processing time data in `*.h5` or NumPy array format.
534
+ """
535
+ Container to process and manage time-domain data with support for masking samples and channels.
357
536
 
358
- This class loads measured data from HDF5 files and provides information about this data. It
359
- supports storing information about (in)valid samples and (in)valid channels and allows to
360
- specify a start and stop index for the valid samples. It also serves as an interface where the
361
- data can be accessed (e.g. for use in a block chain) via the :meth:`result` generator.
537
+ The :class:`MaskedTimeSamples` class extends the functionality of :class:`TimeSamples` by
538
+ allowing the definition of :attr:`start` and :attr:`stop` indices for valid samples and by
539
+ supporting invalidation of specific channels. This makes it suitable for use cases where only a
540
+ subset of the data is of interest, such as analyzing specific time segments or excluding faulty
541
+ sensor channels.
542
+
543
+ See Also
544
+ --------
545
+ :class:`acoular.sources.TimeSamples` : The parent class for managing unmasked time-domain data.
546
+
547
+ Notes
548
+ -----
549
+ Channels specified in :attr:`invalid_channels` are excluded from processing and not included in
550
+ the generator output.
362
551
 
363
552
  Examples
364
553
  --------
@@ -388,35 +577,38 @@ class MaskedTimeSamples(TimeSamples):
388
577
  (488, 4)
389
578
  """
390
579
 
391
- #: Index of the first sample to be considered valid.
580
+ #: Index of the first sample to be considered valid. Default is ``0``.
392
581
  start = CInt(0, desc='start of valid samples')
393
582
 
394
- #: Index of the last sample to be considered valid.
583
+ #: Index of the last sample to be considered valid. If ``None``, all remaining samples from the
584
+ #: :attr:`start` index onward are considered valid. Default is ``None``.
395
585
  stop = Union(None, CInt, desc='stop of valid samples')
396
586
 
397
- #: Channels that are to be treated as invalid.
587
+ #: List of channel indices to be excluded from processing. Default is ``[]``.
398
588
  invalid_channels = List(int, desc='list of invalid channels')
399
589
 
400
- #: Channel mask to serve as an index for all valid channels, is set automatically.
590
+ #: A mask or index array representing valid channels. Automatically updated based on the
591
+ #: :attr:`invalid_channels` and :attr:`num_channels_total` attributes.
401
592
  channels = Property(depends_on=['invalid_channels', 'num_channels_total'], desc='channel mask')
402
593
 
403
- #: Number of channels (including invalid channels), is set automatically.
594
+ #: Total number of input channels, including invalid channels. (read-only).
404
595
  num_channels_total = CInt(0, desc='total number of input channels')
405
596
 
406
- #: Number of time data samples (including invalid samples), is set automatically.
597
+ #: Total number of samples, including invalid samples. (read-only).
407
598
  num_samples_total = CInt(0, desc='total number of samples per channel')
408
599
 
409
- #: Number of valid channels, is set automatically.
600
+ #: Number of valid input channels after excluding :attr:`invalid_channels`. (read-only)
410
601
  num_channels = Property(
411
602
  depends_on=['invalid_channels', 'num_channels_total'], desc='number of valid input channels'
412
603
  )
413
604
 
414
- #: Number of valid time data samples, is set automatically.
605
+ #: Number of valid time-domain samples, based on :attr:`start` and :attr:`stop` indices.
606
+ #: (read-only)
415
607
  num_samples = Property(
416
608
  depends_on=['start', 'stop', 'num_samples_total'], desc='number of valid samples per channel'
417
609
  )
418
610
 
419
- # internal identifier
611
+ #: A unique identifier for the samples, based on its properties. (read-only)
420
612
  digest = Property(depends_on=['basename', 'start', 'stop', 'calib.digest', 'invalid_channels', '_datachecksum'])
421
613
 
422
614
  @cached_property
@@ -443,8 +635,7 @@ class MaskedTimeSamples(TimeSamples):
443
635
 
444
636
  @on_trait_change('basename')
445
637
  def _load_data(self):
446
- # """ open the .h5 file and set attributes
447
- # """
638
+ # Open the .h5 file and set attributes.
448
639
  if not path.isfile(self.file):
449
640
  # no file there
450
641
  self.sample_freq = 0
@@ -460,35 +651,71 @@ class MaskedTimeSamples(TimeSamples):
460
651
 
461
652
  @on_trait_change('data')
462
653
  def _load_shapes(self):
463
- """Set num_channels and num_samples from data."""
654
+ # Set :attr:`num_channels` and num_samples from :attr:`~acoular.sources.TimeSamples.data`.
464
655
  if self.data is not None:
465
656
  self.num_samples_total, self.num_channels_total = self.data.shape
466
657
 
467
658
  def _load_timedata(self):
468
- """Loads timedata from .h5 file. Only for internal use."""
659
+ # Loads timedata from .h5 file. Only for internal use.
469
660
  self.data = self.h5f.get_data_by_reference('time_data')
470
661
  self.sample_freq = self.h5f.get_node_attribute(self.data, 'sample_freq')
471
662
  (self.num_samples_total, self.num_channels_total) = self.data.shape
472
663
 
473
664
  def result(self, num=128):
474
- """Python generator that yields the output block-wise.
665
+ """
666
+ Generate blocks of valid time-domain data iteratively.
475
667
 
476
- Reads the time data either from a HDF5 file or from a numpy array given
477
- by :attr:`data` and iteratively returns a block of size `num` samples.
478
- Calibrated data is returned if a calibration object is given by :attr:`calib`.
668
+ The :meth:`result` method is a Python generator that yields blocks of valid time-domain data
669
+ based on the specified :attr:`start` and :attr:`stop` indices and the valid channels. Data
670
+ can be calibrated if a calibration object, given by :attr:`calib`, is provided.
479
671
 
480
672
  Parameters
481
673
  ----------
482
- num : integer, defaults to 128
483
- This parameter defines the size of the blocks to be yielded
484
- (i.e. the number of samples per block).
674
+ num : :class:`int`, optional
675
+ The size of each block to be yielded, representing the number of time-domain samples
676
+ per block. Default is ``128``.
485
677
 
486
678
  Yields
487
679
  ------
488
- numpy.ndarray
489
- Samples in blocks of shape (num, num_channels).
490
- The last block may be shorter than num.
680
+ :class:`numpy.ndarray`
681
+ A 2D array of shape (``num``, :attr:`num_channels`) representing a block of valid
682
+ time-domain data. The last block may have fewer than ``num`` samples if the
683
+ :attr:`number of valid samples<num_samples>` is not a multiple of ``num``.
491
684
 
685
+ Raises
686
+ ------
687
+ :obj:`OSError`
688
+ If no valid samples are available (i.e., :attr:`start` and :attr:`stop` indices result
689
+ in an empty range).
690
+ :obj:`ValueError`
691
+ If the :attr:`calibration data<calib>` is incompatible with the
692
+ :attr:`number of valid channels<num_channels>`.
693
+
694
+ Warnings
695
+ --------
696
+ A deprecation warning is raised if the calibration functionality is used directly in
697
+ :class:`MaskedTimeSamples`. Instead, the :class:`acoular.calib.Calib` class should be used
698
+ as a separate processing block.
699
+
700
+ Examples
701
+ --------
702
+ Access valid data in blocks:
703
+
704
+ >>> import numpy as np
705
+ >>> from acoular.sources import MaskedTimeSamples
706
+ >>>
707
+ >>> data = np.random.rand(1000, 4)
708
+ >>> ts = MaskedTimeSamples(data=data, start=100, stop=900)
709
+ >>>
710
+ >>> generator = ts.result(num=256)
711
+ >>> for block in generator:
712
+ ... print(block.shape)
713
+ (256, 4)
714
+ (256, 4)
715
+ (256, 4)
716
+ (32, 4)
717
+
718
+ Note that the last block may have fewer that ``num`` samples.
492
719
  """
493
720
  sli = slice(self.start, self.stop).indices(self.num_samples_total)
494
721
  i = sli[0]
@@ -521,23 +748,80 @@ class MaskedTimeSamples(TimeSamples):
521
748
 
522
749
  @deprecated_alias({'numchannels': 'num_channels', 'numsamples': 'num_samples'}, read_only=True)
523
750
  class PointSource(SamplesGenerator):
524
- """Class to define a fixed point source with an arbitrary signal.
525
- This can be used in simulations.
751
+ """
752
+ Define a fixed point source emitting a signal, intended for simulations.
526
753
 
754
+ The :class:`PointSource` class models a stationary sound source that generates a signal
755
+ detected by microphones. It includes support for specifying the source's location, handling
756
+ signal behaviors for pre-padding, and integrating environmental effects on sound propagation.
527
757
  The output is being generated via the :meth:`result` generator.
758
+
759
+ See Also
760
+ --------
761
+ :class:`acoular.signals.SignalGenerator` : For defining custom emitted signals.
762
+ :class:`acoular.microphones.MicGeom` : For specifying microphone geometries.
763
+ :class:`acoular.environments.Environment` : For modeling sound propagation effects.
764
+
765
+ Notes
766
+ -----
767
+ - The signal is adjusted to account for the distances between the source and microphones.
768
+ - The :attr:`prepadding` attribute allows control over how the signal behaves for time indices
769
+ before :attr:`start_t`.
770
+ - Environmental effects such as sound speed are included through the :attr:`env` attribute.
771
+
772
+ Examples
773
+ --------
774
+ To define a point source emitting a signal at a specific location, we first programmatically set
775
+ a microphone geomertry as in :class:`~acoular.microphones.MicGeom`:
776
+
777
+ >>> import numpy as np
778
+ >>>
779
+ >>> # Generate a (3,3) grid of points in the x-y plane
780
+ >>> x = np.linspace(-1, 1, 3) # Generate 3 points for x, from -1 to 1
781
+ >>> y = np.linspace(-1, 1, 3) # Generate 3 points for y, from -1 to 1
782
+ >>>
783
+ >>> # Create a meshgrid for 3D coordinates, with z=0 for all points
784
+ >>> X, Y = np.meshgrid(x, y)
785
+ >>> Z = np.zeros_like(X) # Set all z-values to 0
786
+ >>>
787
+ >>> # Stack the coordinates into a single (3,9) array
788
+ >>> points = np.vstack([X.ravel(), Y.ravel(), Z.ravel()])
789
+ >>> points
790
+ array([[-1., 0., 1., -1., 0., 1., -1., 0., 1.],
791
+ [-1., -1., -1., 0., 0., 0., 1., 1., 1.],
792
+ [ 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
793
+
794
+ Now, to set the actual point source (``ps``), we define a microphone geomerity (``mg``), using
795
+ the positional data from ``points``, and a sine generator (``sg``) with a total number of 6
796
+ samples.
797
+
798
+ >>> from acoular import PointSource, SineGenerator, MicGeom
799
+ >>> mg = MicGeom(pos_total=points)
800
+ >>> sg = SineGenerator(freq=1000, sample_freq=51200, num_samples=6)
801
+ >>> ps = PointSource(signal=sg, loc=(0.5, 0.5, 1.0), mics=mg)
802
+
803
+ We choose a blocksize of 4 and generate the output signal at the microphones in blocks:
804
+
805
+ >>> for block in ps.result(num=4):
806
+ ... print(block.shape)
807
+ (4, 9)
808
+ (2, 9)
809
+
810
+ The first block has shape (4,9) for 4 samples and 9 microphones. The second block has shape
811
+ (2,9), since of a total of 6 samples only 2 remained.
528
812
  """
529
813
 
530
- #: Emitted signal, instance of the :class:`~acoular.signals.SignalGenerator` class.
814
+ #: Instance of the :class:`~acoular.signals.SignalGenerator` class defining the emitted signal.
531
815
  signal = Instance(SignalGenerator)
532
816
 
533
- #: Location of source in (`x`, `y`, `z`) coordinates (left-oriented system).
817
+ #: Coordinates ``(x, y, z)`` of the source in a left-oriented system. Default is
818
+ #: ``(0.0, 0.0, 1.0)``.
534
819
  loc = Tuple((0.0, 0.0, 1.0), desc='source location')
535
820
 
536
- #: Number of channels in output, is set automatically /
537
- #: depends on used microphone geometry.
821
+ #: Number of output channels, automatically set based on the :attr:`microphone geometry<mics>`.
538
822
  num_channels = Delegate('mics', 'num_mics')
539
823
 
540
- #: :class:`~acoular.microphones.MicGeom` object that provides the microphone locations.
824
+ #: :class:`~acoular.microphones.MicGeom` object defining the positions of the microphones.
541
825
  mics = Instance(MicGeom, desc='microphone geometry')
542
826
 
543
827
  def _validate_locations(self):
@@ -545,35 +829,36 @@ class PointSource(SamplesGenerator):
545
829
  if npany(dist < 1e-7):
546
830
  warn('Source and microphone locations are identical.', Warning, stacklevel=2)
547
831
 
548
- #: :class:`~acoular.environments.Environment` or derived object,
549
- #: which provides information about the sound propagation in the medium.
550
- env = Instance(Environment(), Environment)
832
+ #: An :class:`~acoular.environments.Environment` or derived object providing sound propagation
833
+ #: details, such as :attr:`speed of sound in the medium<acoular.environments.Environment.c>`.
834
+ #: Default is :class:`~acoular.environments.Environment`.
835
+ env = Instance(Environment, args=())
551
836
 
552
- #: Start time of the signal in seconds, defaults to 0 s.
837
+ #: Start time of the signal in seconds. Default is ``0.0``.
553
838
  start_t = Float(0.0, desc='signal start time')
554
839
 
555
- #: Start time of the data acquisition at microphones in seconds,
556
- #: defaults to 0 s.
840
+ #: Start time of data acquisition at the microphones in seconds. Default is ``0.0``.
557
841
  start = Float(0.0, desc='sample start time')
558
842
 
559
- #: Signal behaviour for negative time indices, i.e. if :attr:`start` < :attr:start_t.
560
- #: `loop` take values from the end of :attr:`signal.signal()` array.
561
- #: `zeros` set source signal to zero, advisable for deterministic signals.
562
- #: defaults to `loop`.
843
+ #: Behavior of the signal for negative time indices,
844
+ #: i.e. if (:attr:`start` ``<`` :attr:`start_t`):
845
+ #:
846
+ #: - ``'loop'``: Repeat the :attr:`signal` from its end.
847
+ #: - ``'zeros'``: Use zeros, recommended for deterministic signals.
848
+ #:
849
+ #: Default is ``'loop'``.
563
850
  prepadding = Enum('loop', 'zeros', desc='Behaviour for negative time indices.')
564
851
 
565
- #: Upsampling factor, internal use, defaults to 16.
852
+ #: Internal upsampling factor for finer signal resolution. Default is ``16``.
566
853
  up = Int(16, desc='upsampling factor')
567
854
 
568
- #: Number of samples, is set automatically /
569
- #: depends on :attr:`signal`.
855
+ #: Total number of samples in the emitted signal, derived from the :attr:`signal` generator.
570
856
  num_samples = Delegate('signal')
571
857
 
572
- #: Sampling frequency of the signal, is set automatically /
573
- #: depends on :attr:`signal`.
858
+ #: Sampling frequency of the signal, derived from the :attr:`signal` generator.
574
859
  sample_freq = Delegate('signal')
575
860
 
576
- # internal identifier
861
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
577
862
  digest = Property(
578
863
  depends_on=[
579
864
  'mics.digest',
@@ -592,19 +877,31 @@ class PointSource(SamplesGenerator):
592
877
  return digest(self)
593
878
 
594
879
  def result(self, num=128):
595
- """Python generator that yields the output at microphones block-wise.
880
+ """
881
+ Generate output signal at microphones in blocks, incorporating propagation effects.
882
+
883
+ The :meth:`result` method provides a generator that yields blocks of the signal detected at
884
+ microphones. The signal is adjusted for the distances between the source and microphones, as
885
+ well as any environmental propagation effects.
596
886
 
597
887
  Parameters
598
888
  ----------
599
- num : integer, defaults to 128
600
- This parameter defines the size of the blocks to be yielded
601
- (i.e. the number of samples per block) .
889
+ num : :class:`int`, optional
890
+ Number of samples per block to be yielded. Default is ``128``.
602
891
 
603
- Returns
604
- -------
605
- Samples in blocks of shape (num, num_channels).
606
- The last block may be shorter than num.
892
+ Yields
893
+ ------
894
+ :class:`numpy.ndarray`
895
+ A 2D array of shape (``num``, :attr:`num_channels`) containing the signal detected at
896
+ the microphones. The last block may have fewer samples if :attr:`num_samples` is not a
897
+ multiple of ``num``.
607
898
 
899
+ Raises
900
+ ------
901
+ :obj:`ValueError`
902
+ If the source and a microphone are located at the same position.
903
+ :obj:`RuntimeError`
904
+ If signal processing or propagation cannot be performed.
608
905
  """
609
906
  self._validate_locations()
610
907
  N = int(ceil(self.num_samples / num)) # number of output blocks
@@ -649,23 +946,32 @@ class PointSource(SamplesGenerator):
649
946
 
650
947
 
651
948
  class SphericalHarmonicSource(PointSource):
652
- """Class to define a fixed Spherical Harmonic Source with an arbitrary signal.
653
- This can be used in simulations.
949
+ """
950
+ Define a fixed spherical harmonic source emitting a signal.
951
+
952
+ The :class:`SphericalHarmonicSource` class models a stationary sound source that emits a signal
953
+ with spatial properties represented by spherical harmonics. This source can simulate
954
+ directionality and orientation in sound emission, making it suitable for advanced acoustic
955
+ simulations.
654
956
 
655
957
  The output is being generated via the :meth:`result` generator.
656
958
  """
657
959
 
658
- #: Order of spherical harmonic source
960
+ #: Order of the spherical harmonic representation. Default is ``0``.
659
961
  lOrder = Int(0, desc='Order of spherical harmonic') # noqa: N815
660
962
 
963
+ #: Coefficients of the spherical harmonic modes for the given :attr:`lOrder`.
661
964
  alpha = CArray(desc='coefficients of the (lOrder,) spherical harmonic mode')
662
965
 
663
- #: Vector to define the orientation of the SphericalHarmonic.
966
+ #: Vector defining the orientation of the spherical harmonic source. Default is
967
+ #: ``(1.0, 0.0, 0.0)``.
664
968
  direction = Tuple((1.0, 0.0, 0.0), desc='Spherical Harmonic orientation')
665
969
 
970
+ #: Behavior of the signal for negative time indices. Currently only supports `loop`. Default is
971
+ #: ``'loop'``.
666
972
  prepadding = Enum('loop', desc='Behaviour for negative time indices.')
667
973
 
668
- # internal identifier
974
+ # Unique identifier for the current state of the source, based on its properties. (read-only)
669
975
  digest = Property(
670
976
  depends_on=[
671
977
  'mics.digest',
@@ -686,6 +992,35 @@ class SphericalHarmonicSource(PointSource):
686
992
  return digest(self)
687
993
 
688
994
  def transform(self, signals):
995
+ """
996
+ Apply spherical harmonic transformation to input signals.
997
+
998
+ The :meth:`transform` method modifies the input signals using the spherical harmonic modes,
999
+ taking into account the specified coefficients (:attr:`alpha`), order (:attr:`lOrder`), and
1000
+ source orientation (:attr:`direction`).
1001
+
1002
+ Parameters
1003
+ ----------
1004
+ signals : :class:`numpy.ndarray`
1005
+ Input signal array of shape (:attr:`~PointSouce.num_samples`,
1006
+ :attr:`~PointSouce.num_channels`).
1007
+
1008
+ Returns
1009
+ -------
1010
+ :class:`numpy.ndarray`
1011
+ Transformed signal array of the same shape as ``signals``.
1012
+
1013
+ See Also
1014
+ --------
1015
+ :func:`get_modes` : Method for computing spherical harmonic modes.
1016
+
1017
+ Notes
1018
+ -----
1019
+ - The spherical harmonic modes are computed using the :func:`get_modes` function, which
1020
+ requires the microphone positions, source position, and source orientation.
1021
+ - The transformation applies the spherical harmonic coefficients (:attr:`alpha`) to the
1022
+ signal in the frequency domain.
1023
+ """
689
1024
  Y_lm = get_modes(
690
1025
  lOrder=self.lOrder,
691
1026
  direction=self.direction,
@@ -695,19 +1030,29 @@ class SphericalHarmonicSource(PointSource):
695
1030
  return real(ifft(fft(signals, axis=0) * (Y_lm @ self.alpha), axis=0))
696
1031
 
697
1032
  def result(self, num=128):
698
- """Python generator that yields the output at microphones block-wise.
1033
+ """
1034
+ Generate output signal at microphones in blocks, incorporating propagation effects.
1035
+
1036
+ The :meth:`result` method provides a generator that yields blocks of the signal detected at
1037
+ microphones. The signal is adjusted for the distances between the source and microphones, as
1038
+ well as any environmental propagation effects.
699
1039
 
700
1040
  Parameters
701
1041
  ----------
702
- num : integer, defaults to 128
703
- This parameter defines the size of the blocks to be yielded
704
- (i.e. the number of samples per block) .
1042
+ num : :class:`int`, optional
1043
+ Number of samples per block to be yielded. Default is ``128``.
705
1044
 
706
- Returns
707
- -------
708
- Samples in blocks of shape (num, num_channels).
709
- The last block may be shorter than num.
1045
+ Yields
1046
+ ------
1047
+ :class:`numpy.ndarray`
1048
+ A 2D array of shape (``num``, :attr:`~PointSource.num_channels`) containing the signal
1049
+ detected at the microphones. The last block may have fewer samples if
1050
+ :attr:`~PointSource.num_samples` is not a multiple of ``num``.
710
1051
 
1052
+ Raises
1053
+ ------
1054
+ :obj:`IndexError`
1055
+ If no more samples are available from the signal source.
711
1056
  """
712
1057
  # If signal samples are needed for te < t_start, then samples are taken
713
1058
  # from the end of the calculated signal.
@@ -735,24 +1080,34 @@ class SphericalHarmonicSource(PointSource):
735
1080
 
736
1081
 
737
1082
  class MovingPointSource(PointSource):
738
- """Class to define a point source with an arbitrary
739
- signal moving along a given trajectory.
740
- This can be used in simulations.
1083
+ """
1084
+ Define a moving :class:`point source<PointSource>` emitting a :attr:`~PointSource.signal`.
741
1085
 
742
- The output is being generated via the :meth:`result` generator.
1086
+ The :class:`MovingPointSource` class models a sound source that follows a
1087
+ :attr:`specified trajectory<trajectory>` while emitting a :attr:`~PointSource.signal`.
1088
+ This allows for the simulation of dynamic acoustic scenarios,
1089
+ e.g. sources changing position over time such as vehicles in motion.
1090
+
1091
+ See Also
1092
+ --------
1093
+ :class:`acoular.sources.PointSource` : For modeling stationary point sources.
1094
+ :class:`acoular.trajectory.Trajectory` : For specifying source motion paths.
743
1095
  """
744
1096
 
745
- #: Considering of convective amplification
1097
+ #: Determines whether convective amplification is considered. When ``True``, the amplitude of
1098
+ #: the signal is adjusted based on the relative motion between the source and microphones.
1099
+ #: Default is ``False``.
746
1100
  conv_amp = Bool(False, desc='determines if convective amplification is considered')
747
1101
 
748
- #: Trajectory of the source,
749
- #: instance of the :class:`~acoular.trajectory.Trajectory` class.
750
- #: The start time is assumed to be the same as for the samples.
1102
+ #: Instance of the :class:`~acoular.trajectory.Trajectory` class specifying the source's motion.
1103
+ #: The trajectory defines the source's position and velocity at any given time.
751
1104
  trajectory = Instance(Trajectory, desc='trajectory of the source')
752
1105
 
1106
+ #: Behavior of the signal for negative time indices. Currently only supports ``'loop'``.
1107
+ #: Default is ``'loop'``.
753
1108
  prepadding = Enum('loop', desc='Behaviour for negative time indices.')
754
1109
 
755
- # internal identifier
1110
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
756
1111
  digest = Property(
757
1112
  depends_on=[
758
1113
  'mics.digest',
@@ -772,19 +1127,38 @@ class MovingPointSource(PointSource):
772
1127
  return digest(self)
773
1128
 
774
1129
  def result(self, num=128):
775
- """Python generator that yields the output at microphones block-wise.
1130
+ """
1131
+ Generate the output signal at microphones in blocks, accounting for source motion.
1132
+
1133
+ The :meth:`result` method provides a generator that yields blocks of the signal received at
1134
+ microphones. It incorporates the :attr:`source's trajectory<trajectory>`, convective
1135
+ amplification (if enabled), and environmental propagation effects.
776
1136
 
777
1137
  Parameters
778
1138
  ----------
779
- num : integer, defaults to 128
780
- This parameter defines the size of the blocks to be yielded
781
- (i.e. the number of samples per block).
1139
+ num : :class:`int`, optional
1140
+ Number of samples per block to be yielded. Default is ``128``.
782
1141
 
783
- Returns
784
- -------
785
- Samples in blocks of shape (num, num_channels).
786
- The last block may be shorter than num.
1142
+ Yields
1143
+ ------
1144
+ :class:`numpy.ndarray`
1145
+ A 2D array of shape (``num``, :attr:`~PointSource.num_channels`) containing the signal
1146
+ detected at the microphones. The last block may have fewer samples if
1147
+ :attr:`~PointSource.num_samples` is not a multiple of ``num``.
787
1148
 
1149
+ Raises
1150
+ ------
1151
+ :obj:`IndexError`
1152
+ If no more samples are available from the signal source.
1153
+
1154
+ Notes
1155
+ -----
1156
+ - The method iteratively solves for the emission times of the signal at each microphone
1157
+ using the Newton-Raphson method.
1158
+ - Convective amplification is applied if :attr:`conv_amp` ``= True``, modifying the signal's
1159
+ amplitude based on the relative motion between the source and microphones.
1160
+ - The signal's emission time is calculated relative to the trajectory's position and
1161
+ velocity at each step.
788
1162
  """
789
1163
  # If signal samples are needed for te < t_start, then samples are taken
790
1164
  # from the end of the calculated signal.
@@ -833,24 +1207,42 @@ class MovingPointSource(PointSource):
833
1207
 
834
1208
 
835
1209
  class PointSourceDipole(PointSource):
836
- """Class to define a fixed point source with an arbitrary signal and
837
- dipole characteristics via superposition of two nearby inversely
838
- phased monopoles.
839
- This can be used in simulations.
1210
+ """
1211
+ Define a fixed point source with dipole characteristics.
840
1212
 
841
- The output is being generated via the :meth:`result` generator.
1213
+ The :class:`PointSourceDipole` class simulates a fixed point source with dipole characteristics
1214
+ by superimposing two nearby inversely phased monopoles. This is particularly useful for
1215
+ acoustic simulations where dipole sources are required.
1216
+
1217
+ The generated output is available via the :meth:`result` generator.
1218
+
1219
+ See Also
1220
+ --------
1221
+ :class:`acoular.sources.PointSource` : For modeling stationary point sources.
1222
+
1223
+ Notes
1224
+ -----
1225
+ The dipole's output is calculated as the superposition of two monopoles: one shifted forward and
1226
+ the other backward along the :attr:`direction` vector, with inverse phases. This creates the
1227
+ characteristic dipole radiation pattern.
842
1228
  """
843
1229
 
844
- #: Vector to define the orientation of the dipole lobes. Its magnitude
845
- #: governs the distance between the monopoles
846
- #: (dist = [lowest wavelength in spectrum] x [magnitude] x 1e-5).
847
- #: Note: Use vectors with order of magnitude around 1.0 or less
848
- #: for good results.
1230
+ #: Vector defining the orientation of the dipole lobes and the distance between the inversely
1231
+ #: phased monopoles. The magnitude of the vector determines the monopoles' separation:
1232
+ #:
1233
+ #: - ``distance = [lowest wavelength in spectrum] * [magnitude] * 1e-5``
1234
+ #:
1235
+ #: Use vectors with magnitudes on the order of ``1.0`` or smaller for best results.
1236
+ #: Default is ``(0.0, 0.0, 1.0)`` (z-axis orientation).
1237
+ #:
1238
+ #: **Note:** Use vectors with order of magnitude around ``1.0`` or less for good results.
849
1239
  direction = Tuple((0.0, 0.0, 1.0), desc='dipole orientation and distance of the inversely phased monopoles')
850
1240
 
1241
+ #: Behavior of the signal for negative time indices. Currently only supports ``'loop'``.
1242
+ #: Default is ``'loop'``.
851
1243
  prepadding = Enum('loop', desc='Behaviour for negative time indices.')
852
1244
 
853
- # internal identifier
1245
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
854
1246
  digest = Property(
855
1247
  depends_on=[
856
1248
  'mics.digest',
@@ -870,19 +1262,31 @@ class PointSourceDipole(PointSource):
870
1262
  return digest(self)
871
1263
 
872
1264
  def result(self, num=128):
873
- """Python generator that yields the output at microphones block-wise.
1265
+ """
1266
+ Generate output signal at microphones in blocks.
874
1267
 
875
1268
  Parameters
876
1269
  ----------
877
- num : integer, defaults to 128
878
- This parameter defines the size of the blocks to be yielded
879
- (i.e. the number of samples per block) .
1270
+ num : :class:`int`, optional
1271
+ Number of samples per block to yield. Default is ``128``.
880
1272
 
881
- Returns
882
- -------
883
- Samples in blocks of shape (num, num_channels).
884
- The last block may be shorter than num.
1273
+ Yields
1274
+ ------
1275
+ :class:`numpy.ndarray`
1276
+ A 2D array of shape (``num``, :attr:`~PointSource.num_channels`) containing the signal
1277
+ detected at the microphones. The last block may have fewer samples if
1278
+ :attr:`~PointSource.num_samples` is not a multiple of ``num``.
885
1279
 
1280
+ Raises
1281
+ ------
1282
+ :obj:`IndexError`
1283
+ If no more samples are available from the source.
1284
+
1285
+ Notes
1286
+ -----
1287
+ If samples are needed for times earlier than the source's :attr:`~PointSource.start_t`, the
1288
+ signal is taken from the end of the signal array, effectively looping the signal for
1289
+ negative indices.
886
1290
  """
887
1291
  # If signal samples are needed for te < t_start, then samples are taken
888
1292
  # from the end of the calculated signal.
@@ -947,7 +1351,28 @@ class PointSourceDipole(PointSource):
947
1351
 
948
1352
 
949
1353
  class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
950
- # internal identifier
1354
+ """
1355
+ Define a moving point source with dipole characteristics.
1356
+
1357
+ This class extends the functionalities of :class:`PointSourceDipole` and
1358
+ :class:`MovingPointSource` to simulate a dipole source that moves along a
1359
+ :attr:`defined trajectory<MovingPointSource.trajectory>`. It incorporates both rotational and
1360
+ translational dynamics for the dipole lobes, allowing simulation of complex directional sound
1361
+ sources.
1362
+
1363
+ Key Features:
1364
+ - Combines dipole characteristics with source motion.
1365
+ - Supports rotation of the dipole directivity via the :attr:`rvec` attribute.
1366
+ - Calculates emission times using Newton-Raphson iteration.
1367
+
1368
+ See Also
1369
+ --------
1370
+ :class:`acoular.sources.PointSourceDipole` : For stationary dipole sources.
1371
+ :class:`acoular.sources.MovingPointSource` :
1372
+ For moving point sources without dipole characteristics.
1373
+ """
1374
+
1375
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
951
1376
  digest = Property(
952
1377
  depends_on=[
953
1378
  'mics.digest',
@@ -961,8 +1386,9 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
961
1386
  ],
962
1387
  )
963
1388
 
964
- #: Reference vector, perpendicular to the x and y-axis of moving source.
965
- #: rotation source directivity around this axis
1389
+ #: A reference vector, perpendicular to the x and y-axis of moving source, defining the axis of
1390
+ #: rotation for the dipole directivity. If set to ``(0, 0, 0)``, the dipole is only translated
1391
+ #: along the :attr:`~MovingPointSource.trajectory` without rotation. Default is ``(0, 0, 0)``.
966
1392
  rvec = CArray(dtype=float, shape=(3,), value=array((0, 0, 0)), desc='reference vector')
967
1393
 
968
1394
  @cached_property
@@ -970,6 +1396,41 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
970
1396
  return digest(self)
971
1397
 
972
1398
  def get_emission_time(self, t, direction):
1399
+ """
1400
+ Calculate the emission time and related properties for a moving source.
1401
+
1402
+ Parameters
1403
+ ----------
1404
+ t : :class:`numpy.ndarray`
1405
+ The current receiving time at the microphones.
1406
+ direction : :class:`float` or :class:`numpy.ndarray`
1407
+ Direction vector for the source's dipole directivity.
1408
+
1409
+ Returns
1410
+ -------
1411
+ tuple
1412
+ A tuple containing:
1413
+
1414
+ - te : :class:`numpy.ndarray`
1415
+ Emission times for each microphone.
1416
+ - rm : :class:`numpy.ndarray`
1417
+ Distances from the source to each microphone.
1418
+ - Mr : :class:`numpy.ndarray`
1419
+ Radial Mach numbers for the source's motion.
1420
+ - xs : :class:`numpy.ndarray`
1421
+ Source coordinates at the calculated emission times.
1422
+
1423
+ Warnings
1424
+ --------
1425
+ Ensure that the maximum iteration count (``100``) is sufficient for convergence in all
1426
+ scenarios, especially for high Mach numbers or long trajectories.
1427
+
1428
+ Notes
1429
+ -----
1430
+ The emission times are computed iteratively using the Newton-Raphson method. The iteration
1431
+ terminates when the time discrepancy (``eps``) is below a threshold (``epslim``)
1432
+ or after 100 iterations.
1433
+ """
973
1434
  eps = ones(self.mics.num_mics)
974
1435
  epslim = 0.1 / self.up / self.sample_freq
975
1436
  te = t.copy() # init emission time = receiving time
@@ -990,7 +1451,38 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
990
1451
  return te, rm, Mr, xs
991
1452
 
992
1453
  def get_moving_direction(self, direction, time=0):
993
- """Function that yields the moving coordinates along the trajectory."""
1454
+ """
1455
+ Calculate the moving direction of the dipole source along its trajectory.
1456
+
1457
+ This method computes the updated direction vector for the dipole source, considering both
1458
+ translation along the trajectory and rotation defined by the :attr:`reference vector<rvec>`.
1459
+ If the reference vector is ``(0, 0, 0)``, only translation is applied. Otherwise, the method
1460
+ incorporates rotation into the calculation.
1461
+
1462
+ Parameters
1463
+ ----------
1464
+ direction : :class:`numpy.ndarray`
1465
+ The initial direction vector of the dipole, specified as a 3-element
1466
+ array representing the orientation of the dipole lobes.
1467
+ time : :class:`float`, optional
1468
+ The time at which the trajectory position and velocity are evaluated. Defaults to ``0``.
1469
+
1470
+ Returns
1471
+ -------
1472
+ :class:`numpy.ndarray`
1473
+ The updated direction vector of the dipole source after translation
1474
+ and, if applicable, rotation. The output is a 3-element array.
1475
+
1476
+ Notes
1477
+ -----
1478
+ - The method computes the translation direction vector based on the trajectory's velocity at
1479
+ the specified time.
1480
+ - If the :attr:`reference vector<rvec>` is non-zero, the method constructs a rotation matrix
1481
+ to compute the new dipole direction based on the trajectory's motion and the
1482
+ reference vector.
1483
+ - The rotation matrix ensures that the new dipole orientation adheres
1484
+ to the right-hand rule and remains orthogonal.
1485
+ """
994
1486
  trajg1 = array(self.trajectory.location(time, der=1))[:, 0][:, newaxis]
995
1487
  rflag = (self.rvec == 0).all() # flag translation vs. rotation
996
1488
  if rflag:
@@ -1004,19 +1496,25 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1004
1496
  return cross(newdir[:, 0].T, self.rvec.T).T
1005
1497
 
1006
1498
  def result(self, num=128):
1007
- """Python generator that yields the output at microphones block-wise.
1499
+ """
1500
+ Generate the output signal at microphones in blocks.
1008
1501
 
1009
1502
  Parameters
1010
1503
  ----------
1011
- num : integer, defaults to 128
1012
- This parameter defines the size of the blocks to be yielded
1013
- (i.e. the number of samples per block) .
1014
-
1015
- Returns
1016
- -------
1017
- Samples in blocks of shape (num, num_channels).
1018
- The last block may be shorter than num.
1504
+ num : :class:`int`, optional
1505
+ Number of samples per block to yield. Default is ``128``.
1019
1506
 
1507
+ Yields
1508
+ ------
1509
+ :class:`numpy.ndarray`
1510
+ A 2D array of shape (``num``, :attr:`~PointSource.num_channels`) containing the signal
1511
+ detected at the microphones. The last block may have fewer samples if
1512
+ :attr:`~PointSource.num_samples` is not a multiple of ``num``.
1513
+
1514
+ Notes
1515
+ -----
1516
+ Radial Mach number adjustments are applied if :attr:`~MovingPointSource.conv_amp` is
1517
+ enabled.
1020
1518
  """
1021
1519
  # If signal samples are needed for te < t_start, then samples are taken
1022
1520
  # from the end of the calculated signal.
@@ -1080,28 +1578,46 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1080
1578
 
1081
1579
 
1082
1580
  class LineSource(PointSource):
1083
- """Class to define a fixed Line source with an arbitrary signal.
1084
- This can be used in simulations.
1581
+ """
1582
+ Define a fixed line source with a signal.
1085
1583
 
1086
- The output is being generated via the :meth:`result` generator.
1584
+ The :class:`LineSource` class models a fixed line source composed of multiple monopole sources
1585
+ arranged along a specified direction. Each monopole can have its own source strength, and the
1586
+ coherence between them can be controlled.
1587
+
1588
+ Key Features:
1589
+ - Specify the :attr:`orientation<direction>`, :attr:`length`, and
1590
+ :attr:`number<num_sources>` of monopoles in the line source.
1591
+ - Control the :attr:`source strength<source_strength>` of individual monopoles.
1592
+ - Support for :attr:`coherent or incoherent<coherence>` monopole sources.
1593
+
1594
+ The output signals at microphones are generated block-wise using the :meth:`result` generator.
1595
+
1596
+ See Also
1597
+ --------
1598
+ :class:`acoular.sources.PointSource` : For modeling stationary point sources.
1599
+
1600
+ Notes
1601
+ -----
1602
+ For incoherent sources, a unique seed is set for each monopole to generate independent signals.
1087
1603
  """
1088
1604
 
1089
- #: Vector to define the orientation of the line source
1605
+ #: Vector to define the orientation of the line source. Default is ``(0.0, 0.0, 1.0)``.
1090
1606
  direction = Tuple((0.0, 0.0, 1.0), desc='Line orientation ')
1091
1607
 
1092
- #: Vector to define the length of the line source in m
1608
+ #: Vector to define the length of the line source in meters. Default is ``1.0``.
1093
1609
  length = Float(1, desc='length of the line source')
1094
1610
 
1095
- #: number of monopol sources in the line source
1611
+ #: Number of monopole sources in the line source. Default is ``1``.
1096
1612
  num_sources = Int(1)
1097
1613
 
1098
- #: source strength for every monopole
1614
+ #: Strength coefficients for each monopole source.
1099
1615
  source_strength = CArray(desc='coefficients of the source strength')
1100
1616
 
1101
- #:coherence
1617
+ #: Coherence mode for the monopoles (``'coherent'`` or ``'incoherent'``).
1102
1618
  coherence = Enum('coherent', 'incoherent', desc='coherence mode')
1103
1619
 
1104
- # internal identifier
1620
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
1105
1621
  digest = Property(
1106
1622
  depends_on=[
1107
1623
  'mics.digest',
@@ -1122,19 +1638,20 @@ class LineSource(PointSource):
1122
1638
  return digest(self)
1123
1639
 
1124
1640
  def result(self, num=128):
1125
- """Python generator that yields the output at microphones block-wise.
1641
+ """
1642
+ Generate the output signal at microphones in blocks.
1126
1643
 
1127
1644
  Parameters
1128
1645
  ----------
1129
- num : integer, defaults to 128
1130
- This parameter defines the size of the blocks to be yielded
1131
- (i.e. the number of samples per block) .
1132
-
1133
- Returns
1134
- -------
1135
- Samples in blocks of shape (num, num_channels).
1136
- The last block may be shorter than num.
1646
+ num : :class:`int`, optional
1647
+ Number of samples per block to yield. Default is ``128``.
1137
1648
 
1649
+ Yields
1650
+ ------
1651
+ :class:`numpy.ndarray`
1652
+ A 2D array of shape (``num``, :attr:`~PointSource.num_channels`) containing
1653
+ the signal detected at the microphones. The last block may have fewer samples
1654
+ if :attr:`~PointSource.num_samples` is not a multiple of ``num``.
1138
1655
  """
1139
1656
  # If signal samples are needed for te < t_start, then samples are taken
1140
1657
  # from the end of the calculated signal.
@@ -1191,7 +1708,30 @@ class LineSource(PointSource):
1191
1708
 
1192
1709
 
1193
1710
  class MovingLineSource(LineSource, MovingPointSource):
1194
- # internal identifier
1711
+ """
1712
+ A moving :class:`line source<LineSource>` with an arbitrary signal.
1713
+
1714
+ The :class:`MovingLineSource` class models a :class:`line source<LineSource>` composed of
1715
+ multiple monopoles that move along a :attr:`~MovingPointSource.trajectory`. It supports
1716
+ :attr:`coherent and incoherent<LineSource.coherence>` sources and considers Doppler effects due
1717
+ to motion.
1718
+
1719
+ Key Features:
1720
+ - Specify the :attr:`~MovingPointSource.trajectory` and rotation of the
1721
+ :class:`line source<LineSource>`.
1722
+ - Compute emission times considering motion and source :attr:`~LineSource.direction`.
1723
+ - Generate block-wise microphone output with moving source effects.
1724
+
1725
+ See Also
1726
+ --------
1727
+ :class:`acoular.sources.LineSource` :
1728
+ For :class:`line sources<LineSource>` consisting of
1729
+ :attr:`coherent or incoherent<LineSource.coherence>` monopoles.
1730
+ :class:`acoular.sources.MovingPointSource` :
1731
+ For moving point sources without dipole characteristics.
1732
+ """
1733
+
1734
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
1195
1735
  digest = Property(
1196
1736
  depends_on=[
1197
1737
  'mics.digest',
@@ -1205,8 +1745,10 @@ class MovingLineSource(LineSource, MovingPointSource):
1205
1745
  ],
1206
1746
  )
1207
1747
 
1208
- #: Reference vector, perpendicular to the x and y-axis of moving source.
1209
- #: rotation source directivity around this axis
1748
+ #: A reference vector, perpendicular to the x and y-axis of moving source, defining the axis of
1749
+ #: rotation for the line source directivity. If set to ``(0, 0, 0)``, the line source is only
1750
+ #: translated along the :attr:`~MovingPointSource.trajectory` without rotation. Default is
1751
+ #: ``(0, 0, 0)``.
1210
1752
  rvec = CArray(dtype=float, shape=(3,), value=array((0, 0, 0)), desc='reference vector')
1211
1753
 
1212
1754
  @cached_property
@@ -1214,7 +1756,40 @@ class MovingLineSource(LineSource, MovingPointSource):
1214
1756
  return digest(self)
1215
1757
 
1216
1758
  def get_moving_direction(self, direction, time=0):
1217
- """Function that yields the moving coordinates along the trajectory."""
1759
+ """
1760
+ Calculate the moving direction of the line source along its trajectory.
1761
+
1762
+ This method computes the updated direction vector for the line source,
1763
+ considering both translation along the :attr:`~MovingPointSource.trajectory` and rotation
1764
+ defined by the :attr:`reference vector<rvec>`. If the :attr:`reference vector<rvec>` is
1765
+ `(0, 0, 0)`, only translation is applied. Otherwise, the method incorporates rotation
1766
+ into the calculation.
1767
+
1768
+ Parameters
1769
+ ----------
1770
+ direction : :class:`numpy.ndarray`
1771
+ The initial direction vector of the line source, specified as a
1772
+ 3-element array representing the orientation of the line.
1773
+ time : :class:`float`, optional
1774
+ The time at which the :attr:`~MovingPointSource.trajectory` position and velocity
1775
+ are evaluated. Defaults to ``0``.
1776
+
1777
+ Returns
1778
+ -------
1779
+ :class:`numpy.ndarray`
1780
+ The updated direction vector of the line source after translation and,
1781
+ if applicable, rotation. The output is a 3-element array.
1782
+
1783
+ Notes
1784
+ -----
1785
+ - The method computes the translation direction vector based on the
1786
+ :attr:`~MovingPointSource.trajectory`'s velocity at the specified time.
1787
+ - If the :attr:`reference vector<rvec>` is non-zero, the method constructs a
1788
+ rotation matrix to compute the new line source direction based on the
1789
+ :attr:`~MovingPointSource.trajectory`'s motion and the :attr:`reference vector<rvec>`.
1790
+ - The rotation matrix ensures that the new orientation adheres to the
1791
+ right-hand rule and remains orthogonal.
1792
+ """
1218
1793
  trajg1 = array(self.trajectory.location(time, der=1))[:, 0][:, newaxis]
1219
1794
  rflag = (self.rvec == 0).all() # flag translation vs. rotation
1220
1795
  if rflag:
@@ -1228,6 +1803,47 @@ class MovingLineSource(LineSource, MovingPointSource):
1228
1803
  return cross(newdir[:, 0].T, self.rvec.T).T
1229
1804
 
1230
1805
  def get_emission_time(self, t, direction):
1806
+ """
1807
+ Calculate the emission time for a moving line source based on its trajectory.
1808
+
1809
+ This method computes the time at which sound waves are emitted from the line source
1810
+ at a specific point along its :attr:`~MovingPointSource.trajectory`. It also determines the
1811
+ distances from the source to each microphone and calculates the radial Mach number, which
1812
+ accounts for the Doppler effect due to the motion of the source.
1813
+
1814
+ Parameters
1815
+ ----------
1816
+ t : :class:`float`
1817
+ The current receiving time at the microphones, specified in seconds.
1818
+ direction : :class:`numpy.ndarray`
1819
+ The current direction vector of the line source, specified as a 3-element array
1820
+ representing the orientation of the line.
1821
+
1822
+ Returns
1823
+ -------
1824
+ te : :class:`numpy.ndarray`
1825
+ The computed emission times for each microphone, specified as an array of floats.
1826
+ rm : :class:`numpy.ndarray`
1827
+ The distances from the line source to each microphone, represented as an
1828
+ array of absolute distances.
1829
+ Mr : :class:`numpy.ndarray`
1830
+ The radial Mach number, which accounts for the Doppler effect, calculated for
1831
+ each microphone.
1832
+ xs : :class:`numpy.ndarray`
1833
+ The position of the line source at the computed emission time, returned as
1834
+ a 3-element array.
1835
+
1836
+ Notes
1837
+ -----
1838
+ - This method performs Newton-Raphson iteration to find the emission time where
1839
+ the sound wave from the source reaches the microphones.
1840
+ - The distance between the line source and microphones is computed using
1841
+ Euclidean geometry.
1842
+ - The radial Mach number (``Mr``) is calculated using the velocity of the source
1843
+ and the speed of sound in the medium (:attr:`~acoular.environments.Environment.c`).
1844
+ - The method iterates until the difference between the computed emission time and
1845
+ the current time is sufficiently small (within a defined threshold).
1846
+ """
1231
1847
  eps = ones(self.mics.num_mics)
1232
1848
  epslim = 0.1 / self.up / self.sample_freq
1233
1849
  te = t.copy() # init emission time = receiving time
@@ -1248,19 +1864,20 @@ class MovingLineSource(LineSource, MovingPointSource):
1248
1864
  return te, rm, Mr, xs
1249
1865
 
1250
1866
  def result(self, num=128):
1251
- """Python generator that yields the output at microphones block-wise.
1867
+ """
1868
+ Generate the output signal at microphones in blocks.
1252
1869
 
1253
1870
  Parameters
1254
1871
  ----------
1255
- num : integer, defaults to 128
1256
- This parameter defines the size of the blocks to be yielded
1257
- (i.e. the number of samples per block) .
1258
-
1259
- Returns
1260
- -------
1261
- Samples in blocks of shape (num, num_channels).
1262
- The last block may be shorter than num.
1872
+ num : :class:`int`, optional
1873
+ Number of samples per block to yield. Default is ``128``.
1263
1874
 
1875
+ Yields
1876
+ ------
1877
+ :class:`numpy.ndarray`
1878
+ A 2D array of shape (``num``, :attr:`~PointSource.num_channels`) containing
1879
+ the signal detected at the microphones. The last block may have fewer samples
1880
+ if :attr:`~PointSource.num_samples` is not a multiple of ``num``.
1264
1881
  """
1265
1882
  # If signal samples are needed for te < t_start, then samples are taken
1266
1883
  # from the end of the calculated signal.
@@ -1332,45 +1949,94 @@ class MovingLineSource(LineSource, MovingPointSource):
1332
1949
 
1333
1950
  @deprecated_alias({'numchannels': 'num_channels'}, read_only=True)
1334
1951
  class UncorrelatedNoiseSource(SamplesGenerator):
1335
- """Class to simulate white or pink noise as uncorrelated signal at each
1336
- channel.
1952
+ """
1953
+ Simulate uncorrelated white or pink noise signals at multiple channels.
1337
1954
 
1338
- The output is being generated via the :meth:`result` generator.
1955
+ The :class:`UncorrelatedNoiseSource` class generates noise signals (e.g., white or pink noise)
1956
+ independently at each channel. It supports a user-defined random seed for reproducibility and
1957
+ adapts the number of channels based on the provided microphone geometry. The output is
1958
+ generated block-by-block through the :meth:`result` generator.
1959
+
1960
+ See Also
1961
+ --------
1962
+ :class:`acoular.signals.SignalGenerator` : For defining noise types and properties.
1963
+ :class:`acoular.microphones.MicGeom` : For specifying microphone geometries.
1964
+
1965
+ Notes
1966
+ -----
1967
+ - The type of noise is defined by the :attr:`signal` attribute, which must be an instance of
1968
+ a :class:`~acoular.signals.SignalGenerator`-derived class that supports a ``seed`` parameter.
1969
+ - Each channel generates independent noise, with optional pre-defined random seeds via the
1970
+ :attr:`seed` attribute.
1971
+ - If no seeds are provided, they are generated automatically based on the number of channels
1972
+ and the signal seed.
1973
+
1974
+ Examples
1975
+ --------
1976
+ To simulate uncorrelated white noise at multiple channels:
1977
+
1978
+ >>> from acoular import UncorrelatedNoiseSource, WNoiseGenerator, MicGeom
1979
+ >>> import numpy as np
1980
+ >>>
1981
+ >>> # Define microphone geometry
1982
+ >>> mic_positions = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]]).T # Three microphones
1983
+ >>> mics = MicGeom(pos_total=mic_positions)
1984
+ >>>
1985
+ >>> # Define white noise generator
1986
+ >>> noise_gen = WNoiseGenerator(sample_freq=51200, num_samples=1024, rms=1.0, seed=42)
1987
+ >>>
1988
+ >>> # Create the noise source
1989
+ >>> noise_source = UncorrelatedNoiseSource(signal=noise_gen, mics=mics)
1990
+ >>>
1991
+ >>> # Generate noise output block-by-block
1992
+ >>> for block in noise_source.result(num=256):
1993
+ ... print(block.shape)
1994
+ (256, 3)
1995
+ (256, 3)
1996
+ (256, 3)
1997
+ (256, 3)
1998
+
1999
+ The output blocks contain noise signals for each of the 3 channels. The number of blocks
2000
+ depends on the total number of samples and the block size.
1339
2001
  """
1340
2002
 
1341
- #: Type of noise to generate at the channels.
1342
- #: The `~acoular.signals.SignalGenerator`-derived class has to
1343
- # feature the parameter "seed" (i.e. white or pink noise).
2003
+ #: Instance of a :class:`~acoular.signals.NoiseGenerator`-derived class. For example:
2004
+ #: - :class:`~acoular.signals.WNoiseGenerator` for white noise.
2005
+ #: - :class:`~acoular.signals.PNoiseGenerator` for pink noise.
1344
2006
  signal = Instance(NoiseGenerator, desc='type of noise')
1345
2007
 
1346
- #: Array with seeds for random number generator.
1347
- #: When left empty, arange(:attr:`num_channels`) + :attr:`signal`.seed
1348
- #: will be used.
2008
+ #: Array of random seed values for generating uncorrelated noise at each channel. If left empty,
2009
+ #: seeds will be automatically generated as ``np.arange(self.num_channels) + signal.seed``. The
2010
+ #: size of the array must match the :attr:`number of output channels<num_channels>`.
1349
2011
  seed = CArray(dtype=uint32, desc='random seed values')
1350
2012
 
1351
- #: Number of channels in output; is set automatically /
1352
- #: depends on used microphone geometry.
2013
+ #: Number of output channels, automatically determined by the number of microphones
2014
+ #: defined in the :attr:`mics` attribute. Corresponds to the number of uncorrelated noise
2015
+ #: signals generated.
1353
2016
  num_channels = Delegate('mics', 'num_mics')
1354
2017
 
1355
- #: :class:`~acoular.microphones.MicGeom` object that provides the microphone locations.
2018
+ #: :class:`~acoular.microphones.MicGeom` object specifying the positions of microphones.
2019
+ #: This attribute is used to define the microphone geometry and the
2020
+ #: :attr:`number of channels<num_channels>`.
1356
2021
  mics = Instance(MicGeom, desc='microphone geometry')
1357
2022
 
1358
- #: Start time of the signal in seconds, defaults to 0 s.
2023
+ #: Start time of the generated noise signal in seconds. Determines the time offset for the noise
2024
+ #: output relative to the start of data acquisition. Default is ``0.0``.
1359
2025
  start_t = Float(0.0, desc='signal start time')
1360
2026
 
1361
- #: Start time of the data acquisition at microphones in seconds,
1362
- #: defaults to 0 s.
2027
+ #: Start time of data acquisition at the microphones in seconds. This value determines when the
2028
+ #: generated noise begins relative to the acquisition process. Default is ``0.0``.
1363
2029
  start = Float(0.0, desc='sample start time')
1364
2030
 
1365
- #: Number of samples is set automatically /
1366
- #: depends on :attr:`signal`.
2031
+ #: Total number of samples in the noise signal, derived from the :attr:`signal` generator.
2032
+ #: This value determines the length of the output signal for all channels.
1367
2033
  num_samples = Delegate('signal')
1368
2034
 
1369
- #: Sampling frequency of the signal; is set automatically /
1370
- #: depends on :attr:`signal`.
2035
+ #: Sampling frequency of the generated noise signal in Hz, derived from the :attr:`signal`
2036
+ #: generator. This value defines the temporal resolution of the noise output.
1371
2037
  sample_freq = Delegate('signal')
1372
2038
 
1373
- # internal identifier
2039
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
1374
2040
  digest = Property(
1375
2041
  depends_on=[
1376
2042
  'mics.digest',
@@ -1387,19 +2053,35 @@ class UncorrelatedNoiseSource(SamplesGenerator):
1387
2053
  return digest(self)
1388
2054
 
1389
2055
  def result(self, num=128):
1390
- """Python generator that yields the output at microphones block-wise.
2056
+ """
2057
+ Generate uncorrelated noise signals at microphones in blocks.
2058
+
2059
+ The :meth:`result` method produces a Python generator that yields blocks of noise signals
2060
+ generated independently for each channel. This method supports customizable block sizes and
2061
+ ensures that the last block may have fewer samples if the total number of samples is not an
2062
+ exact multiple of the block size.
1391
2063
 
1392
2064
  Parameters
1393
2065
  ----------
1394
- num : integer, defaults to 128
1395
- This parameter defines the size of the blocks to be yielded
1396
- (i.e. the number of samples per block) .
2066
+ num : :class:`int`, optional
2067
+ Number of samples per block to be yielded. Default is ``128``.
1397
2068
 
1398
- Returns
1399
- -------
1400
- Samples in blocks of shape (num, num_channels).
1401
- The last block may be shorter than num.
2069
+ Yields
2070
+ ------
2071
+ :class:`numpy.ndarray`
2072
+ A 2D array of shape (``num``, :attr:`num_channels`) containing uncorrelated noise
2073
+ signals. The last block may be shorter if the total number of samples is not a
2074
+ multiple of ``num``.
2075
+
2076
+ Raises
2077
+ ------
2078
+ :obj:`ValueError`
2079
+ If the shape of the :attr:`seed` array does not match the number of channels.
1402
2080
 
2081
+ Notes
2082
+ -----
2083
+ - Each channel's noise signal is generated using a unique random seed.
2084
+ - The type and characteristics of the noise are defined by the :attr:`signal` attribute.
1403
2085
  """
1404
2086
  Noise = self.signal.__class__
1405
2087
  # create or get the array of random seeds
@@ -1431,34 +2113,130 @@ class UncorrelatedNoiseSource(SamplesGenerator):
1431
2113
 
1432
2114
  @deprecated_alias({'numchannels': 'num_channels', 'numsamples': 'num_samples'}, read_only=True)
1433
2115
  class SourceMixer(SamplesGenerator):
1434
- """Mixes the signals from several sources."""
2116
+ """
2117
+ Combine signals from multiple sources by mixing their outputs.
2118
+
2119
+ The :class:`SourceMixer` class takes signals generated by multiple
2120
+ :class:`~acoular.base.SamplesGenerator` instances and combines them into
2121
+ a single mixed output. The signals are weighted (if weights are provided)
2122
+ and added block-by-block, supporting efficient streaming.
2123
+
2124
+ See Also
2125
+ --------
2126
+ :class:`acoular.base.SamplesGenerator` : Base class for signal generators.
2127
+
2128
+ Notes
2129
+ -----
2130
+ - All sources must have the same sampling frequency, number of channels,
2131
+ and number of samples for proper mixing.
2132
+ - The weights for the sources can be specified to control their relative
2133
+ contributions to the mixed output. If no weights are provided, all sources
2134
+ are equally weighted.
2135
+
2136
+ Examples
2137
+ --------
2138
+ Mix a stationary point source emitting a sine signal with two pink noise emitting point sources
2139
+ circling it and white noise for each channel:
1435
2140
 
1436
- #: List of :class:`~acoular.base.SamplesGenerator` objects
1437
- #: to be mixed.
2141
+ >>> import numpy as np
2142
+ >>> import acoular as ac
2143
+ >>>
2144
+ >>> # Generate positional microphone data for a 3x3 grid in the x-y plane at z=0
2145
+ >>> mic_positions = []
2146
+ >>> for i in range(3):
2147
+ ... for j in range(3):
2148
+ ... mic_positions.append([i - 1, j - 1, 0]) # Center the grid at the origin
2149
+ >>>
2150
+ >>> # Convert positions to the format required by MicGeom
2151
+ >>> mg = ac.MicGeom(pos_total=np.array(mic_positions).T)
2152
+ >>>
2153
+ >>> # Generate positional data for trajectories of two moving sources
2154
+ >>> # Trajectory 1: Circle in x-y plane at z=1
2155
+ >>> args = 2 * np.pi * np.arange(10) / 10 # Discrete points around the circle
2156
+ >>> x = np.cos(args)
2157
+ >>> y = np.sin(args)
2158
+ >>> z = np.ones_like(x) # Constant height at z=1
2159
+ >>>
2160
+ >>> locs1 = np.array([x, y, z])
2161
+ >>> # Map time indices to positions for Trajectory 1
2162
+ >>> points1 = {time: tuple(pos) for time, pos in enumerate(locs1.T)}
2163
+ >>> tr1 = ac.Trajectory(points=points1)
2164
+ >>>
2165
+ >>> # Trajectory 2: Same circle but with a 180-degree phase shift
2166
+ >>> locs2 = np.roll(locs1, 5, axis=1) # Shift the positions by half the circle
2167
+ >>> # Map time indices to positions for Trajectory 2
2168
+ >>> points2 = {time: tuple(pos) for time, pos in enumerate(locs2.T)}
2169
+ >>> tr2 = ac.Trajectory(points=points2)
2170
+ >>>
2171
+ >>> # Create signal sources
2172
+ >>> # Pink noise sources with different RMS values and random seeds
2173
+ >>> pinkNoise1 = ac.PNoiseGenerator(sample_freq=51200, num_samples=1024, rms=1.0, seed=42)
2174
+ >>> pinkNoise2 = ac.PNoiseGenerator(sample_freq=51200, num_samples=1024, rms=0.5, seed=24)
2175
+ >>>
2176
+ >>> # Moving sources emitting pink noise along their respective trajectories
2177
+ >>> pinkSource1 = ac.MovingPointSource(trajectory=tr1, signal=pinkNoise1, mics=mg)
2178
+ >>> pinkSource2 = ac.MovingPointSource(trajectory=tr2, signal=pinkNoise2, mics=mg)
2179
+ >>>
2180
+ >>> # White noise source generating uncorrelated noise for each microphone channel
2181
+ >>> whiteNoise = ac.WNoiseGenerator(sample_freq=51200, num_samples=1024, rms=1.0, seed=73)
2182
+ >>> whiteSources = ac.UncorrelatedNoiseSource(signal=whiteNoise, mics=mg)
2183
+ >>>
2184
+ >>> # Stationary point source emitting a sine wave
2185
+ >>> sineSignal = ac.SineGenerator(freq=1200, sample_freq=51200, num_samples=1024)
2186
+ >>> sineSource = ac.PointSource(signal=sineSignal, loc=(0, 0, 1), mics=mg)
2187
+ >>>
2188
+ >>> # Combine all sources in a SourceMixer with specified weights
2189
+ >>> sources = [pinkSource1, pinkSource2, whiteSources, sineSource]
2190
+ >>> mixer = ac.SourceMixer(sources=sources, weights=[1.0, 1.0, 0.3, 2.0])
2191
+ >>>
2192
+ >>> # Generate and process the mixed output block by block
2193
+ >>> for block in mixer.result(num=256): # Generate blocks of 256 samples
2194
+ ... print(block.shape)
2195
+ Pink noise filter depth set to maximum possible value of 10.
2196
+ Pink noise filter depth set to maximum possible value of 10.
2197
+ (256, 9)
2198
+ (256, 9)
2199
+ (256, 9)
2200
+ (256, 9)
2201
+
2202
+ The output contains blocks of mixed signals. Each block is a combination of
2203
+ the four signals, weighted according to the provided weights.
2204
+ """
2205
+
2206
+ #: List of :class:`~acoular.base.SamplesGenerator` instances to be mixed.
2207
+ #: Each source provides a signal that will be combined in the output.
2208
+ #: All sources must have the same sampling frequency, number of channels,
2209
+ #: and number of samples. The list must contain at least one source.
1438
2210
  sources = List(Instance(SamplesGenerator, ()))
1439
2211
 
1440
- #: Sampling frequency of the signal.
2212
+ #: Sampling frequency of the mixed signal in Hz. Derived automatically from the
2213
+ #: first source in :attr:`sources`. If no sources are provided, default is ``0``.
1441
2214
  sample_freq = Property(depends_on=['sdigest'])
1442
2215
 
1443
- #: Number of channels.
2216
+ #: Number of channels in the mixed signal. Derived automatically from the
2217
+ #: first source in :attr:`sources`. If no sources are provided, default is ``0``.
1444
2218
  num_channels = Property(depends_on=['sdigest'])
1445
2219
 
1446
- #: Number of samples.
2220
+ #: Total number of samples in the mixed signal. Derived automatically from
2221
+ #: the first source in :attr:`sources`. If no sources are provided, default is ``0``.
1447
2222
  num_samples = Property(depends_on=['sdigest'])
1448
2223
 
1449
- #: Amplitude weight(s) for the sources as array. If not set,
1450
- #: all source signals are equally weighted.
1451
- #: Must match the number of sources in :attr:`sources`.
2224
+ #: Array of amplitude weights for the sources. If not set, all sources are equally weighted.
2225
+ #: The size of the weights array must match the number of sources in :attr:`sources`.
2226
+ #: For example, with two sources, ``weights = [1.0, 0.5]`` would mix the first source at
2227
+ #: full amplitude and the second source at half amplitude.
1452
2228
  weights = CArray(desc='channel weights')
1453
2229
 
1454
- # internal identifier
2230
+ #: Internal identifier for the combined state of all sources, used to track
2231
+ #: changes in the sources for reproducibility and caching.
1455
2232
  sdigest = Str()
1456
2233
 
1457
2234
  @observe('sources.items.digest')
1458
2235
  def _set_sources_digest(self, event): # noqa ARG002
1459
2236
  self.sdigest = ldigest(self.sources)
1460
2237
 
1461
- # internal identifier
2238
+ #: A unique identifier for the current state of the source,
2239
+ #: based on the states of the sources and the weights. (read-only)
1462
2240
  digest = Property(depends_on=['sdigest', 'weights'])
1463
2241
 
1464
2242
  @cached_property
@@ -1478,7 +2256,18 @@ class SourceMixer(SamplesGenerator):
1478
2256
  return self.sources[0].num_samples if self.sources else 0
1479
2257
 
1480
2258
  def validate_sources(self):
1481
- """Validates if sources fit together."""
2259
+ """
2260
+ Ensure that all sources are compatible for mixing.
2261
+
2262
+ This method checks that all sources in :attr:`sources` have the same
2263
+ sampling frequency, number of channels, and number of samples. A
2264
+ :class:`ValueError` is raised if any mismatch is detected.
2265
+
2266
+ Raises
2267
+ ------
2268
+ :obj:`ValueError`
2269
+ If any source has incompatible attributes.
2270
+ """
1482
2271
  if len(self.sources) < 1:
1483
2272
  msg = 'Number of sources in SourceMixer should be at least 1.'
1484
2273
  raise ValueError(msg)
@@ -1494,20 +2283,29 @@ class SourceMixer(SamplesGenerator):
1494
2283
  raise ValueError(msg)
1495
2284
 
1496
2285
  def result(self, num):
1497
- """Python generator that yields the output block-wise.
1498
- The outputs from the sources in the list are being added.
2286
+ """
2287
+ Generate uncorrelated the mixed signal at microphones in blocks.
2288
+
2289
+ The :meth:`result` method combines signals from all sources block-by-block,
2290
+ applying the specified weights to each source. The output blocks contain
2291
+ the mixed signal for all channels.
1499
2292
 
1500
2293
  Parameters
1501
2294
  ----------
1502
- num : integer
1503
- This parameter defines the size of the blocks to be yielded
1504
- (i.e. the number of samples per block).
2295
+ num : :class:`int`
2296
+ Number of samples per block to be yielded.
1505
2297
 
1506
- Returns
1507
- -------
1508
- Samples in blocks of shape (num, num_channels).
1509
- The last block may be shorter than num.
2298
+ Yields
2299
+ ------
2300
+ :class:`numpy.ndarray`
2301
+ A 2D array of shape (``num``, :attr:`num_channels`) containing the mixed
2302
+ signal. The last block may have fewer samples if the total number of samples
2303
+ is not a multiple of ``num``.
1510
2304
 
2305
+ Raises
2306
+ ------
2307
+ :obj:`ValueError`
2308
+ If the sources are not compatible for mixing.
1511
2309
  """
1512
2310
  # check whether all sources fit together
1513
2311
  self.validate_sources()
@@ -1531,54 +2329,117 @@ class SourceMixer(SamplesGenerator):
1531
2329
 
1532
2330
 
1533
2331
  class PointSourceConvolve(PointSource):
1534
- """Class to blockwise convolve an arbitrary source signal with a room impulse response."""
2332
+ """
2333
+ Blockwise convolution of a source signal with an impulse response (IR).
1535
2334
 
1536
- #: Convolution kernel in the time domain. The second dimension of the kernel array
1537
- #: has to be either 1 or match :attr:`~SamplesGenerator.num_channels`.
1538
- #: If only a single kernel is supplied, it is applied to all channels.
1539
- kernel = CArray(dtype=float, desc='Convolution kernel.')
2335
+ The :class:`PointSourceConvolve` class extends :class:`PointSource` to simulate the effects of
2336
+ sound propagation through a room or acoustic environment by convolving the input signal with a
2337
+ specified :attr:`convolution kernel<kernel>` (the IR).
2338
+
2339
+ The convolution is performed block-by-block to allow efficient streaming
2340
+ and processing of large signals.
2341
+
2342
+ See Also
2343
+ --------
2344
+ :class:`PointSource` : Base class for point sources.
2345
+ :class:`acoular.tprocess.TimeConvolve` : Class used for performing time-domain convolution.
2346
+
2347
+ Notes
2348
+ -----
2349
+ - The input :attr:`convolution kernel<kernel>` must be provided as a time-domain array.
2350
+ - The second dimension of :attr:`kernel` must either be ``1`` (a single kernel applied to all
2351
+ channels) or match the :attr:`number of channels<acoular.base.Generator.num_channels>`
2352
+ in the output.
2353
+ - Convolution is performed using the :class:`~acoular.tprocess.TimeConvolve` class.
1540
2354
 
1541
- # ------------- overwrite traits that are not supported by this class -------------
2355
+ Examples
2356
+ --------
2357
+ Convolve a stationary sine wave source with a room impulse response (RIR):
1542
2358
 
1543
- #: Start time of the signal in seconds, defaults to 0 s.
2359
+ >>> import numpy as np
2360
+ >>> import acoular as ac
2361
+ >>>
2362
+ >>> # Define microphone geometry: 4 microphones in a 2x2 grid at z=0
2363
+ >>> mic_positions = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]]).T
2364
+ >>> mg = ac.MicGeom(pos_total=mic_positions)
2365
+ >>>
2366
+ >>> # Generate a sine wave signal
2367
+ >>> sine_signal = ac.SineGenerator(freq=1000, sample_freq=48000, num_samples=1000)
2368
+ >>>
2369
+ >>> # Define an impulse response kernel (example: 100-tap random kernel)
2370
+ >>> kernel = np.random.randn(100, 1) # One kernel for all channels
2371
+ >>>
2372
+ >>> # Create the convolving source
2373
+ >>> convolve_source = PointSourceConvolve(
2374
+ ... signal=sine_signal,
2375
+ ... loc=(0, 0, 1), # Source located at (0, 0, 1)
2376
+ ... kernel=kernel,
2377
+ ... mics=mg,
2378
+ ... )
2379
+ >>>
2380
+ >>> # Generate the convolved signal block by block
2381
+ >>> for block in convolve_source.result(num=256): # 256 samples per block
2382
+ ... print(block.shape)
2383
+ (256, 4)
2384
+ (256, 4)
2385
+ (256, 4)
2386
+ (256, 4)
2387
+ (75, 4)
2388
+
2389
+ The last block has fewer samples.
2390
+ """
2391
+
2392
+ #: Convolution kernel in the time domain.
2393
+ #: The array must either have one column (a single kernel applied to all channels)
2394
+ #: or match the number of output channels in its second dimension.
2395
+ kernel = CArray(dtype=float, desc='Convolution kernel.')
2396
+
2397
+ #: Start time of the signal in seconds. Default is ``0.0``.
1544
2398
  start_t = Enum(0.0, desc='signal start time')
1545
2399
 
1546
- #: Start time of the data acquisition at microphones in seconds,
1547
- #: defaults to 0 s.
2400
+ #: Start time of the data acquisition the the microphones in seconds. Default is ``0.0``.
1548
2401
  start = Enum(0.0, desc='sample start time')
1549
2402
 
1550
- #: Signal behaviour for negative time indices, i.e. if :attr:`start` < :attr:start_t.
1551
- #: `loop` take values from the end of :attr:`signal.signal()` array.
1552
- #: `zeros` set source signal to zero, advisable for deterministic signals.
1553
- #: defaults to `loop`.
1554
- prepadding = Enum(None, desc='Behaviour for negative time indices.')
2403
+ #: Behavior for negative time indices. Default is ``None``.
2404
+ prepadding = Enum(None, desc='Behavior for negative time indices.')
1555
2405
 
1556
- #: Upsampling factor, internal use, defaults to 16.
2406
+ #: Upsampling factor for internal use. Default is ``None``.
1557
2407
  up = Enum(None, desc='upsampling factor')
1558
2408
 
1559
- # internal identifier
1560
- digest = Property(
1561
- depends_on=['mics.digest', 'signal.digest', 'loc', 'kernel'],
1562
- )
2409
+ #: Unique identifier for the current state of the source,
2410
+ #: based on microphone geometry, input signal, source location, and kernel. (read-only)
2411
+ digest = Property(depends_on=['mics.digest', 'signal.digest', 'loc', 'kernel'])
1563
2412
 
1564
2413
  @cached_property
1565
2414
  def _get_digest(self):
1566
2415
  return digest(self)
1567
2416
 
1568
2417
  def result(self, num=128):
1569
- """Python generator that yields the output at microphones block-wise.
2418
+ """
2419
+ Generate the convolved signal at microphones in blocks.
2420
+
2421
+ The :meth:`result` method produces blocks of the output signal
2422
+ by convolving the input signal with the specified kernel. Each block
2423
+ contains the signal for all output channels (microphones).
1570
2424
 
1571
2425
  Parameters
1572
2426
  ----------
1573
- num : integer, defaults to 128
1574
- This parameter defines the size of the blocks to be yielded
1575
- (i.e. the number of samples per block) .
1576
-
1577
- Returns
1578
- -------
1579
- Samples in blocks of shape (num, num_channels).
1580
- The last block may be shorter than num.
2427
+ num : :class:`int`, optional
2428
+ The number of samples per block to yield. Default is ``128``.
1581
2429
 
2430
+ Yields
2431
+ ------
2432
+ :class:`numpy.ndarray`
2433
+ A 2D array of shape (``num``, :attr:`~PointSource.num_channels`) containing
2434
+ the convolved signal for all microphones. The last block may
2435
+ contain fewer samples if the total number of samples is not
2436
+ a multiple of ``num``.
2437
+
2438
+ Notes
2439
+ -----
2440
+ - The input signal is expanded to match the number of microphones, if necessary.
2441
+ - Convolution is performed using the :class:`~acoular.tprocess.TimeConvolve` class
2442
+ to ensure efficiency.
1582
2443
  """
1583
2444
  data = repeat(self.signal.signal()[:, newaxis], self.mics.num_mics, axis=1)
1584
2445
  source = TimeSamples(