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