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