acoular 24.10__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
@@ -57,7 +61,7 @@ from traits.api import (
57
61
  Any,
58
62
  Bool,
59
63
  CArray,
60
- CLong,
64
+ CInt,
61
65
  Delegate,
62
66
  Dict,
63
67
  Enum,
@@ -66,11 +70,10 @@ from traits.api import (
66
70
  Instance,
67
71
  Int,
68
72
  List,
69
- ListInt,
70
73
  Property,
71
74
  Str,
72
- Trait,
73
75
  Tuple,
76
+ Union,
74
77
  cached_property,
75
78
  observe,
76
79
  on_trait_change,
@@ -80,20 +83,22 @@ from .base import SamplesGenerator
80
83
 
81
84
  # acoular imports
82
85
  from .calib import Calib
86
+ from .deprecation import deprecated_alias
83
87
  from .environments import Environment
84
88
  from .h5files import H5FileBase, _get_h5file_class
85
89
  from .internal import digest, ldigest
86
90
  from .microphones import MicGeom
87
- from .signals import SignalGenerator
91
+ from .signals import NoiseGenerator, SignalGenerator
92
+ from .tools.utils import get_file_basename
88
93
  from .tprocess import TimeConvolve
89
94
  from .trajectory import Trajectory
90
95
 
91
96
 
92
- @nb.njit(cache=True, error_model='numpy') # jit with nopython
93
- def _fill_mic_signal_block(out, signal, rm, ind, blocksize, numchannels, up, prepadding):
97
+ @nb.njit(cache=True, error_model='numpy') # pragma: no cover
98
+ def _fill_mic_signal_block(out, signal, rm, ind, blocksize, num_channels, up, prepadding):
94
99
  if prepadding:
95
100
  for b in range(blocksize):
96
- for m in range(numchannels):
101
+ for m in range(num_channels):
97
102
  if ind[0, m] < 0:
98
103
  out[b, m] = 0
99
104
  else:
@@ -101,34 +106,125 @@ def _fill_mic_signal_block(out, signal, rm, ind, blocksize, numchannels, up, pre
101
106
  ind += up
102
107
  else:
103
108
  for b in range(blocksize):
104
- for m in range(numchannels):
109
+ for m in range(num_channels):
105
110
  out[b, m] = signal[int(0.5 + ind[0, m])] / rm[0, m]
106
111
  ind += up
107
112
  return out
108
113
 
109
114
 
110
115
  def spherical_hn1(n, z):
111
- """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
+ """
112
168
  return spherical_jn(n, z, derivative=False) + 1j * spherical_yn(n, z, derivative=False)
113
169
 
114
170
 
115
171
  def get_radiation_angles(direction, mpos, sourceposition):
116
- """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``.
117
178
 
118
179
  Parameters
119
180
  ----------
120
- direction : array of floats
121
- Spherical Harmonic orientation
122
- mpos : array of floats
123
- x, y, z position of microphones
124
- sourceposition : array of floats
125
- 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.
126
190
 
127
191
  Returns
128
192
  -------
129
- azi, ele : array of floats
130
- 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]`.
131
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.
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])
132
228
  """
133
229
  # direction of the Spherical Harmonics
134
230
  direc = array(direction, dtype=float)
@@ -150,24 +246,63 @@ def get_radiation_angles(direction, mpos, sourceposition):
150
246
 
151
247
 
152
248
  def get_modes(lOrder, direction, mpos, sourceposition=None): # noqa: N803
153
- """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``).
154
255
 
155
256
  Parameters
156
257
  ----------
157
- lOrder : int
158
- Maximal order of spherical harmonic
159
- direction : array of floats
160
- Spherical Harmonic orientation
161
- mpos : array of floats
162
- x, y, z position of microphones
163
- sourceposition : array of floats
164
- 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]``.
165
270
 
166
271
  Returns
167
272
  -------
168
- modes : array of floats
169
- 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.
170
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``.
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)
171
306
  """
172
307
  sourceposition = sourceposition if sourceposition is not None else array([0, 0, 0])
173
308
  azi, ele = get_radiation_angles(direction, mpos, sourceposition) # angles between source and mics
@@ -182,32 +317,48 @@ def get_modes(lOrder, direction, mpos, sourceposition=None): # noqa: N803
182
317
  return modes
183
318
 
184
319
 
320
+ @deprecated_alias({'name': 'file'})
185
321
  class TimeSamples(SamplesGenerator):
186
- """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.
329
+
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.
187
335
 
188
- This class loads measured data from HDF5 files and provides information about this data.
189
- It also serves as an interface where the data can be accessed (e.g. for use in a block chain) via the
190
- :meth:`result` generator.
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.
191
341
 
192
342
  Examples
193
343
  --------
194
344
  Data can be loaded from a HDF5 file as follows:
195
345
 
196
346
  >>> from acoular import TimeSamples
197
- >>> name = <some_h5_file.h5> # doctest: +SKIP
198
- >>> ts = TimeSamples(name=name) # doctest: +SKIP
199
- >>> print(f'number of channels: {ts.numchannels}') # doctest: +SKIP
347
+ >>> file = <some_h5_file.h5> # doctest: +SKIP
348
+ >>> ts = TimeSamples(file=file) # doctest: +SKIP
349
+ >>> print(f'number of channels: {ts.num_channels}') # doctest: +SKIP
200
350
  number of channels: 56 # doctest: +SKIP
201
351
 
202
- Alternatively, the time data can be specified directly as a numpy array.
203
- 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.
204
354
 
205
355
  >>> import numpy as np
206
356
  >>> data = np.random.rand(1000, 4)
207
357
  >>> ts = TimeSamples(data=data, sample_freq=51200)
208
358
 
209
- Chunks of the time data can be accessed iteratively via the :meth:`result` generator.
210
- The last block will be shorter than the block size if the number of samples is not a multiple of the block size.
359
+ Chunks of the time data can be accessed iteratively via the :meth:`result` generator. The last
360
+ block will be shorter than the block size if the number of samples is not a multiple of the
361
+ block size.
211
362
 
212
363
  >>> blocksize = 512
213
364
  >>> generator = ts.result(num=blocksize)
@@ -215,47 +366,42 @@ class TimeSamples(SamplesGenerator):
215
366
  ... print(block.shape)
216
367
  (512, 4)
217
368
  (488, 4)
218
-
219
- See Also
220
- --------
221
- acoular.sources.MaskedTimeSamples :
222
- Extends the functionality of class :class:`TimeSamples` by enabling the definition of start and stop samples
223
- as well as the specification of invalid channels.
224
369
  """
225
370
 
226
- #: Full name of the .h5 file with data.
227
- name = File(filter=['*.h5'], desc='name of data file')
371
+ #: Full path to the ``.h5`` file containing time-domain data.
372
+ file = File(filter=['*.h5'], exists=True, desc='name of data file')
228
373
 
229
- #: Basename of the .h5 file with data, is set automatically.
230
- basename = Property(
231
- depends_on='name', # filter=['*.h5'],
232
- desc='basename of data file',
233
- )
374
+ #: Basename of the ``.h5`` file, set automatically from the :attr:`file` attribute.
375
+ basename = Property(depends_on=['file'], desc='basename of data file')
234
376
 
235
- #: Calibration data, instance of :class:`~acoular.calib.Calib` class, optional .
236
- calib = Trait(Calib, desc='Calibration data')
377
+ #: Calibration data, an instance of the :class:`~acoular.calib.Calib` class.
378
+ #: (optional; if provided, the time data will be calibrated.)
379
+ calib = Instance(Calib, desc='Calibration data')
237
380
 
238
- #: Number of channels, is set automatically / read from file.
239
- numchannels = CLong(0, desc='number of input channels')
381
+ #: Number of input channels in the time data, set automatically based on the
382
+ #: :attr:`loaded data<file>` or :attr:`specified array<data>`.
383
+ num_channels = CInt(0, desc='number of input channels')
240
384
 
241
- #: Number of time data samples, is set automatically / read from file.
242
- numsamples = CLong(0, desc='number of samples')
385
+ #: Total number of time-domain samples, set automatically based on the :attr:`loaded data<file>`
386
+ #: or :attr:`specified array<data>`.
387
+ num_samples = CInt(0, desc='number of samples')
243
388
 
244
- #: The time data as array of floats with dimension (numsamples, numchannels).
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
- depends_on=['basename', 'calib.digest', '_datachecksum', 'sample_freq', 'numchannels', 'numsamples']
404
+ depends_on=['basename', 'calib.digest', '_datachecksum', 'sample_freq', 'num_channels', 'num_samples']
259
405
  )
260
406
 
261
407
  def _get__datachecksum(self):
@@ -267,95 +413,150 @@ class TimeSamples(SamplesGenerator):
267
413
 
268
414
  @cached_property
269
415
  def _get_basename(self):
270
- return path.splitext(path.basename(self.name))[0]
416
+ return get_file_basename(self.file)
271
417
 
272
418
  @on_trait_change('basename')
273
419
  def _load_data(self):
274
- """Open the .h5 file and set attributes."""
275
- if not path.isfile(self.name):
276
- self.sample_freq = 0
277
- raise OSError('No such file: %s' % self.name)
420
+ # Open the .h5 file and set attributes.
278
421
  if self.h5f is not None:
279
422
  with contextlib.suppress(OSError):
280
423
  self.h5f.close()
281
424
  file = _get_h5file_class()
282
- self.h5f = file(self.name)
425
+ self.h5f = file(self.file)
283
426
  self._load_timedata()
284
427
  self._load_metadata()
285
428
 
286
429
  @on_trait_change('data')
287
430
  def _load_shapes(self):
288
- """Set numchannels and numsamples from data."""
431
+ # Set :attr:`num_channels` and :attr:`num_samples` from data.
289
432
  if self.data is not None:
290
- self.numsamples, self.numchannels = self.data.shape
433
+ self.num_samples, self.num_channels = self.data.shape
291
434
 
292
435
  def _load_timedata(self):
293
- """Loads timedata from .h5 file. Only for internal use."""
436
+ # Loads timedata from :attr:`.h5 file<file>`. Only for internal use.
294
437
  self.data = self.h5f.get_data_by_reference('time_data')
295
438
  self.sample_freq = self.h5f.get_node_attribute(self.data, 'sample_freq')
296
439
 
297
440
  def _load_metadata(self):
298
- """Loads metadata from .h5 file. Only for internal use."""
441
+ # Loads :attr:`metadata` from :attr:`.h5 file<file>`. Only for internal use.
299
442
  self.metadata = {}
300
443
  if '/metadata' in self.h5f:
301
444
  self.metadata = self.h5f.node_to_dict('/metadata')
302
445
 
303
446
  def result(self, num=128):
304
- """Python generator that yields the output block-wise.
447
+ """
448
+ Generate blocks of time-domain data iteratively.
305
449
 
306
- Reads the time data either from a HDF5 file or from a numpy array given
307
- by :attr:`data` and iteratively returns a block of size `num` samples.
308
- 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.
309
454
 
310
455
  Parameters
311
456
  ----------
312
- num : integer, defaults to 128
313
- This parameter defines the size of the blocks to be yielded
314
- (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.
315
460
 
316
461
  Yields
317
462
  ------
318
- numpy.ndarray
319
- Samples in blocks of shape (num, numchannels).
320
- 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``.
321
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.
322
497
  """
323
- if self.numsamples == 0:
498
+ if self.num_samples == 0:
324
499
  msg = 'no samples available'
325
500
  raise OSError(msg)
326
501
  self._datachecksum # trigger checksum calculation # noqa: B018
327
502
  i = 0
328
503
  if self.calib:
329
- if self.calib.num_mics == self.numchannels:
504
+ warn(
505
+ 'The use of the calibration functionality in TimeSamples is deprecated and will be removed in \
506
+ Acoular 25.10. Use the Calib class as an additional processing block instead.',
507
+ DeprecationWarning,
508
+ stacklevel=2,
509
+ )
510
+ if self.calib.num_mics == self.num_channels:
330
511
  cal_factor = self.calib.data[newaxis]
331
512
  else:
332
- raise ValueError('calibration data not compatible: %i, %i' % (self.calib.num_mics, self.numchannels))
333
- while i < self.numsamples:
513
+ msg = f'calibration data not compatible: {self.calib.num_mics:d}, {self.num_channels:d}'
514
+ raise ValueError(msg)
515
+ while i < self.num_samples:
334
516
  yield self.data[i : i + num] * cal_factor
335
517
  i += num
336
518
  else:
337
- while i < self.numsamples:
519
+ while i < self.num_samples:
338
520
  yield self.data[i : i + num]
339
521
  i += num
340
522
 
341
523
 
524
+ @deprecated_alias(
525
+ {
526
+ 'numchannels_total': 'num_channels_total',
527
+ 'numsamples_total': 'num_samples_total',
528
+ 'numchannels': 'num_channels',
529
+ 'numsamples': 'num_samples',
530
+ },
531
+ read_only=['numchannels', 'numsamples'],
532
+ )
342
533
  class MaskedTimeSamples(TimeSamples):
343
- """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.
536
+
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.
344
542
 
345
- This class loads measured data from HDF5 files and provides information about this data.
346
- It supports storing information about (in)valid samples and (in)valid channels and allows
347
- to specify a start and stop index for the valid samples.
348
- It also serves as an interface where the data can be accessed (e.g. for use in a block chain) via the
349
- :meth:`result` generator.
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.
350
551
 
351
552
  Examples
352
553
  --------
353
554
  Data can be loaded from a HDF5 file and invalid channels can be specified as follows:
354
555
 
355
556
  >>> from acoular import MaskedTimeSamples
356
- >>> name = <some_h5_file.h5> # doctest: +SKIP
357
- >>> ts = MaskedTimeSamples(name=name, invalid_channels=[0, 1]) # doctest: +SKIP
358
- >>> print(f'number of valid channels: {ts.numchannels}') # doctest: +SKIP
557
+ >>> file = <some_h5_file.h5> # doctest: +SKIP
558
+ >>> ts = MaskedTimeSamples(file=file, invalid_channels=[0, 1]) # doctest: +SKIP
559
+ >>> print(f'number of valid channels: {ts.num_channels}') # doctest: +SKIP
359
560
  number of valid channels: 54 # doctest: +SKIP
360
561
 
361
562
  Alternatively, the time data can be specified directly as a numpy array.
@@ -368,116 +569,155 @@ class MaskedTimeSamples(TimeSamples):
368
569
 
369
570
  Chunks of the time data can be accessed iteratively via the :meth:`result` generator:
370
571
 
371
- >>> blocksize = 512
372
- >>> generator = ts.result(num=blocksize)
572
+ >>> block_size = 512
573
+ >>> generator = ts.result(num=block_size)
373
574
  >>> for block in generator:
374
575
  ... print(block.shape)
375
576
  (512, 4)
376
577
  (488, 4)
377
578
  """
378
579
 
379
- #: Index of the first sample to be considered valid.
380
- start = CLong(0, desc='start of valid samples')
580
+ #: Index of the first sample to be considered valid. Default is ``0``.
581
+ start = CInt(0, desc='start of valid samples')
381
582
 
382
- #: Index of the last sample to be considered valid.
383
- stop = Trait(None, None, CLong, desc='stop of valid samples')
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``.
585
+ stop = Union(None, CInt, desc='stop of valid samples')
384
586
 
385
- #: Channels that are to be treated as invalid.
386
- invalid_channels = ListInt(desc='list of invalid channels')
587
+ #: List of channel indices to be excluded from processing. Default is ``[]``.
588
+ invalid_channels = List(int, desc='list of invalid channels')
387
589
 
388
- #: Channel mask to serve as an index for all valid channels, is set automatically.
389
- channels = Property(depends_on=['invalid_channels', 'numchannels_total'], desc='channel mask')
590
+ #: A mask or index array representing valid channels. Automatically updated based on the
591
+ #: :attr:`invalid_channels` and :attr:`num_channels_total` attributes.
592
+ channels = Property(depends_on=['invalid_channels', 'num_channels_total'], desc='channel mask')
390
593
 
391
- #: Number of channels (including invalid channels), is set automatically.
392
- numchannels_total = CLong(0, desc='total number of input channels')
594
+ #: Total number of input channels, including invalid channels. (read-only).
595
+ num_channels_total = CInt(0, desc='total number of input channels')
393
596
 
394
- #: Number of time data samples (including invalid samples), is set automatically.
395
- numsamples_total = CLong(0, desc='total number of samples per channel')
597
+ #: Total number of samples, including invalid samples. (read-only).
598
+ num_samples_total = CInt(0, desc='total number of samples per channel')
396
599
 
397
- #: Number of valid channels, is set automatically.
398
- numchannels = Property(depends_on=['invalid_channels', 'numchannels_total'], desc='number of valid input channels')
600
+ #: Number of valid input channels after excluding :attr:`invalid_channels`. (read-only)
601
+ num_channels = Property(
602
+ depends_on=['invalid_channels', 'num_channels_total'], desc='number of valid input channels'
603
+ )
399
604
 
400
- #: Number of valid time data samples, is set automatically.
401
- numsamples = Property(depends_on=['start', 'stop', 'numsamples_total'], desc='number of valid samples per channel')
605
+ #: Number of valid time-domain samples, based on :attr:`start` and :attr:`stop` indices.
606
+ #: (read-only)
607
+ num_samples = Property(
608
+ depends_on=['start', 'stop', 'num_samples_total'], desc='number of valid samples per channel'
609
+ )
402
610
 
403
- # internal identifier
611
+ #: A unique identifier for the samples, based on its properties. (read-only)
404
612
  digest = Property(depends_on=['basename', 'start', 'stop', 'calib.digest', 'invalid_channels', '_datachecksum'])
405
613
 
406
614
  @cached_property
407
615
  def _get_digest(self):
408
616
  return digest(self)
409
617
 
410
- @cached_property
411
- def _get_basename(self):
412
- return path.splitext(path.basename(self.name))[0]
413
-
414
618
  @cached_property
415
619
  def _get_channels(self):
416
620
  if len(self.invalid_channels) == 0:
417
621
  return slice(0, None, None)
418
- allr = [i for i in range(self.numchannels_total) if i not in self.invalid_channels]
622
+ allr = [i for i in range(self.num_channels_total) if i not in self.invalid_channels]
419
623
  return array(allr)
420
624
 
421
625
  @cached_property
422
- def _get_numchannels(self):
626
+ def _get_num_channels(self):
423
627
  if len(self.invalid_channels) == 0:
424
- return self.numchannels_total
628
+ return self.num_channels_total
425
629
  return len(self.channels)
426
630
 
427
631
  @cached_property
428
- def _get_numsamples(self):
429
- sli = slice(self.start, self.stop).indices(self.numsamples_total)
632
+ def _get_num_samples(self):
633
+ sli = slice(self.start, self.stop).indices(self.num_samples_total)
430
634
  return sli[1] - sli[0]
431
635
 
432
636
  @on_trait_change('basename')
433
637
  def _load_data(self):
434
- # """ open the .h5 file and set attributes
435
- # """
436
- if not path.isfile(self.name):
638
+ # Open the .h5 file and set attributes.
639
+ if not path.isfile(self.file):
437
640
  # no file there
438
641
  self.sample_freq = 0
439
- raise OSError('No such file: %s' % self.name)
642
+ msg = f'No such file: {self.file}'
643
+ raise OSError(msg)
440
644
  if self.h5f is not None:
441
645
  with contextlib.suppress(OSError):
442
646
  self.h5f.close()
443
647
  file = _get_h5file_class()
444
- self.h5f = file(self.name)
648
+ self.h5f = file(self.file)
445
649
  self._load_timedata()
446
650
  self._load_metadata()
447
651
 
448
652
  @on_trait_change('data')
449
653
  def _load_shapes(self):
450
- """Set numchannels and numsamples from data."""
654
+ # Set :attr:`num_channels` and num_samples from :attr:`~acoular.sources.TimeSamples.data`.
451
655
  if self.data is not None:
452
- self.numsamples_total, self.numchannels_total = self.data.shape
656
+ self.num_samples_total, self.num_channels_total = self.data.shape
453
657
 
454
658
  def _load_timedata(self):
455
- """Loads timedata from .h5 file. Only for internal use."""
659
+ # Loads timedata from .h5 file. Only for internal use.
456
660
  self.data = self.h5f.get_data_by_reference('time_data')
457
661
  self.sample_freq = self.h5f.get_node_attribute(self.data, 'sample_freq')
458
- (self.numsamples_total, self.numchannels_total) = self.data.shape
662
+ (self.num_samples_total, self.num_channels_total) = self.data.shape
459
663
 
460
664
  def result(self, num=128):
461
- """Python generator that yields the output block-wise.
665
+ """
666
+ Generate blocks of valid time-domain data iteratively.
462
667
 
463
- Reads the time data either from a HDF5 file or from a numpy array given
464
- by :attr:`data` and iteratively returns a block of size `num` samples.
465
- 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.
466
671
 
467
672
  Parameters
468
673
  ----------
469
- num : integer, defaults to 128
470
- This parameter defines the size of the blocks to be yielded
471
- (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``.
472
677
 
473
678
  Yields
474
679
  ------
475
- numpy.ndarray
476
- Samples in blocks of shape (num, numchannels).
477
- 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``.
478
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.
479
719
  """
480
- sli = slice(self.start, self.stop).indices(self.numsamples_total)
720
+ sli = slice(self.start, self.stop).indices(self.num_samples_total)
481
721
  i = sli[0]
482
722
  stop = sli[1]
483
723
  cal_factor = 1.0
@@ -486,105 +726,139 @@ class MaskedTimeSamples(TimeSamples):
486
726
  raise OSError(msg)
487
727
  self._datachecksum # trigger checksum calculation # noqa: B018
488
728
  if self.calib:
489
- if self.calib.num_mics == self.numchannels_total:
729
+ warn(
730
+ 'The use of the calibration functionality in MaskedTimeSamples is deprecated and will be removed in \
731
+ Acoular 25.10. Use the Calib class as an additional processing block instead.',
732
+ DeprecationWarning,
733
+ stacklevel=2,
734
+ )
735
+ if self.calib.num_mics == self.num_channels_total:
490
736
  cal_factor = self.calib.data[self.channels][newaxis]
491
- elif self.calib.num_mics == self.numchannels:
737
+ elif self.calib.num_mics == self.num_channels:
492
738
  cal_factor = self.calib.data[newaxis]
493
739
  elif self.calib.num_mics == 0:
494
740
  warn('No calibration data used.', Warning, stacklevel=2)
495
741
  else:
496
- raise ValueError('calibration data not compatible: %i, %i' % (self.calib.num_mics, self.numchannels))
742
+ msg = f'calibration data not compatible: {self.calib.num_mics:d}, {self.num_channels:d}'
743
+ raise ValueError(msg)
497
744
  while i < stop:
498
745
  yield self.data[i : min(i + num, stop)][:, self.channels] * cal_factor
499
746
  i += num
500
747
 
501
748
 
749
+ @deprecated_alias({'numchannels': 'num_channels', 'numsamples': 'num_samples'}, read_only=True)
502
750
  class PointSource(SamplesGenerator):
503
- """Class to define a fixed point source with an arbitrary signal.
504
- This can be used in simulations.
505
-
506
- The output is being generated via the :meth:`result` generator.
507
751
  """
752
+ Define a fixed point source emitting a signal, intended for simulations.
508
753
 
509
- #: Emitted signal, instance of the :class:`~acoular.signals.SignalGenerator` class.
510
- signal = Trait(SignalGenerator)
511
-
512
- #: Location of source in (`x`, `y`, `z`) coordinates (left-oriented system).
513
- loc = Tuple((0.0, 0.0, 1.0), desc='source location')
514
-
515
- #: Number of channels in output, is set automatically /
516
- #: depends on used microphone geometry.
517
- numchannels = Delegate('mics', 'num_mics')
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.
757
+ The output is being generated via the :meth:`result` generator.
518
758
 
519
- #: :class:`~acoular.microphones.MicGeom` object that provides the microphone locations.
520
- mics = Trait(MicGeom, desc='microphone geometry')
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.
521
764
 
522
- def _validate_locations(self):
523
- dist = self.env._r(array(self.loc).reshape((3, 1)), self.mics.mpos)
524
- if npany(dist < 1e-7):
525
- warn('Source and microphone locations are identical.', Warning, stacklevel=2)
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.
526
771
 
527
- #: :class:`~acoular.environments.Environment` or derived object,
528
- #: which provides information about the sound propagation in the medium.
529
- env = Trait(Environment(), Environment)
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`:
530
776
 
531
- # --- List of backwards compatibility traits and their setters/getters -----------
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)
532
809
 
533
- # Microphone locations.
534
- # Deprecated! Use :attr:`mics` trait instead.
535
- mpos = Property()
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.
812
+ """
536
813
 
537
- def _get_mpos(self):
538
- return self.mics
814
+ #: Instance of the :class:`~acoular.signals.SignalGenerator` class defining the emitted signal.
815
+ signal = Instance(SignalGenerator)
539
816
 
540
- def _set_mpos(self, mpos):
541
- msg = (
542
- "Deprecated use of 'mpos' trait. Use 'mics' trait instead."
543
- "The 'mpos' trait will be removed in version 25.01."
544
- )
545
- warn(msg, DeprecationWarning, stacklevel=2)
546
- self.mics = mpos
817
+ #: Coordinates ``(x, y, z)`` of the source in a left-oriented system. Default is
818
+ #: ``(0.0, 0.0, 1.0)``.
819
+ loc = Tuple((0.0, 0.0, 1.0), desc='source location')
547
820
 
548
- # The speed of sound.
549
- # Deprecated! Only kept for backwards compatibility.
550
- # Now governed by :attr:`env` trait.
551
- c = Property()
821
+ #: Number of output channels, automatically set based on the :attr:`microphone geometry<mics>`.
822
+ num_channels = Delegate('mics', 'num_mics')
552
823
 
553
- def _get_c(self):
554
- return self.env.c
824
+ #: :class:`~acoular.microphones.MicGeom` object defining the positions of the microphones.
825
+ mics = Instance(MicGeom, desc='microphone geometry')
555
826
 
556
- def _set_c(self, c):
557
- msg = "Deprecated use of 'c' trait. Use 'env' trait instead." "The 'c' trait will be removed in version 25.01."
558
- warn(msg, DeprecationWarning, stacklevel=2)
559
- self.env.c = c
827
+ def _validate_locations(self):
828
+ dist = self.env._r(array(self.loc).reshape((3, 1)), self.mics.pos)
829
+ if npany(dist < 1e-7):
830
+ warn('Source and microphone locations are identical.', Warning, stacklevel=2)
560
831
 
561
- # --- End of backwards compatibility traits --------------------------------------
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=())
562
836
 
563
- #: Start time of the signal in seconds, defaults to 0 s.
837
+ #: Start time of the signal in seconds. Default is ``0.0``.
564
838
  start_t = Float(0.0, desc='signal start time')
565
839
 
566
- #: Start time of the data aquisition at microphones in seconds,
567
- #: defaults to 0 s.
840
+ #: Start time of data acquisition at the microphones in seconds. Default is ``0.0``.
568
841
  start = Float(0.0, desc='sample start time')
569
842
 
570
- #: Signal behaviour for negative time indices, i.e. if :attr:`start` < :attr:start_t.
571
- #: `loop` take values from the end of :attr:`signal.signal()` array.
572
- #: `zeros` set source signal to zero, advisable for deterministic signals.
573
- #: defaults to `loop`.
574
- prepadding = Trait('loop', 'zeros', desc='Behaviour for negative time indices.')
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'``.
850
+ prepadding = Enum('loop', 'zeros', desc='Behaviour for negative time indices.')
575
851
 
576
- #: Upsampling factor, internal use, defaults to 16.
852
+ #: Internal upsampling factor for finer signal resolution. Default is ``16``.
577
853
  up = Int(16, desc='upsampling factor')
578
854
 
579
- #: Number of samples, is set automatically /
580
- #: depends on :attr:`signal`.
581
- numsamples = Delegate('signal')
855
+ #: Total number of samples in the emitted signal, derived from the :attr:`signal` generator.
856
+ num_samples = Delegate('signal')
582
857
 
583
- #: Sampling frequency of the signal, is set automatically /
584
- #: depends on :attr:`signal`.
858
+ #: Sampling frequency of the signal, derived from the :attr:`signal` generator.
585
859
  sample_freq = Delegate('signal')
586
860
 
587
- # internal identifier
861
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
588
862
  digest = Property(
589
863
  depends_on=[
590
864
  'mics.digest',
@@ -595,7 +869,6 @@ class PointSource(SamplesGenerator):
595
869
  'start',
596
870
  'up',
597
871
  'prepadding',
598
- '__class__',
599
872
  ],
600
873
  )
601
874
 
@@ -604,26 +877,38 @@ class PointSource(SamplesGenerator):
604
877
  return digest(self)
605
878
 
606
879
  def result(self, num=128):
607
- """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.
608
886
 
609
887
  Parameters
610
888
  ----------
611
- num : integer, defaults to 128
612
- This parameter defines the size of the blocks to be yielded
613
- (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``.
614
891
 
615
- Returns
616
- -------
617
- Samples in blocks of shape (num, numchannels).
618
- 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``.
619
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.
620
905
  """
621
906
  self._validate_locations()
622
- N = int(ceil(self.numsamples / num)) # number of output blocks
907
+ N = int(ceil(self.num_samples / num)) # number of output blocks
623
908
  signal = self.signal.usignal(self.up)
624
- out = empty((num, self.numchannels))
909
+ out = empty((num, self.num_channels))
625
910
  # distances
626
- rm = self.env._r(array(self.loc).reshape((3, 1)), self.mics.mpos).reshape(1, -1)
911
+ rm = self.env._r(array(self.loc).reshape((3, 1)), self.mics.pos).reshape(1, -1)
627
912
  # emission time relative to start_t (in samples) for first sample
628
913
  ind = (-rm / self.env.c - self.start_t + self.start) * self.sample_freq * self.up
629
914
 
@@ -634,16 +919,16 @@ class PointSource(SamplesGenerator):
634
919
  # if signal stops during prepadding, terminate
635
920
  if pre >= N:
636
921
  for _nb in range(N - 1):
637
- out = _fill_mic_signal_block(out, signal, rm, ind, num, self.numchannels, self.up, True)
922
+ out = _fill_mic_signal_block(out, signal, rm, ind, num, self.num_channels, self.up, True)
638
923
  yield out
639
924
 
640
- blocksize = self.numsamples % num or num
641
- out = _fill_mic_signal_block(out, signal, rm, ind, blocksize, self.numchannels, self.up, True)
925
+ blocksize = self.num_samples % num or num
926
+ out = _fill_mic_signal_block(out, signal, rm, ind, blocksize, self.num_channels, self.up, True)
642
927
  yield out[:blocksize]
643
928
  return
644
929
  else:
645
930
  for _nb in range(pre):
646
- out = _fill_mic_signal_block(out, signal, rm, ind, num, self.numchannels, self.up, True)
931
+ out = _fill_mic_signal_block(out, signal, rm, ind, num, self.num_channels, self.up, True)
647
932
  yield out
648
933
 
649
934
  else:
@@ -651,33 +936,42 @@ class PointSource(SamplesGenerator):
651
936
 
652
937
  # main generator
653
938
  for _nb in range(N - pre - 1):
654
- out = _fill_mic_signal_block(out, signal, rm, ind, num, self.numchannels, self.up, False)
939
+ out = _fill_mic_signal_block(out, signal, rm, ind, num, self.num_channels, self.up, False)
655
940
  yield out
656
941
 
657
942
  # last block of variable size
658
- blocksize = self.numsamples % num or num
659
- out = _fill_mic_signal_block(out, signal, rm, ind, blocksize, self.numchannels, self.up, False)
943
+ blocksize = self.num_samples % num or num
944
+ out = _fill_mic_signal_block(out, signal, rm, ind, blocksize, self.num_channels, self.up, False)
660
945
  yield out[:blocksize]
661
946
 
662
947
 
663
948
  class SphericalHarmonicSource(PointSource):
664
- """Class to define a fixed Spherical Harmonic Source with an arbitrary signal.
665
- 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.
666
956
 
667
957
  The output is being generated via the :meth:`result` generator.
668
958
  """
669
959
 
670
- #: Order of spherical harmonic source
960
+ #: Order of the spherical harmonic representation. Default is ``0``.
671
961
  lOrder = Int(0, desc='Order of spherical harmonic') # noqa: N815
672
962
 
963
+ #: Coefficients of the spherical harmonic modes for the given :attr:`lOrder`.
673
964
  alpha = CArray(desc='coefficients of the (lOrder,) spherical harmonic mode')
674
965
 
675
- #: 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)``.
676
968
  direction = Tuple((1.0, 0.0, 0.0), desc='Spherical Harmonic orientation')
677
969
 
970
+ #: Behavior of the signal for negative time indices. Currently only supports `loop`. Default is
971
+ #: ``'loop'``.
678
972
  prepadding = Enum('loop', desc='Behaviour for negative time indices.')
679
973
 
680
- # internal identifier
974
+ # Unique identifier for the current state of the source, based on its properties. (read-only)
681
975
  digest = Property(
682
976
  depends_on=[
683
977
  'mics.digest',
@@ -687,7 +981,6 @@ class SphericalHarmonicSource(PointSource):
687
981
  'start_t',
688
982
  'start',
689
983
  'up',
690
- '__class__',
691
984
  'alpha',
692
985
  'lOrder',
693
986
  'prepadding',
@@ -699,39 +992,78 @@ class SphericalHarmonicSource(PointSource):
699
992
  return digest(self)
700
993
 
701
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
+ """
702
1024
  Y_lm = get_modes(
703
1025
  lOrder=self.lOrder,
704
1026
  direction=self.direction,
705
- mpos=self.mics.mpos,
1027
+ mpos=self.mics.pos,
706
1028
  sourceposition=array(self.loc),
707
1029
  )
708
1030
  return real(ifft(fft(signals, axis=0) * (Y_lm @ self.alpha), axis=0))
709
1031
 
710
1032
  def result(self, num=128):
711
- """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.
712
1039
 
713
1040
  Parameters
714
1041
  ----------
715
- num : integer, defaults to 128
716
- This parameter defines the size of the blocks to be yielded
717
- (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``.
718
1044
 
719
- Returns
720
- -------
721
- Samples in blocks of shape (num, numchannels).
722
- 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``.
723
1051
 
1052
+ Raises
1053
+ ------
1054
+ :obj:`IndexError`
1055
+ If no more samples are available from the signal source.
724
1056
  """
725
1057
  # If signal samples are needed for te < t_start, then samples are taken
726
1058
  # from the end of the calculated signal.
727
1059
 
728
1060
  signal = self.signal.usignal(self.up)
729
1061
  # emission time relative to start_t (in samples) for first sample
730
- rm = self.env._r(array(self.loc).reshape((3, 1)), self.mics.mpos)
1062
+ rm = self.env._r(array(self.loc).reshape((3, 1)), self.mics.pos)
731
1063
  ind = (-rm / self.env.c - self.start_t + self.start) * self.sample_freq + pi / 30
732
1064
  i = 0
733
- n = self.numsamples
734
- out = empty((num, self.numchannels))
1065
+ n = self.num_samples
1066
+ out = empty((num, self.num_channels))
735
1067
  while n:
736
1068
  n -= 1
737
1069
  try:
@@ -748,24 +1080,34 @@ class SphericalHarmonicSource(PointSource):
748
1080
 
749
1081
 
750
1082
  class MovingPointSource(PointSource):
751
- """Class to define a point source with an arbitrary
752
- signal moving along a given trajectory.
753
- This can be used in simulations.
1083
+ """
1084
+ Define a moving :class:`point source<PointSource>` emitting a :attr:`~PointSource.signal`.
754
1085
 
755
- 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.
756
1095
  """
757
1096
 
758
- #: 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``.
759
1100
  conv_amp = Bool(False, desc='determines if convective amplification is considered')
760
1101
 
761
- #: Trajectory of the source,
762
- #: instance of the :class:`~acoular.trajectory.Trajectory` class.
763
- #: The start time is assumed to be the same as for the samples.
764
- trajectory = Trait(Trajectory, desc='trajectory of the source')
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.
1104
+ trajectory = Instance(Trajectory, desc='trajectory of the source')
765
1105
 
1106
+ #: Behavior of the signal for negative time indices. Currently only supports ``'loop'``.
1107
+ #: Default is ``'loop'``.
766
1108
  prepadding = Enum('loop', desc='Behaviour for negative time indices.')
767
1109
 
768
- # internal identifier
1110
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
769
1111
  digest = Property(
770
1112
  depends_on=[
771
1113
  'mics.digest',
@@ -777,7 +1119,6 @@ class MovingPointSource(PointSource):
777
1119
  'start',
778
1120
  'trajectory.digest',
779
1121
  'prepadding',
780
- '__class__',
781
1122
  ],
782
1123
  )
783
1124
 
@@ -786,33 +1127,52 @@ class MovingPointSource(PointSource):
786
1127
  return digest(self)
787
1128
 
788
1129
  def result(self, num=128):
789
- """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.
790
1136
 
791
1137
  Parameters
792
1138
  ----------
793
- num : integer, defaults to 128
794
- This parameter defines the size of the blocks to be yielded
795
- (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``.
796
1141
 
797
- Returns
798
- -------
799
- Samples in blocks of shape (num, numchannels).
800
- 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``.
801
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.
802
1162
  """
803
1163
  # If signal samples are needed for te < t_start, then samples are taken
804
1164
  # from the end of the calculated signal.
805
1165
 
806
1166
  signal = self.signal.usignal(self.up)
807
- out = empty((num, self.numchannels))
808
- # shortcuts and intial values
1167
+ out = empty((num, self.num_channels))
1168
+ # shortcuts and initial values
809
1169
  m = self.mics
810
1170
  t = self.start * ones(m.num_mics)
811
1171
  i = 0
812
1172
  epslim = 0.1 / self.up / self.sample_freq
813
1173
  c0 = self.env.c
814
1174
  tr = self.trajectory
815
- n = self.numsamples
1175
+ n = self.num_samples
816
1176
  while n:
817
1177
  n -= 1
818
1178
  eps = ones(m.num_mics)
@@ -821,7 +1181,7 @@ class MovingPointSource(PointSource):
821
1181
  # Newton-Rhapson iteration
822
1182
  while abs(eps).max() > epslim and j < 100:
823
1183
  loc = array(tr.location(te))
824
- rm = loc - m.mpos # distance vectors to microphones
1184
+ rm = loc - m.pos # distance vectors to microphones
825
1185
  rm = sqrt((rm * rm).sum(0)) # absolute distance
826
1186
  loc /= sqrt((loc * loc).sum(0)) # distance unit vector
827
1187
  der = array(tr.location(te, der=1))
@@ -847,24 +1207,42 @@ class MovingPointSource(PointSource):
847
1207
 
848
1208
 
849
1209
  class PointSourceDipole(PointSource):
850
- """Class to define a fixed point source with an arbitrary signal and
851
- dipole characteristics via superposition of two nearby inversely
852
- phased monopoles.
853
- This can be used in simulations.
1210
+ """
1211
+ Define a fixed point source with dipole characteristics.
854
1212
 
855
- 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.
856
1228
  """
857
1229
 
858
- #: Vector to define the orientation of the dipole lobes. Its magnitude
859
- #: governs the distance between the monopoles
860
- #: (dist = [lowest wavelength in spectrum] x [magnitude] x 1e-5).
861
- #: Note: Use vectors with order of magnitude around 1.0 or less
862
- #: 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.
863
1239
  direction = Tuple((0.0, 0.0, 1.0), desc='dipole orientation and distance of the inversely phased monopoles')
864
1240
 
1241
+ #: Behavior of the signal for negative time indices. Currently only supports ``'loop'``.
1242
+ #: Default is ``'loop'``.
865
1243
  prepadding = Enum('loop', desc='Behaviour for negative time indices.')
866
1244
 
867
- # internal identifier
1245
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
868
1246
  digest = Property(
869
1247
  depends_on=[
870
1248
  'mics.digest',
@@ -876,7 +1254,6 @@ class PointSourceDipole(PointSource):
876
1254
  'up',
877
1255
  'direction',
878
1256
  'prepadding',
879
- '__class__',
880
1257
  ],
881
1258
  )
882
1259
 
@@ -885,24 +1262,36 @@ class PointSourceDipole(PointSource):
885
1262
  return digest(self)
886
1263
 
887
1264
  def result(self, num=128):
888
- """Python generator that yields the output at microphones block-wise.
1265
+ """
1266
+ Generate output signal at microphones in blocks.
889
1267
 
890
1268
  Parameters
891
1269
  ----------
892
- num : integer, defaults to 128
893
- This parameter defines the size of the blocks to be yielded
894
- (i.e. the number of samples per block) .
1270
+ num : :class:`int`, optional
1271
+ Number of samples per block to yield. Default is ``128``.
895
1272
 
896
- Returns
897
- -------
898
- Samples in blocks of shape (num, numchannels).
899
- 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``.
900
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.
901
1290
  """
902
1291
  # If signal samples are needed for te < t_start, then samples are taken
903
1292
  # from the end of the calculated signal.
904
1293
 
905
- mpos = self.mics.mpos
1294
+ mpos = self.mics.pos
906
1295
  # position of the dipole as (3,1) vector
907
1296
  loc = array(self.loc, dtype=float).reshape((3, 1))
908
1297
  # direction vector from tuple
@@ -921,7 +1310,7 @@ class PointSourceDipole(PointSource):
921
1310
  dir2 = (direc_n * dist / 2.0).reshape((3, 1))
922
1311
 
923
1312
  signal = self.signal.usignal(self.up)
924
- out = empty((num, self.numchannels))
1313
+ out = empty((num, self.num_channels))
925
1314
 
926
1315
  # distance from dipole center to microphones
927
1316
  rm = self.env._r(loc, mpos)
@@ -935,7 +1324,7 @@ class PointSourceDipole(PointSource):
935
1324
  ind2 = (-rm2 / c - self.start_t + self.start) * self.sample_freq
936
1325
 
937
1326
  i = 0
938
- n = self.numsamples
1327
+ n = self.num_samples
939
1328
  while n:
940
1329
  n -= 1
941
1330
  try:
@@ -962,7 +1351,28 @@ class PointSourceDipole(PointSource):
962
1351
 
963
1352
 
964
1353
  class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
965
- # 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)
966
1376
  digest = Property(
967
1377
  depends_on=[
968
1378
  'mics.digest',
@@ -973,12 +1383,12 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
973
1383
  'start',
974
1384
  'up',
975
1385
  'direction',
976
- '__class__',
977
1386
  ],
978
1387
  )
979
1388
 
980
- #: Reference vector, perpendicular to the x and y-axis of moving source.
981
- #: 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)``.
982
1392
  rvec = CArray(dtype=float, shape=(3,), value=array((0, 0, 0)), desc='reference vector')
983
1393
 
984
1394
  @cached_property
@@ -986,6 +1396,41 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
986
1396
  return digest(self)
987
1397
 
988
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
+ """
989
1434
  eps = ones(self.mics.num_mics)
990
1435
  epslim = 0.1 / self.up / self.sample_freq
991
1436
  te = t.copy() # init emission time = receiving time
@@ -995,7 +1440,7 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
995
1440
  xs = array(self.trajectory.location(te))
996
1441
  loc = xs.copy()
997
1442
  loc += direction
998
- rm = loc - self.mics.mpos # distance vectors to microphones
1443
+ rm = loc - self.mics.pos # distance vectors to microphones
999
1444
  rm = sqrt((rm * rm).sum(0)) # absolute distance
1000
1445
  loc /= sqrt((loc * loc).sum(0)) # distance unit vector
1001
1446
  der = array(self.trajectory.location(te, der=1))
@@ -1006,7 +1451,38 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1006
1451
  return te, rm, Mr, xs
1007
1452
 
1008
1453
  def get_moving_direction(self, direction, time=0):
1009
- """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
+ """
1010
1486
  trajg1 = array(self.trajectory.location(time, der=1))[:, 0][:, newaxis]
1011
1487
  rflag = (self.rvec == 0).all() # flag translation vs. rotation
1012
1488
  if rflag:
@@ -1020,23 +1496,29 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1020
1496
  return cross(newdir[:, 0].T, self.rvec.T).T
1021
1497
 
1022
1498
  def result(self, num=128):
1023
- """Python generator that yields the output at microphones block-wise.
1499
+ """
1500
+ Generate the output signal at microphones in blocks.
1024
1501
 
1025
1502
  Parameters
1026
1503
  ----------
1027
- num : integer, defaults to 128
1028
- This parameter defines the size of the blocks to be yielded
1029
- (i.e. the number of samples per block) .
1030
-
1031
- Returns
1032
- -------
1033
- Samples in blocks of shape (num, numchannels).
1034
- The last block may be shorter than num.
1504
+ num : :class:`int`, optional
1505
+ Number of samples per block to yield. Default is ``128``.
1035
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.
1036
1518
  """
1037
1519
  # If signal samples are needed for te < t_start, then samples are taken
1038
1520
  # from the end of the calculated signal.
1039
- mpos = self.mics.mpos
1521
+ mpos = self.mics.pos
1040
1522
 
1041
1523
  # direction vector from tuple
1042
1524
  direc = array(self.direction, dtype=float) * 1e-5
@@ -1051,13 +1533,13 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1051
1533
  dir2 = (direc_n * dist / 2.0).reshape((3, 1))
1052
1534
 
1053
1535
  signal = self.signal.usignal(self.up)
1054
- out = empty((num, self.numchannels))
1055
- # shortcuts and intial values
1536
+ out = empty((num, self.num_channels))
1537
+ # shortcuts and initial values
1056
1538
  m = self.mics
1057
1539
  t = self.start * ones(m.num_mics)
1058
1540
 
1059
1541
  i = 0
1060
- n = self.numsamples
1542
+ n = self.num_samples
1061
1543
  while n:
1062
1544
  n -= 1
1063
1545
  te, rm, Mr, locs = self.get_emission_time(t, 0)
@@ -1096,28 +1578,46 @@ class MovingPointSourceDipole(PointSourceDipole, MovingPointSource):
1096
1578
 
1097
1579
 
1098
1580
  class LineSource(PointSource):
1099
- """Class to define a fixed Line source with an arbitrary signal.
1100
- This can be used in simulations.
1581
+ """
1582
+ Define a fixed line source with a signal.
1101
1583
 
1102
- 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.
1103
1603
  """
1104
1604
 
1105
- #: 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)``.
1106
1606
  direction = Tuple((0.0, 0.0, 1.0), desc='Line orientation ')
1107
1607
 
1108
- #: 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``.
1109
1609
  length = Float(1, desc='length of the line source')
1110
1610
 
1111
- #: number of monopol sources in the line source
1611
+ #: Number of monopole sources in the line source. Default is ``1``.
1112
1612
  num_sources = Int(1)
1113
1613
 
1114
- #: source strength for every monopole
1614
+ #: Strength coefficients for each monopole source.
1115
1615
  source_strength = CArray(desc='coefficients of the source strength')
1116
1616
 
1117
- #:coherence
1118
- coherence = Trait('coherent', 'incoherent', desc='coherence mode')
1617
+ #: Coherence mode for the monopoles (``'coherent'`` or ``'incoherent'``).
1618
+ coherence = Enum('coherent', 'incoherent', desc='coherence mode')
1119
1619
 
1120
- # internal identifier
1620
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
1121
1621
  digest = Property(
1122
1622
  depends_on=[
1123
1623
  'mics.digest',
@@ -1130,7 +1630,6 @@ class LineSource(PointSource):
1130
1630
  'direction',
1131
1631
  'source_strength',
1132
1632
  'coherence',
1133
- '__class__',
1134
1633
  ],
1135
1634
  )
1136
1635
 
@@ -1139,24 +1638,25 @@ class LineSource(PointSource):
1139
1638
  return digest(self)
1140
1639
 
1141
1640
  def result(self, num=128):
1142
- """Python generator that yields the output at microphones block-wise.
1641
+ """
1642
+ Generate the output signal at microphones in blocks.
1143
1643
 
1144
1644
  Parameters
1145
1645
  ----------
1146
- num : integer, defaults to 128
1147
- This parameter defines the size of the blocks to be yielded
1148
- (i.e. the number of samples per block) .
1149
-
1150
- Returns
1151
- -------
1152
- Samples in blocks of shape (num, numchannels).
1153
- The last block may be shorter than num.
1646
+ num : :class:`int`, optional
1647
+ Number of samples per block to yield. Default is ``128``.
1154
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``.
1155
1655
  """
1156
1656
  # If signal samples are needed for te < t_start, then samples are taken
1157
1657
  # from the end of the calculated signal.
1158
1658
 
1159
- mpos = self.mics.mpos
1659
+ mpos = self.mics.pos
1160
1660
 
1161
1661
  # direction vector from tuple
1162
1662
  direc = array(self.direction, dtype=float)
@@ -1168,14 +1668,14 @@ class LineSource(PointSource):
1168
1668
  dist = self.length / self.num_sources
1169
1669
 
1170
1670
  # blocwise output
1171
- out = zeros((num, self.numchannels))
1671
+ out = zeros((num, self.num_channels))
1172
1672
 
1173
1673
  # distance from line start position to microphones
1174
1674
  loc = array(self.loc, dtype=float).reshape((3, 1))
1175
1675
 
1176
1676
  # distances from monopoles in the line to microphones
1177
- rms = empty((self.numchannels, self.num_sources))
1178
- inds = empty((self.numchannels, self.num_sources))
1677
+ rms = empty((self.num_channels, self.num_sources))
1678
+ inds = empty((self.num_channels, self.num_sources))
1179
1679
  signals = empty((self.num_sources, len(self.signal.usignal(self.up))))
1180
1680
  # for every source - distances
1181
1681
  for s in range(self.num_sources):
@@ -1187,7 +1687,7 @@ class LineSource(PointSource):
1187
1687
  self.signal.rms = self.signal.rms * self.source_strength[s]
1188
1688
  signals[s] = self.signal.usignal(self.up)
1189
1689
  i = 0
1190
- n = self.numsamples
1690
+ n = self.num_samples
1191
1691
  while n:
1192
1692
  n -= 1
1193
1693
  try:
@@ -1199,7 +1699,7 @@ class LineSource(PointSource):
1199
1699
  i += 1
1200
1700
  if i == num:
1201
1701
  yield out
1202
- out = zeros((num, self.numchannels))
1702
+ out = zeros((num, self.num_channels))
1203
1703
  i = 0
1204
1704
  except IndexError:
1205
1705
  break
@@ -1208,7 +1708,30 @@ class LineSource(PointSource):
1208
1708
 
1209
1709
 
1210
1710
  class MovingLineSource(LineSource, MovingPointSource):
1211
- # 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)
1212
1735
  digest = Property(
1213
1736
  depends_on=[
1214
1737
  'mics.digest',
@@ -1219,12 +1742,13 @@ class MovingLineSource(LineSource, MovingPointSource):
1219
1742
  'start',
1220
1743
  'up',
1221
1744
  'direction',
1222
- '__class__',
1223
1745
  ],
1224
1746
  )
1225
1747
 
1226
- #: Reference vector, perpendicular to the x and y-axis of moving source.
1227
- #: 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)``.
1228
1752
  rvec = CArray(dtype=float, shape=(3,), value=array((0, 0, 0)), desc='reference vector')
1229
1753
 
1230
1754
  @cached_property
@@ -1232,7 +1756,40 @@ class MovingLineSource(LineSource, MovingPointSource):
1232
1756
  return digest(self)
1233
1757
 
1234
1758
  def get_moving_direction(self, direction, time=0):
1235
- """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
+ """
1236
1793
  trajg1 = array(self.trajectory.location(time, der=1))[:, 0][:, newaxis]
1237
1794
  rflag = (self.rvec == 0).all() # flag translation vs. rotation
1238
1795
  if rflag:
@@ -1246,6 +1803,47 @@ class MovingLineSource(LineSource, MovingPointSource):
1246
1803
  return cross(newdir[:, 0].T, self.rvec.T).T
1247
1804
 
1248
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
+ """
1249
1847
  eps = ones(self.mics.num_mics)
1250
1848
  epslim = 0.1 / self.up / self.sample_freq
1251
1849
  te = t.copy() # init emission time = receiving time
@@ -1255,7 +1853,7 @@ class MovingLineSource(LineSource, MovingPointSource):
1255
1853
  xs = array(self.trajectory.location(te))
1256
1854
  loc = xs.copy()
1257
1855
  loc += direction
1258
- rm = loc - self.mics.mpos # distance vectors to microphones
1856
+ rm = loc - self.mics.pos # distance vectors to microphones
1259
1857
  rm = sqrt((rm * rm).sum(0)) # absolute distance
1260
1858
  loc /= sqrt((loc * loc).sum(0)) # distance unit vector
1261
1859
  der = array(self.trajectory.location(te, der=1))
@@ -1266,23 +1864,24 @@ class MovingLineSource(LineSource, MovingPointSource):
1266
1864
  return te, rm, Mr, xs
1267
1865
 
1268
1866
  def result(self, num=128):
1269
- """Python generator that yields the output at microphones block-wise.
1867
+ """
1868
+ Generate the output signal at microphones in blocks.
1270
1869
 
1271
1870
  Parameters
1272
1871
  ----------
1273
- num : integer, defaults to 128
1274
- This parameter defines the size of the blocks to be yielded
1275
- (i.e. the number of samples per block) .
1276
-
1277
- Returns
1278
- -------
1279
- Samples in blocks of shape (num, numchannels).
1280
- The last block may be shorter than num.
1872
+ num : :class:`int`, optional
1873
+ Number of samples per block to yield. Default is ``128``.
1281
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``.
1282
1881
  """
1283
1882
  # If signal samples are needed for te < t_start, then samples are taken
1284
1883
  # from the end of the calculated signal.
1285
- mpos = self.mics.mpos
1884
+ mpos = self.mics.pos
1286
1885
 
1287
1886
  # direction vector from tuple
1288
1887
  direc = array(self.direction, dtype=float)
@@ -1294,11 +1893,11 @@ class MovingLineSource(LineSource, MovingPointSource):
1294
1893
  dir2 = (direc_n * dist).reshape((3, 1))
1295
1894
 
1296
1895
  # blocwise output
1297
- out = zeros((num, self.numchannels))
1896
+ out = zeros((num, self.num_channels))
1298
1897
 
1299
1898
  # distances from monopoles in the line to microphones
1300
- rms = empty((self.numchannels, self.num_sources))
1301
- inds = empty((self.numchannels, self.num_sources))
1899
+ rms = empty((self.num_channels, self.num_sources))
1900
+ inds = empty((self.num_channels, self.num_sources))
1302
1901
  signals = empty((self.num_sources, len(self.signal.usignal(self.up))))
1303
1902
  # coherence
1304
1903
  for s in range(self.num_sources):
@@ -1307,13 +1906,13 @@ class MovingLineSource(LineSource, MovingPointSource):
1307
1906
  self.signal.seed = s + abs(int(hash(self.digest) // 10e12))
1308
1907
  self.signal.rms = self.signal.rms * self.source_strength[s]
1309
1908
  signals[s] = self.signal.usignal(self.up)
1310
- mpos = self.mics.mpos
1909
+ mpos = self.mics.pos
1311
1910
 
1312
- # shortcuts and intial values
1911
+ # shortcuts and initial values
1313
1912
  m = self.mics
1314
1913
  t = self.start * ones(m.num_mics)
1315
1914
  i = 0
1316
- n = self.numsamples
1915
+ n = self.num_samples
1317
1916
  while n:
1318
1917
  n -= 1
1319
1918
  t += 1.0 / self.sample_freq
@@ -1323,7 +1922,7 @@ class MovingLineSource(LineSource, MovingPointSource):
1323
1922
  # get distance and ind for every source in the line
1324
1923
  for s in range(self.num_sources):
1325
1924
  diff = self.get_moving_direction(dir2, te1)
1326
- te, rm, Mr, locs = self.get_emission_time(t, tile((diff * s).T, (self.numchannels, 1)).T)
1925
+ te, rm, Mr, locs = self.get_emission_time(t, tile((diff * s).T, (self.num_channels, 1)).T)
1327
1926
  loc = array(self.trajectory.location(te), dtype=float)[:, 0][:, newaxis]
1328
1927
  diff = self.get_moving_direction(dir2, te)
1329
1928
  rms[:, s] = self.env._r((loc + diff * s), mpos)
@@ -1341,84 +1940,111 @@ class MovingLineSource(LineSource, MovingPointSource):
1341
1940
  i += 1
1342
1941
  if i == num:
1343
1942
  yield out
1344
- out = zeros((num, self.numchannels))
1943
+ out = zeros((num, self.num_channels))
1345
1944
  i = 0
1346
1945
  except IndexError:
1347
1946
  break
1348
1947
  yield out[:i]
1349
1948
 
1350
1949
 
1950
+ @deprecated_alias({'numchannels': 'num_channels'}, read_only=True)
1351
1951
  class UncorrelatedNoiseSource(SamplesGenerator):
1352
- """Class to simulate white or pink noise as uncorrelated signal at each
1353
- channel.
1354
-
1355
- The output is being generated via the :meth:`result` generator.
1356
1952
  """
1953
+ Simulate uncorrelated white or pink noise signals at multiple channels.
1357
1954
 
1358
- #: Type of noise to generate at the channels.
1359
- #: The `~acoular.signals.SignalGenerator`-derived class has to
1360
- # feature the parameter "seed" (i.e. white or pink noise).
1361
- signal = Trait(SignalGenerator, desc='type of noise')
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.
1362
1959
 
1363
- #: Array with seeds for random number generator.
1364
- #: When left empty, arange(:attr:`numchannels`) + :attr:`signal`.seed
1365
- #: will be used.
1366
- seed = CArray(dtype=uint32, desc='random seed values')
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.
1367
1973
 
1368
- #: Number of channels in output; is set automatically /
1369
- #: depends on used microphone geometry.
1370
- numchannels = Delegate('mics', 'num_mics')
1974
+ Examples
1975
+ --------
1976
+ To simulate uncorrelated white noise at multiple channels:
1371
1977
 
1372
- #: :class:`~acoular.microphones.MicGeom` object that provides the microphone locations.
1373
- mics = Trait(MicGeom, desc='microphone geometry')
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)
1374
1998
 
1375
- # --- List of backwards compatibility traits and their setters/getters -----------
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.
2001
+ """
1376
2002
 
1377
- # Microphone locations.
1378
- # Deprecated! Use :attr:`mics` trait instead.
1379
- mpos = Property()
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.
2006
+ signal = Instance(NoiseGenerator, desc='type of noise')
1380
2007
 
1381
- def _get_mpos(self):
1382
- return self.mics
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>`.
2011
+ seed = CArray(dtype=uint32, desc='random seed values')
1383
2012
 
1384
- def _set_mpos(self, mpos):
1385
- msg = (
1386
- "Deprecated use of 'mpos' trait. Use 'mics' trait instead."
1387
- "The 'mpos' trait will be removed in version 25.01."
1388
- )
1389
- warn(msg, DeprecationWarning, stacklevel=2)
1390
- self.mics = mpos
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.
2016
+ num_channels = Delegate('mics', 'num_mics')
1391
2017
 
1392
- # --- End of backwards compatibility traits --------------------------------------
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>`.
2021
+ mics = Instance(MicGeom, desc='microphone geometry')
1393
2022
 
1394
- #: 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``.
1395
2025
  start_t = Float(0.0, desc='signal start time')
1396
2026
 
1397
- #: Start time of the data aquisition at microphones in seconds,
1398
- #: 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``.
1399
2029
  start = Float(0.0, desc='sample start time')
1400
2030
 
1401
- #: Number of samples is set automatically /
1402
- #: depends on :attr:`signal`.
1403
- numsamples = Delegate('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.
2033
+ num_samples = Delegate('signal')
1404
2034
 
1405
- #: Sampling frequency of the signal; is set automatically /
1406
- #: 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.
1407
2037
  sample_freq = Delegate('signal')
1408
2038
 
1409
- # internal identifier
2039
+ #: A unique identifier for the current state of the source, based on its properties. (read-only)
1410
2040
  digest = Property(
1411
2041
  depends_on=[
1412
2042
  'mics.digest',
1413
- 'signal.rms',
1414
- 'signal.numsamples',
1415
- 'signal.sample_freq',
1416
- 'signal.__class__',
2043
+ 'signal.digest',
1417
2044
  'seed',
1418
2045
  'loc',
1419
2046
  'start_t',
1420
2047
  'start',
1421
- '__class__',
1422
2048
  ],
1423
2049
  )
1424
2050
 
@@ -1427,80 +2053,190 @@ class UncorrelatedNoiseSource(SamplesGenerator):
1427
2053
  return digest(self)
1428
2054
 
1429
2055
  def result(self, num=128):
1430
- """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.
1431
2063
 
1432
2064
  Parameters
1433
2065
  ----------
1434
- num : integer, defaults to 128
1435
- This parameter defines the size of the blocks to be yielded
1436
- (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``.
1437
2068
 
1438
- Returns
1439
- -------
1440
- Samples in blocks of shape (num, numchannels).
1441
- 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.
1442
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.
1443
2085
  """
1444
2086
  Noise = self.signal.__class__
1445
2087
  # create or get the array of random seeds
1446
- if not self.seed:
1447
- seed = arange(self.numchannels) + self.signal.seed
1448
- elif self.seed.shape == (self.numchannels,):
2088
+ if not self.seed.size > 0:
2089
+ seed = arange(self.num_channels) + self.signal.seed
2090
+ elif self.seed.shape == (self.num_channels,):
1449
2091
  seed = self.seed
1450
2092
  else:
1451
- raise ValueError(
1452
- 'Seed array expected to be of shape (%i,), but has shape %s.'
1453
- % (self.numchannels, str(self.seed.shape)),
1454
- )
1455
-
1456
- # create array with [numchannels] noise signal tracks
2093
+ msg = f'Seed array expected to be of shape ({self.num_channels:d},), but has shape {self.seed.shape}.'
2094
+ raise ValueError(msg)
2095
+ # create array with [num_channels] noise signal tracks
1457
2096
  signal = array(
1458
2097
  [
1459
- Noise(seed=s, numsamples=self.numsamples, sample_freq=self.sample_freq, rms=self.signal.rms).signal()
2098
+ Noise(seed=s, num_samples=self.num_samples, sample_freq=self.sample_freq, rms=self.signal.rms).signal()
1460
2099
  for s in seed
1461
2100
  ],
1462
2101
  ).T
1463
2102
 
1464
2103
  n = num
1465
- while n <= self.numsamples:
2104
+ while n <= self.num_samples:
1466
2105
  yield signal[n - num : n, :]
1467
2106
  n += num
1468
2107
  else:
1469
- if (n - num) < self.numsamples:
2108
+ if (n - num) < self.num_samples:
1470
2109
  yield signal[n - num :, :]
1471
2110
  else:
1472
2111
  return
1473
2112
 
1474
2113
 
2114
+ @deprecated_alias({'numchannels': 'num_channels', 'numsamples': 'num_samples'}, read_only=True)
1475
2115
  class SourceMixer(SamplesGenerator):
1476
- """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.
1477
2127
 
1478
- #: List of :class:`~acoular.base.SamplesGenerator` objects
1479
- #: to be mixed.
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:
2140
+
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.
1480
2210
  sources = List(Instance(SamplesGenerator, ()))
1481
2211
 
1482
- #: 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``.
1483
2214
  sample_freq = Property(depends_on=['sdigest'])
1484
2215
 
1485
- #: Number of channels.
1486
- numchannels = Property(depends_on=['sdigest'])
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``.
2218
+ num_channels = Property(depends_on=['sdigest'])
1487
2219
 
1488
- #: Number of samples.
1489
- numsamples = Property(depends_on=['sdigest'])
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``.
2222
+ num_samples = Property(depends_on=['sdigest'])
1490
2223
 
1491
- #: Amplitude weight(s) for the sources as array. If not set,
1492
- #: all source signals are equally weighted.
1493
- #: 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.
1494
2228
  weights = CArray(desc='channel weights')
1495
2229
 
1496
- # internal identifier
2230
+ #: Internal identifier for the combined state of all sources, used to track
2231
+ #: changes in the sources for reproducibility and caching.
1497
2232
  sdigest = Str()
1498
2233
 
1499
2234
  @observe('sources.items.digest')
1500
2235
  def _set_sources_digest(self, event): # noqa ARG002
1501
2236
  self.sdigest = ldigest(self.sources)
1502
2237
 
1503
- # 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)
1504
2240
  digest = Property(depends_on=['sdigest', 'weights'])
1505
2241
 
1506
2242
  @cached_property
@@ -1512,41 +2248,64 @@ class SourceMixer(SamplesGenerator):
1512
2248
  return self.sources[0].sample_freq if self.sources else 0
1513
2249
 
1514
2250
  @cached_property
1515
- def _get_numchannels(self):
1516
- return self.sources[0].numchannels if self.sources else 0
2251
+ def _get_num_channels(self):
2252
+ return self.sources[0].num_channels if self.sources else 0
1517
2253
 
1518
2254
  @cached_property
1519
- def _get_numsamples(self):
1520
- return self.sources[0].numsamples if self.sources else 0
2255
+ def _get_num_samples(self):
2256
+ return self.sources[0].num_samples if self.sources else 0
1521
2257
 
1522
2258
  def validate_sources(self):
1523
- """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
+ """
1524
2271
  if len(self.sources) < 1:
1525
2272
  msg = 'Number of sources in SourceMixer should be at least 1.'
1526
2273
  raise ValueError(msg)
1527
2274
  for s in self.sources[1:]:
1528
2275
  if self.sample_freq != s.sample_freq:
1529
- raise ValueError('Sample frequency of %s does not fit' % s)
1530
- if self.numchannels != s.numchannels:
1531
- raise ValueError('Channel count of %s does not fit' % s)
1532
- if self.numsamples != s.numsamples:
1533
- raise ValueError('Number of samples of %s does not fit' % s)
2276
+ msg = f'Sample frequency of {s} does not fit'
2277
+ raise ValueError(msg)
2278
+ if self.num_channels != s.num_channels:
2279
+ msg = f'Channel count of {s} does not fit'
2280
+ raise ValueError(msg)
2281
+ if self.num_samples != s.num_samples:
2282
+ msg = f'Number of samples of {s} does not fit'
2283
+ raise ValueError(msg)
1534
2284
 
1535
2285
  def result(self, num):
1536
- """Python generator that yields the output block-wise.
1537
- 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.
1538
2292
 
1539
2293
  Parameters
1540
2294
  ----------
1541
- num : integer
1542
- This parameter defines the size of the blocks to be yielded
1543
- (i.e. the number of samples per block).
2295
+ num : :class:`int`
2296
+ Number of samples per block to be yielded.
1544
2297
 
1545
- Returns
1546
- -------
1547
- Samples in blocks of shape (num, numchannels).
1548
- 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``.
1549
2304
 
2305
+ Raises
2306
+ ------
2307
+ :obj:`ValueError`
2308
+ If the sources are not compatible for mixing.
1550
2309
  """
1551
2310
  # check whether all sources fit together
1552
2311
  self.validate_sources()
@@ -1570,61 +2329,124 @@ class SourceMixer(SamplesGenerator):
1570
2329
 
1571
2330
 
1572
2331
  class PointSourceConvolve(PointSource):
1573
- """Class to blockwise convolve an arbitrary source signal with a spatial room impulse response."""
2332
+ """
2333
+ Blockwise convolution of a source signal with an impulse response (IR).
2334
+
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.
2354
+
2355
+ Examples
2356
+ --------
2357
+ Convolve a stationary sine wave source with a room impulse response (RIR):
2358
+
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
+ """
1574
2391
 
1575
2392
  #: Convolution kernel in the time domain.
1576
- #: The second dimension of the kernel array has to be either 1 or match :attr:`~SamplesGenerator.numchannels`.
1577
- #: If only a single kernel is supplied, it is applied to all channels.
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.
1578
2395
  kernel = CArray(dtype=float, desc='Convolution kernel.')
1579
2396
 
1580
- # ------------- overwrite traits that are not supported by this class -------------
1581
-
1582
- #: Start time of the signal in seconds, defaults to 0 s.
2397
+ #: Start time of the signal in seconds. Default is ``0.0``.
1583
2398
  start_t = Enum(0.0, desc='signal start time')
1584
2399
 
1585
- #: Start time of the data aquisition at microphones in seconds,
1586
- #: defaults to 0 s.
2400
+ #: Start time of the data acquisition the the microphones in seconds. Default is ``0.0``.
1587
2401
  start = Enum(0.0, desc='sample start time')
1588
2402
 
1589
- #: Signal behaviour for negative time indices, i.e. if :attr:`start` < :attr:start_t.
1590
- #: `loop` take values from the end of :attr:`signal.signal()` array.
1591
- #: `zeros` set source signal to zero, advisable for deterministic signals.
1592
- #: defaults to `loop`.
1593
- 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.')
1594
2405
 
1595
- #: Upsampling factor, internal use, defaults to 16.
2406
+ #: Upsampling factor for internal use. Default is ``None``.
1596
2407
  up = Enum(None, desc='upsampling factor')
1597
2408
 
1598
- # internal identifier
1599
- digest = Property(
1600
- depends_on=['mics.digest', 'signal.digest', 'loc', 'kernel', '__class__'],
1601
- )
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'])
1602
2412
 
1603
2413
  @cached_property
1604
2414
  def _get_digest(self):
1605
2415
  return digest(self)
1606
2416
 
1607
2417
  def result(self, num=128):
1608
- """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).
1609
2424
 
1610
2425
  Parameters
1611
2426
  ----------
1612
- num : integer, defaults to 128
1613
- This parameter defines the size of the blocks to be yielded
1614
- (i.e. the number of samples per block) .
1615
-
1616
- Returns
1617
- -------
1618
- Samples in blocks of shape (num, numchannels).
1619
- 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``.
1620
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.
1621
2443
  """
1622
2444
  data = repeat(self.signal.signal()[:, newaxis], self.mics.num_mics, axis=1)
1623
2445
  source = TimeSamples(
1624
2446
  data=data,
1625
2447
  sample_freq=self.sample_freq,
1626
- numsamples=self.numsamples,
1627
- numchannels=self.mics.num_mics,
2448
+ num_samples=self.num_samples,
2449
+ num_channels=self.mics.num_mics,
1628
2450
  )
1629
2451
  time_convolve = TimeConvolve(
1630
2452
  source=source,