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/signals.py
CHANGED
|
@@ -1,149 +1,335 @@
|
|
|
1
1
|
# ------------------------------------------------------------------------------
|
|
2
2
|
# Copyright (c) Acoular Development Team.
|
|
3
3
|
# ------------------------------------------------------------------------------
|
|
4
|
-
"""
|
|
4
|
+
"""
|
|
5
|
+
Implements signal generators for the simulation of acoustic sources.
|
|
5
6
|
|
|
6
7
|
.. autosummary::
|
|
7
8
|
:toctree: generated/
|
|
8
9
|
|
|
9
10
|
SignalGenerator
|
|
11
|
+
PeriodicSignalGenerator
|
|
12
|
+
NoiseGenerator
|
|
10
13
|
WNoiseGenerator
|
|
11
14
|
PNoiseGenerator
|
|
12
15
|
FiltWNoiseGenerator
|
|
13
16
|
SineGenerator
|
|
14
17
|
GenericSignalGenerator
|
|
15
|
-
|
|
16
18
|
"""
|
|
17
19
|
|
|
18
20
|
# imports from other packages
|
|
21
|
+
from abc import abstractmethod
|
|
19
22
|
from warnings import warn
|
|
20
23
|
|
|
21
24
|
from numpy import arange, array, log, pi, repeat, sin, sqrt, tile, zeros
|
|
22
25
|
from numpy.random import RandomState
|
|
23
26
|
from scipy.signal import resample, sosfilt, tf2sos
|
|
24
|
-
from traits.api import
|
|
27
|
+
from traits.api import (
|
|
28
|
+
ABCHasStrictTraits,
|
|
29
|
+
Bool,
|
|
30
|
+
CArray,
|
|
31
|
+
CInt,
|
|
32
|
+
Delegate,
|
|
33
|
+
Float,
|
|
34
|
+
Instance,
|
|
35
|
+
Int,
|
|
36
|
+
Property,
|
|
37
|
+
cached_property,
|
|
38
|
+
)
|
|
25
39
|
|
|
26
40
|
# acoular imports
|
|
27
41
|
from .base import SamplesGenerator
|
|
42
|
+
from .deprecation import deprecated_alias
|
|
28
43
|
from .internal import digest
|
|
29
44
|
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Defines the common interface for all SignalGenerator classes. This class
|
|
35
|
-
may be used as a base for specialized SignalGenerator implementations. It
|
|
36
|
-
should not be used directly as it contains no real functionality.
|
|
46
|
+
@deprecated_alias({'numsamples': 'num_samples'})
|
|
47
|
+
class SignalGenerator(ABCHasStrictTraits):
|
|
37
48
|
"""
|
|
49
|
+
ABC for a simple one-channel signal generator.
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
This ABC defines the common interface and attributes for all signal generator implementations.
|
|
52
|
+
It provides a template for generating one-channel signals with specified amplitude,
|
|
53
|
+
sampling frequency, and duration. Subclasses should implement the core functionality,
|
|
54
|
+
including signal generation and computation of the internal identifier.
|
|
55
|
+
|
|
56
|
+
See Also
|
|
57
|
+
--------
|
|
58
|
+
:func:`scipy.signal.resample` : Used for resampling signals in the :meth:`usignal` method.
|
|
59
|
+
|
|
60
|
+
Notes
|
|
61
|
+
-----
|
|
62
|
+
This class should not be instantiated directly. Instead, use a subclass that
|
|
63
|
+
implements the required methods for signal generation.
|
|
64
|
+
"""
|
|
41
65
|
|
|
42
|
-
#: Sampling frequency of the signal.
|
|
66
|
+
#: Sampling frequency of the signal in Hz. Default is ``1.0``.
|
|
43
67
|
sample_freq = Float(1.0, desc='sampling frequency')
|
|
44
68
|
|
|
45
|
-
#:
|
|
46
|
-
|
|
69
|
+
#: The number of samples to generate for the signal.
|
|
70
|
+
num_samples = CInt
|
|
47
71
|
|
|
48
|
-
|
|
49
|
-
digest = Property
|
|
72
|
+
#: A unique checksum identifier based on the object properties. (read-only)
|
|
73
|
+
digest = Property(depends_on=['sample_freq', 'num_samples'])
|
|
50
74
|
|
|
75
|
+
@abstractmethod
|
|
51
76
|
def _get_digest(self):
|
|
52
|
-
|
|
77
|
+
"""Return the internal identifier."""
|
|
53
78
|
|
|
79
|
+
@abstractmethod
|
|
54
80
|
def signal(self):
|
|
55
|
-
"""
|
|
81
|
+
"""
|
|
82
|
+
Generate and return the signal.
|
|
83
|
+
|
|
84
|
+
This method must be implemented by subclasses to provide the generated signal
|
|
85
|
+
as a 1D array of samples.
|
|
86
|
+
"""
|
|
56
87
|
|
|
57
88
|
def usignal(self, factor):
|
|
58
|
-
"""
|
|
89
|
+
"""
|
|
90
|
+
Resample the signal at a higher sampling frequency.
|
|
59
91
|
|
|
60
|
-
|
|
92
|
+
This method uses Fourier transform-based resampling to deliver the signal at a
|
|
93
|
+
sampling frequency that is a multiple of the original :attr:`sample_freq`.
|
|
94
|
+
The resampled signal has a length of ``factor * num_samples``.
|
|
61
95
|
|
|
62
96
|
Parameters
|
|
63
97
|
----------
|
|
64
|
-
factor :
|
|
65
|
-
The factor
|
|
66
|
-
|
|
98
|
+
factor : int
|
|
99
|
+
The resampling factor. Defines how many times larger the new sampling frequency is
|
|
100
|
+
compared to the original :attr:`sample_freq`.
|
|
67
101
|
|
|
68
102
|
Returns
|
|
69
103
|
-------
|
|
70
|
-
|
|
71
|
-
The
|
|
72
|
-
|
|
104
|
+
:class:`numpy.ndarray`
|
|
105
|
+
The resampled signal as a 1D array of floats.
|
|
106
|
+
|
|
107
|
+
Notes
|
|
108
|
+
-----
|
|
109
|
+
This method relies on the :func:`scipy.signal.resample` function for resampling.
|
|
110
|
+
|
|
111
|
+
Examples
|
|
112
|
+
--------
|
|
113
|
+
Resample a signal by a factor of 4:
|
|
114
|
+
|
|
115
|
+
>>> from acoular import SineGenerator # Class extending SignalGenerator
|
|
116
|
+
>>> sg = SineGenerator(sample_freq=100.0, num_samples=1000)
|
|
117
|
+
>>> resampled_signal = sg.usignal(4)
|
|
118
|
+
>>> len(resampled_signal)
|
|
119
|
+
4000
|
|
73
120
|
"""
|
|
74
|
-
return resample(self.signal(), factor * self.
|
|
121
|
+
return resample(self.signal(), factor * self.num_samples)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class PeriodicSignalGenerator(SignalGenerator):
|
|
125
|
+
"""
|
|
126
|
+
Abstract base class for periodic signal generators.
|
|
127
|
+
|
|
128
|
+
The :class:`PeriodicSignalGenerator` class defines the common interface
|
|
129
|
+
for all :class:`SignalGenerator`-derived classes with periodic signals.
|
|
130
|
+
This class may be used as a base for class handling periodic signals
|
|
131
|
+
that can be characterized by their frequency, phase and amplitude.
|
|
132
|
+
|
|
133
|
+
It should not be used directly as it contains no real functionality.
|
|
134
|
+
|
|
135
|
+
See Also
|
|
136
|
+
--------
|
|
137
|
+
SineGenerator : Generate a sine signal.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
#: The frequency of the signal. Default is ``1000.0``.
|
|
141
|
+
freq = Float(1000.0, desc='Frequency')
|
|
142
|
+
|
|
143
|
+
#: The phase of the signal (in radians). Default is ``0.0``.
|
|
144
|
+
phase = Float(0.0, desc='Phase')
|
|
145
|
+
|
|
146
|
+
#: The amplitude of the signal. Default is ``1.0``.
|
|
147
|
+
amplitude = Float(1.0)
|
|
148
|
+
|
|
149
|
+
#: Internal identifier based on generator properties. (read-only)
|
|
150
|
+
digest = Property(depends_on=['amplitude', 'num_samples', 'sample_freq', 'freq', 'phase'])
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def _get_digest(self):
|
|
154
|
+
"""Return the internal identifier."""
|
|
155
|
+
|
|
156
|
+
@abstractmethod
|
|
157
|
+
def signal(self):
|
|
158
|
+
"""Deliver the signal."""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class NoiseGenerator(SignalGenerator):
|
|
162
|
+
"""
|
|
163
|
+
Abstract base class for noise signal generators.
|
|
75
164
|
|
|
165
|
+
The :class:`NoiseGenerator` class defines the common interface for all :class:`SignalGenerator`
|
|
166
|
+
classes with noise signals. This class may be used as a base for class handling noise signals
|
|
167
|
+
that can be characterized by their RMS amplitude.
|
|
76
168
|
|
|
77
|
-
|
|
78
|
-
"""White noise signal generator."""
|
|
169
|
+
It should not be used directly as it contains no real functionality.
|
|
79
170
|
|
|
80
|
-
|
|
171
|
+
See Also
|
|
172
|
+
--------
|
|
173
|
+
:class:`acoular.signals.PNoiseGenerator` : For pink noise generation.
|
|
174
|
+
:class:`acoular.signals.WNoiseGenerator` : For pink white generation.
|
|
175
|
+
:class:`acoular.sources.UncorrelatedNoiseSource` : For per-channel noise generation.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
#: Root mean square (RMS) amplitude of the signal. For a point source,
|
|
179
|
+
#: this corresponds to the RMS amplitude at a distance of 1 meter. Default is ``1.0``.
|
|
180
|
+
rms = Float(1.0, desc='rms amplitude')
|
|
181
|
+
|
|
182
|
+
#: Seed for random number generator. Default is ``0``.
|
|
81
183
|
#: This parameter should be set differently for different instances
|
|
82
184
|
#: to guarantee statistically independent (non-correlated) outputs.
|
|
83
185
|
seed = Int(0, desc='random seed value')
|
|
84
186
|
|
|
187
|
+
#: Internal identifier based on generator properties. (read-only)
|
|
188
|
+
digest = Property(depends_on=['rms', 'seed', 'sample_freq', 'num_samples'])
|
|
189
|
+
|
|
190
|
+
@abstractmethod
|
|
191
|
+
def _get_digest(self):
|
|
192
|
+
"""Return the internal identifier."""
|
|
193
|
+
|
|
194
|
+
@abstractmethod
|
|
195
|
+
def signal(self):
|
|
196
|
+
"""Generate and deliver the periodic signal."""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class WNoiseGenerator(NoiseGenerator):
|
|
200
|
+
"""
|
|
201
|
+
White noise signal generator.
|
|
202
|
+
|
|
203
|
+
This class generates white noise signals with a specified
|
|
204
|
+
:attr:`root mean square (RMS)<SignalGenerator.rms>` amplitude,
|
|
205
|
+
:attr:`number of samples<SignalGenerator.num_samples>`, and
|
|
206
|
+
:attr:`sampling frequency<SignalGenerator.sample_freq>`. The white noise is generated using a
|
|
207
|
+
:obj:`random number generator<numpy.random.RandomState.standard_normal>` initialized with a
|
|
208
|
+
:attr:`user-defined seed<seed>` for reproducibility.
|
|
209
|
+
|
|
210
|
+
See Also
|
|
211
|
+
--------
|
|
212
|
+
:obj:`numpy.random.RandomState.standard_normal` :
|
|
213
|
+
Used here to generate normally distributed noise.
|
|
214
|
+
:class:`acoular.signals.PNoiseGenerator` : For pink noise generation.
|
|
215
|
+
:class:`acoular.sources.UncorrelatedNoiseSource` : For per-channel noise generation.
|
|
216
|
+
|
|
217
|
+
Examples
|
|
218
|
+
--------
|
|
219
|
+
Generate white noise with an RMS amplitude of 1.0 and 0.5:
|
|
220
|
+
|
|
221
|
+
>>> from acoular import WNoiseGenerator
|
|
222
|
+
>>> from numpy import mean
|
|
223
|
+
>>>
|
|
224
|
+
>>> # White noise with RMS of 1.0
|
|
225
|
+
>>> gen1 = WNoiseGenerator(rms=1.0, num_samples=1000, seed=42)
|
|
226
|
+
>>> signal1 = gen1.signal()
|
|
227
|
+
>>>
|
|
228
|
+
>>> # White noise with RMS of 0.5
|
|
229
|
+
>>> gen2 = WNoiseGenerator(rms=0.5, num_samples=1000, seed=24)
|
|
230
|
+
>>> signal2 = gen2.signal()
|
|
231
|
+
>>>
|
|
232
|
+
>>> mean(signal1) > mean(signal2)
|
|
233
|
+
np.True_
|
|
234
|
+
|
|
235
|
+
Ensure different outputs with different seeds:
|
|
236
|
+
|
|
237
|
+
>>> gen1 = WNoiseGenerator(num_samples=3, seed=42)
|
|
238
|
+
>>> gen2 = WNoiseGenerator(num_samples=3, seed=73)
|
|
239
|
+
>>> gen1.signal() == gen2.signal()
|
|
240
|
+
array([False, False, False])
|
|
241
|
+
"""
|
|
242
|
+
|
|
85
243
|
# internal identifier
|
|
86
|
-
digest = Property(
|
|
87
|
-
depends_on=['rms', 'numsamples', 'sample_freq', 'seed', '__class__'],
|
|
88
|
-
)
|
|
244
|
+
digest = Property(depends_on=['rms', 'seed', 'sample_freq', 'num_samples'])
|
|
89
245
|
|
|
90
246
|
@cached_property
|
|
91
247
|
def _get_digest(self):
|
|
92
248
|
return digest(self)
|
|
93
249
|
|
|
94
250
|
def signal(self):
|
|
95
|
-
"""
|
|
251
|
+
"""
|
|
252
|
+
Generate and deliver the white noise signal.
|
|
253
|
+
|
|
254
|
+
The signal is created using a Gaussian distribution with mean 0 and variance 1,
|
|
255
|
+
scaled by the :attr:`RMS<SignalGenerator.rms>` amplitude of the object.
|
|
96
256
|
|
|
97
257
|
Returns
|
|
98
258
|
-------
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
259
|
+
:class:`numpy.ndarray`
|
|
260
|
+
A 1D array of floats containing the generated white noise signal.
|
|
261
|
+
The length of the array is equal to :attr:`~SignalGenerator.num_samples`.
|
|
102
262
|
"""
|
|
103
263
|
rnd_gen = RandomState(self.seed)
|
|
104
|
-
return self.rms * rnd_gen.standard_normal(self.
|
|
264
|
+
return self.rms * rnd_gen.standard_normal(self.num_samples)
|
|
105
265
|
|
|
106
266
|
|
|
107
|
-
class PNoiseGenerator(
|
|
108
|
-
"""
|
|
267
|
+
class PNoiseGenerator(NoiseGenerator):
|
|
268
|
+
"""
|
|
269
|
+
Generate pink noise signal.
|
|
109
270
|
|
|
110
|
-
|
|
111
|
-
|
|
271
|
+
The :class:`PNoiseGenerator` class generates pink noise signals,
|
|
272
|
+
which exhibit a :math:`1/f` power spectral density. Pink noise is characterized by
|
|
273
|
+
equal energy per octave, making it useful in various applications such as audio testing,
|
|
274
|
+
sound synthesis, and environmental noise simulations.
|
|
112
275
|
|
|
113
|
-
|
|
114
|
-
|
|
276
|
+
The pink noise simulation is based on the Voss-McCartney algorithm, which iteratively adds
|
|
277
|
+
noise with increasing wavelength to achieve the desired :math:`1/f` characteristic.
|
|
115
278
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
279
|
+
See Also
|
|
280
|
+
--------
|
|
281
|
+
:class:`acoular.signals.WNoiseGenerator` : For white noise generation.
|
|
282
|
+
:class:`acoular.sources.UncorrelatedNoiseSource` : For per-channel noise generation.
|
|
119
283
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
284
|
+
References
|
|
285
|
+
----------
|
|
286
|
+
- S.J. Orfanidis: Signal Processing (2010), pp. 729-733 :cite:`Orfanidis2010`
|
|
287
|
+
- Online discussion: http://www.firstpr.com.au/dsp/pink-noise/
|
|
288
|
+
"""
|
|
124
289
|
|
|
125
|
-
#: "Octave depth"
|
|
126
|
-
#:
|
|
290
|
+
#: "Octave depth" of the pink noise generation. Higher values result in a better approximation
|
|
291
|
+
#: of the :math:`1/f` spectrum at low frequencies but increase computation time. The maximum
|
|
292
|
+
#: allowable value depends on the :attr:`number of samples<SignalGenerator.num_samples>`.
|
|
293
|
+
#: Default is ``16``.
|
|
127
294
|
depth = Int(16, desc='octave depth')
|
|
128
295
|
|
|
129
|
-
|
|
130
|
-
digest = Property(
|
|
131
|
-
depends_on=['rms', 'numsamples', 'sample_freq', 'seed', 'depth', '__class__'],
|
|
132
|
-
)
|
|
296
|
+
#: A unique checksum identifier based on the object properties. (read-only)
|
|
297
|
+
digest = Property(depends_on=['rms', 'seed', 'sample_freq', 'num_samples', 'depth'])
|
|
133
298
|
|
|
134
299
|
@cached_property
|
|
135
300
|
def _get_digest(self):
|
|
136
301
|
return digest(self)
|
|
137
302
|
|
|
138
303
|
def signal(self):
|
|
139
|
-
|
|
304
|
+
"""
|
|
305
|
+
Generate and deliver the pink noise signal.
|
|
306
|
+
|
|
307
|
+
The signal is computed using the Voss-McCartney algorithm, which generates noise
|
|
308
|
+
with a :math:`1/f` power spectral density. The method ensures that the output has the
|
|
309
|
+
desired :attr:`RMS<SignalGenerator.rms>` amplitude and spectrum.
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
-------
|
|
313
|
+
:class:`numpy.ndarray`
|
|
314
|
+
A 1D array of floats containing the generated pink noise signal. The length
|
|
315
|
+
of the array is equal to :attr:`~SignalGenerator.num_samples`.
|
|
316
|
+
|
|
317
|
+
Notes
|
|
318
|
+
-----
|
|
319
|
+
- The "depth" parameter controls the number of octaves included in the pink noise
|
|
320
|
+
simulation. If the specified depth exceeds the maximum possible value based on
|
|
321
|
+
the number of samples, it is automatically adjusted, and a warning is printed.
|
|
322
|
+
- The output signal is scaled to have the same overall level as white noise by dividing
|
|
323
|
+
the result by ``sqrt(depth + 1.5)``.
|
|
324
|
+
"""
|
|
325
|
+
nums = self.num_samples
|
|
140
326
|
depth = self.depth
|
|
141
327
|
# maximum depth depending on number of samples
|
|
142
328
|
max_depth = int(log(nums) / log(2))
|
|
143
329
|
|
|
144
330
|
if depth > max_depth:
|
|
145
331
|
depth = max_depth
|
|
146
|
-
print('Pink noise filter depth set to maximum possible value of
|
|
332
|
+
print(f'Pink noise filter depth set to maximum possible value of {max_depth:d}.')
|
|
147
333
|
|
|
148
334
|
rnd_gen = RandomState(self.seed)
|
|
149
335
|
s = rnd_gen.standard_normal(nums)
|
|
@@ -157,51 +343,112 @@ class PNoiseGenerator(SignalGenerator):
|
|
|
157
343
|
|
|
158
344
|
|
|
159
345
|
class FiltWNoiseGenerator(WNoiseGenerator):
|
|
160
|
-
"""
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
346
|
+
"""
|
|
347
|
+
Generate filtered white noise using an **AR**, **MA**, or **ARMA** process.
|
|
348
|
+
|
|
349
|
+
- **AR:** autoregressive (:attr:`ar`)
|
|
350
|
+
- **MA:** moving-average (:attr:`ma`)
|
|
351
|
+
- **ARMA:** autoregressive moving-average
|
|
352
|
+
|
|
353
|
+
This class extends the :class:`WNoiseGenerator` class to apply a digital filter to white noise,
|
|
354
|
+
producing a signal with specific characteristics based on the provided filter coefficients.
|
|
355
|
+
The desired filter is defined using the autoregressive coefficients (:attr:`ar`) and
|
|
356
|
+
moving-average coefficients (:attr:`ma`).
|
|
357
|
+
|
|
358
|
+
The filter is implemented as a series of second-order sections (sos) for numerical stability,
|
|
359
|
+
especially at high filter orders.
|
|
360
|
+
|
|
361
|
+
See Also
|
|
362
|
+
--------
|
|
363
|
+
:class:`WNoiseGenerator` : For white noise generation.
|
|
364
|
+
|
|
365
|
+
Notes
|
|
366
|
+
-----
|
|
367
|
+
- The output signal is adjusted for the group delay introduced by the filter, ensuring
|
|
368
|
+
proper alignment of the filtered signal.
|
|
369
|
+
- The RMS value specified in the :attr:`~NoiseGenerator.rms` attribute corresponds to the
|
|
370
|
+
original white noise signal and not the filtered output.
|
|
371
|
+
|
|
372
|
+
Examples
|
|
373
|
+
--------
|
|
374
|
+
Generate filtered white noise using :attr:`AR<ar>` and :attr:`MA<ma>` coefficients:
|
|
375
|
+
|
|
376
|
+
>>> import acoular as ac
|
|
377
|
+
>>> import numpy as np
|
|
378
|
+
>>>
|
|
379
|
+
>>> # Define AR and MA coefficients
|
|
380
|
+
>>> ar = np.array([1.0, -0.5])
|
|
381
|
+
>>> ma = np.array([0.5, 0.5])
|
|
382
|
+
>>>
|
|
383
|
+
>>> # Create generator
|
|
384
|
+
>>> gen = ac.FiltWNoiseGenerator(
|
|
385
|
+
... rms=1.0,
|
|
386
|
+
... seed=42,
|
|
387
|
+
... sample_freq=1000,
|
|
388
|
+
... num_samples=10000,
|
|
389
|
+
... ar=ar,
|
|
390
|
+
... ma=ma,
|
|
391
|
+
... )
|
|
392
|
+
>>>
|
|
393
|
+
>>> # Generate signal
|
|
394
|
+
>>> signal = gen.signal()
|
|
395
|
+
>>> print(signal[:10]) # Print the first 10 samples
|
|
396
|
+
[0.24835708 0.30340346 0.40641385 1.28856612 1.2887213 0.41021549
|
|
397
|
+
0.87764567 1.61214661 0.95505348 0.51406957]
|
|
169
398
|
"""
|
|
170
399
|
|
|
400
|
+
#: A :class:`numpy.ndarray` of autoregressive coefficients (denominator). Default is ``[]``,
|
|
401
|
+
#: which results in no AR filtering (i.e., all-pole filter is ``[1.0]``).
|
|
171
402
|
ar = CArray(value=array([]), dtype=float, desc='autoregressive coefficients (coefficients of the denominator)')
|
|
172
403
|
|
|
404
|
+
#: A :class:`numpy.ndarray` of moving-average coefficients (numerator). Default is ``[]``,
|
|
405
|
+
#: which results in no MA filtering (i.e., all-zero filter is ``[1.0]``).
|
|
173
406
|
ma = CArray(value=array([]), dtype=float, desc='moving-average coefficients (coefficients of the numerator)')
|
|
174
407
|
|
|
175
|
-
|
|
176
|
-
digest = Property(
|
|
177
|
-
depends_on=[
|
|
178
|
-
'ar',
|
|
179
|
-
'ma',
|
|
180
|
-
'rms',
|
|
181
|
-
'numsamples',
|
|
182
|
-
'sample_freq',
|
|
183
|
-
'seed',
|
|
184
|
-
'__class__',
|
|
185
|
-
],
|
|
186
|
-
)
|
|
408
|
+
#: A unique checksum identifier based on the object properties. (read-only)
|
|
409
|
+
digest = Property(depends_on=['rms', 'seed', 'sample_freq', 'num_samples', 'ar', 'ma'])
|
|
187
410
|
|
|
188
411
|
@cached_property
|
|
189
412
|
def _get_digest(self):
|
|
190
413
|
return digest(self)
|
|
191
414
|
|
|
192
415
|
def handle_empty_coefficients(self, coefficients):
|
|
416
|
+
"""
|
|
417
|
+
Handle empty filter coefficient arrays by returning a default value.
|
|
418
|
+
|
|
419
|
+
This method ensures that both the autoregressive (:attr:`ar`) and moving-average
|
|
420
|
+
(:attr:`ma`) coefficients are non-empty before filtering. If a coefficient array is empty,
|
|
421
|
+
it is replaced with a default array containing a single value of ``1.0``.
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
coefficients : :class:`numpy.ndarray`
|
|
426
|
+
Array of filter coefficients to check.
|
|
427
|
+
|
|
428
|
+
Returns
|
|
429
|
+
-------
|
|
430
|
+
:class:`numpy.ndarray`
|
|
431
|
+
The original array if it is non-empty, or a default array containing ``[1.0]``
|
|
432
|
+
if the input array is empty.
|
|
433
|
+
"""
|
|
193
434
|
if coefficients.size == 0:
|
|
194
435
|
return array([1.0])
|
|
195
436
|
return coefficients
|
|
196
437
|
|
|
197
438
|
def signal(self):
|
|
198
|
-
"""
|
|
439
|
+
"""
|
|
440
|
+
Generate and return the filtered white noise signal.
|
|
441
|
+
|
|
442
|
+
This method creates a white noise signal with the specified
|
|
443
|
+
:attr:`RMS value<NoiseGenerator.rms>` and :attr:`~NoiseGenerator.seed`, then filters it
|
|
444
|
+
using the autoregressive (:attr:`ar`) and moving-average (:attr:`ma`) coefficients.
|
|
445
|
+
The filtering process compensates for group delay introduced by the filter.
|
|
199
446
|
|
|
200
447
|
Returns
|
|
201
448
|
-------
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
449
|
+
:class:`numpy.ndarray` of :class:`floats<float>`
|
|
450
|
+
An array representing the filtered white noise signal. The length of the returned array
|
|
451
|
+
is equal to :attr:`the number of samples<SignalGenerator.num_samples>`.
|
|
205
452
|
"""
|
|
206
453
|
rnd_gen = RandomState(self.seed)
|
|
207
454
|
ma = self.handle_empty_coefficients(self.ma)
|
|
@@ -210,117 +457,169 @@ class FiltWNoiseGenerator(WNoiseGenerator):
|
|
|
210
457
|
ntaps = ma.shape[0]
|
|
211
458
|
sdelay = round(0.5 * (ntaps - 1))
|
|
212
459
|
wnoise = self.rms * rnd_gen.standard_normal(
|
|
213
|
-
self.
|
|
460
|
+
self.num_samples + sdelay,
|
|
214
461
|
) # create longer signal to compensate delay
|
|
215
462
|
return sosfilt(sos, x=wnoise)[sdelay:]
|
|
216
463
|
|
|
217
464
|
|
|
218
|
-
class SineGenerator(
|
|
219
|
-
"""
|
|
465
|
+
class SineGenerator(PeriodicSignalGenerator):
|
|
466
|
+
r"""
|
|
467
|
+
Generate a sine signal.
|
|
220
468
|
|
|
221
|
-
|
|
222
|
-
|
|
469
|
+
The :class:`SineGenerator` class extends the :class:`PeriodicSignalGenerator` class and
|
|
470
|
+
generates a sinusoidal signal based on specified :attr:`~PeriodicSignalGenerator.amplitude`,
|
|
471
|
+
:attr:`frequency<PeriodicSignalGenerator.freq>`, and :attr:`~PeriodicSignalGenerator.phase`.
|
|
223
472
|
|
|
224
|
-
|
|
225
|
-
|
|
473
|
+
This generator is commonly used for creating test signals in signal processing, acoustics,
|
|
474
|
+
and control systems.
|
|
226
475
|
|
|
227
|
-
|
|
228
|
-
# Do not set directly.
|
|
229
|
-
_amp = Float(1.0)
|
|
476
|
+
The signal is defined as
|
|
230
477
|
|
|
231
|
-
|
|
232
|
-
#: Deprecated. For amplitude use :attr:`amplitude`.
|
|
233
|
-
rms = Property(desc='rms amplitude')
|
|
478
|
+
.. math::
|
|
234
479
|
|
|
235
|
-
|
|
236
|
-
return self._amp / 2**0.5
|
|
480
|
+
s(t) = A \sin(2 \pi f t + \phi)
|
|
237
481
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
stacklevel=2,
|
|
246
|
-
)
|
|
247
|
-
self._amp = rms * 2**0.5
|
|
482
|
+
where:
|
|
483
|
+
- :math:`A` is the amplitude,
|
|
484
|
+
- :math:`f` is the frequency,
|
|
485
|
+
- :math:`\phi` is the phase,
|
|
486
|
+
- :math:`t` is the time (computed from the
|
|
487
|
+
:attr:`sampling frequency<SignalGenerator.sample_freq>` and the
|
|
488
|
+
:attr:`number of samples<SignalGenerator.num_samples>`).
|
|
248
489
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
490
|
+
See Also
|
|
491
|
+
--------
|
|
492
|
+
PeriodicSignalGenerator : Base class for periodic signal generators.
|
|
252
493
|
|
|
253
|
-
|
|
254
|
-
|
|
494
|
+
Examples
|
|
495
|
+
--------
|
|
496
|
+
Generate a sine wave signal:
|
|
255
497
|
|
|
256
|
-
|
|
257
|
-
|
|
498
|
+
>>> import acoular as ac
|
|
499
|
+
>>>
|
|
500
|
+
>>> gen = ac.SineGenerator(
|
|
501
|
+
... amplitude=2.0,
|
|
502
|
+
... freq=50.0,
|
|
503
|
+
... phase=0.0,
|
|
504
|
+
... num_samples=1000,
|
|
505
|
+
... sample_freq=1000,
|
|
506
|
+
... )
|
|
507
|
+
>>> signal = gen.signal()
|
|
508
|
+
>>> signal[:5] # The first 5 samples
|
|
509
|
+
array([0. , 0.61803399, 1.1755705 , 1.61803399, 1.90211303])
|
|
258
510
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
511
|
+
Generate a sine wave with a phase shift (arguably a cosine wave):
|
|
512
|
+
|
|
513
|
+
>>> import numpy as np
|
|
514
|
+
>>>
|
|
515
|
+
>>> gen = ac.SineGenerator(
|
|
516
|
+
... amplitude=1.0,
|
|
517
|
+
... freq=100.0,
|
|
518
|
+
... phase=np.pi / 2,
|
|
519
|
+
... num_samples=500,
|
|
520
|
+
... sample_freq=2000,
|
|
521
|
+
... )
|
|
522
|
+
>>> signal = gen.signal()
|
|
523
|
+
>>> signal[:5] # The first 5 samples
|
|
524
|
+
array([1. , 0.95105652, 0.80901699, 0.58778525, 0.30901699])
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
#: A unique checksum identifier based on the object properties. (read-only)
|
|
528
|
+
digest = Property(depends_on=['num_samples', 'sample_freq', 'amplitude', 'freq', 'phase'])
|
|
263
529
|
|
|
264
530
|
@cached_property
|
|
265
531
|
def _get_digest(self):
|
|
266
532
|
return digest(self)
|
|
267
533
|
|
|
268
534
|
def signal(self):
|
|
269
|
-
"""
|
|
535
|
+
r"""
|
|
536
|
+
Generate and return the sine wave signal.
|
|
537
|
+
|
|
538
|
+
The method computes the sine wave based on the specified
|
|
539
|
+
:attr:`~PeriodicSignalGenerator.amplitude`, :attr:`frequency<PeriodicSignalGenerator.freq>`,
|
|
540
|
+
and :attr:`~PeriodicSignalGenerator.phase`. The time values are determined by the
|
|
541
|
+
:attr:`sampling frequency<SignalGenerator.sample_freq>` and the
|
|
542
|
+
:attr:`number of samples<SignalGenerator.num_samples>`.
|
|
270
543
|
|
|
271
544
|
Returns
|
|
272
545
|
-------
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
546
|
+
:class:`numpy.ndarray` of :class:`floats<float>`
|
|
547
|
+
A 1D array representing the sine wave signal.
|
|
548
|
+
The length of the array is equal to :attr:`~SignalGenerator.num_samples`.
|
|
549
|
+
|
|
550
|
+
Notes
|
|
551
|
+
-----
|
|
552
|
+
The generator supports high-frequency and high-resolution signals,
|
|
553
|
+
limited by the Nyquist criterion.
|
|
276
554
|
"""
|
|
277
|
-
t = arange(self.
|
|
555
|
+
t = arange(self.num_samples, dtype=float) / self.sample_freq
|
|
278
556
|
return self.amplitude * sin(2 * pi * self.freq * t + self.phase)
|
|
279
557
|
|
|
280
558
|
|
|
559
|
+
@deprecated_alias({'rms': 'amplitude'})
|
|
281
560
|
class GenericSignalGenerator(SignalGenerator):
|
|
282
|
-
"""
|
|
561
|
+
"""
|
|
562
|
+
Generate signals from a :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
563
|
+
|
|
564
|
+
The :class:`GenericSignalGenerator` class enables the integration of arbitrary signals into
|
|
565
|
+
Acoular processing chains. The signal is fetched from a specified data source and optionally
|
|
566
|
+
scaled by an amplitude factor. It supports looping the signal to match the desired number of
|
|
567
|
+
samples and can handle signals with multiple channels (only the first channel is used).
|
|
283
568
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
569
|
+
Common use cases include:
|
|
570
|
+
- Injecting custom or pre-recorded signals from HDF5 files.
|
|
571
|
+
- Creating signals using the :class:`~acoular.sources.TimeSamples` class.
|
|
572
|
+
- Generating a continuous or repeated signal for simulations.
|
|
287
573
|
|
|
288
|
-
|
|
289
|
-
|
|
574
|
+
Notes
|
|
575
|
+
-----
|
|
576
|
+
If the signal source has more than one channel, only channel 0 is used.
|
|
577
|
+
|
|
578
|
+
Examples
|
|
579
|
+
--------
|
|
580
|
+
Inject a random signal into a processing chain:
|
|
581
|
+
|
|
582
|
+
>>> import acoular as ac
|
|
290
583
|
>>> import numpy as np
|
|
291
|
-
>>>
|
|
584
|
+
>>>
|
|
292
585
|
>>> data = np.random.rand(1000, 1)
|
|
293
|
-
>>> ts = TimeSamples(data=data, sample_freq=51200)
|
|
294
|
-
>>> sig = GenericSignalGenerator(source=ts)
|
|
295
|
-
|
|
586
|
+
>>> ts = ac.TimeSamples(data=data, sample_freq=51200)
|
|
587
|
+
>>> sig = ac.GenericSignalGenerator(source=ts)
|
|
588
|
+
>>> output_signal = sig.signal()
|
|
296
589
|
"""
|
|
297
590
|
|
|
298
|
-
#:
|
|
299
|
-
|
|
591
|
+
#: The data source from which the signal is fetched.
|
|
592
|
+
#: This can be any object derived from :class:`SamplesGenerator`.
|
|
593
|
+
source = Instance(SamplesGenerator)
|
|
594
|
+
|
|
595
|
+
#: Scaling factor applied to the generated signal. Defaults to ``1.0``.
|
|
596
|
+
amplitude = Float(1.0)
|
|
300
597
|
|
|
301
|
-
#: Sampling frequency of output signal, as
|
|
598
|
+
#: Sampling frequency of the output signal, as provided by the :attr:`source` object.
|
|
302
599
|
sample_freq = Delegate('source')
|
|
303
600
|
|
|
304
|
-
|
|
601
|
+
_num_samples = CInt(0)
|
|
305
602
|
|
|
306
|
-
#:
|
|
307
|
-
|
|
603
|
+
#: The number of samples to generate. Default is the number of samples available in the
|
|
604
|
+
#: :attr:`source` (``source.num_samples``). If set explicitly, it can exceed the source length,
|
|
605
|
+
#: in which case the signal will loop if :attr:`loop_signal` is ``True``.
|
|
606
|
+
num_samples = Property()
|
|
308
607
|
|
|
309
|
-
def
|
|
310
|
-
if self.
|
|
311
|
-
return self.
|
|
312
|
-
return self.source.
|
|
608
|
+
def _get_num_samples(self):
|
|
609
|
+
if self._num_samples:
|
|
610
|
+
return self._num_samples
|
|
611
|
+
return self.source.num_samples
|
|
313
612
|
|
|
314
|
-
def
|
|
315
|
-
self.
|
|
613
|
+
def _set_num_samples(self, num_samples):
|
|
614
|
+
self._num_samples = num_samples
|
|
316
615
|
|
|
317
|
-
#:
|
|
318
|
-
#:
|
|
616
|
+
#: If ``True`` (default), the signal is repeated to meet the requested :attr:`num_samples`.
|
|
617
|
+
#: If ``False``, the signal stops once the source data is exhausted.
|
|
319
618
|
loop_signal = Bool(True)
|
|
320
619
|
|
|
321
|
-
|
|
620
|
+
#: A unique checksum identifier based on the object properties. (read-only)
|
|
322
621
|
digest = Property(
|
|
323
|
-
depends_on=['source.digest', 'loop_signal', '
|
|
622
|
+
depends_on=['source.digest', 'loop_signal', 'num_samples', 'amplitude'],
|
|
324
623
|
)
|
|
325
624
|
|
|
326
625
|
@cached_property
|
|
@@ -328,22 +627,27 @@ class GenericSignalGenerator(SignalGenerator):
|
|
|
328
627
|
return digest(self)
|
|
329
628
|
|
|
330
629
|
def signal(self):
|
|
331
|
-
"""
|
|
630
|
+
"""
|
|
631
|
+
Deliver the signal from the specified source.
|
|
332
632
|
|
|
333
633
|
Returns
|
|
334
634
|
-------
|
|
335
|
-
array of floats
|
|
336
|
-
The resulting signal
|
|
635
|
+
:class:`numpy.array` of :class:`floats<float>`
|
|
636
|
+
The resulting signal, scaled by the :attr:`amplitude` attribute, with a length
|
|
637
|
+
matching :attr:`~GenericSignalGenerator.num_samples`.
|
|
337
638
|
|
|
639
|
+
Warnings
|
|
640
|
+
--------
|
|
641
|
+
A warning is raised if the source has more than one channel.
|
|
338
642
|
"""
|
|
339
643
|
block = 1024
|
|
340
|
-
if self.source.
|
|
644
|
+
if self.source.num_channels > 1:
|
|
341
645
|
warn(
|
|
342
646
|
'Signal source has more than one channel. Only channel 0 will be used for signal.',
|
|
343
647
|
Warning,
|
|
344
648
|
stacklevel=2,
|
|
345
649
|
)
|
|
346
|
-
nums = self.
|
|
650
|
+
nums = self.num_samples
|
|
347
651
|
track = zeros(nums)
|
|
348
652
|
|
|
349
653
|
# iterate through source generator to fill signal track
|
|
@@ -366,6 +670,4 @@ class GenericSignalGenerator(SignalGenerator):
|
|
|
366
670
|
res = nums % stop # last part of unfinished loop
|
|
367
671
|
if res > 0:
|
|
368
672
|
track[stop * nloops :] = track[:res]
|
|
369
|
-
|
|
370
|
-
# The rms value is just an amplification here
|
|
371
|
-
return self.rms * track
|
|
673
|
+
return self.amplitude * track
|