acoular 24.7__py3-none-any.whl → 25.1__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 +21 -9
- acoular/aiaa/__init__.py +12 -0
- acoular/{tools → aiaa}/aiaa.py +26 -31
- acoular/base.py +332 -0
- acoular/calib.py +129 -34
- acoular/configuration.py +13 -11
- acoular/demo/__init__.py +1 -0
- acoular/demo/acoular_demo.py +30 -17
- acoular/deprecation.py +85 -0
- acoular/environments.py +38 -24
- acoular/fastFuncs.py +90 -84
- acoular/fbeamform.py +342 -387
- acoular/fprocess.py +376 -0
- acoular/grids.py +122 -150
- acoular/h5cache.py +29 -40
- acoular/h5files.py +2 -6
- acoular/microphones.py +50 -59
- acoular/process.py +771 -0
- acoular/sdinput.py +35 -21
- acoular/signals.py +120 -113
- acoular/sources.py +208 -234
- acoular/spectra.py +59 -254
- acoular/tbeamform.py +280 -280
- acoular/tfastfuncs.py +21 -21
- acoular/tools/__init__.py +3 -7
- acoular/tools/helpers.py +218 -4
- acoular/tools/metrics.py +5 -5
- acoular/tools/utils.py +116 -0
- acoular/tprocess.py +416 -741
- acoular/traitsviews.py +15 -13
- acoular/trajectory.py +7 -10
- acoular/version.py +2 -2
- {acoular-24.7.dist-info → acoular-25.1.dist-info}/METADATA +63 -21
- acoular-25.1.dist-info/RECORD +56 -0
- {acoular-24.7.dist-info → acoular-25.1.dist-info}/WHEEL +1 -1
- acoular-24.7.dist-info/RECORD +0 -50
- {acoular-24.7.dist-info → acoular-25.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {acoular-24.7.dist-info → acoular-25.1.dist-info}/licenses/LICENSE +0 -0
acoular/fprocess.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# ------------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) Acoular Development Team.
|
|
3
|
+
# ------------------------------------------------------------------------------
|
|
4
|
+
"""Implements blockwise processing methods in the frequency domain.
|
|
5
|
+
|
|
6
|
+
.. autosummary::
|
|
7
|
+
:toctree: generated/
|
|
8
|
+
|
|
9
|
+
RFFT
|
|
10
|
+
IRFFT
|
|
11
|
+
AutoPowerSpectra
|
|
12
|
+
CrossPowerSpectra
|
|
13
|
+
FFTSpectra
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from warnings import warn
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
from scipy import fft
|
|
20
|
+
from traits.api import Bool, CArray, Enum, Instance, Int, Property, Union, cached_property
|
|
21
|
+
|
|
22
|
+
# acoular imports
|
|
23
|
+
from .base import SamplesGenerator, SpectraGenerator, SpectraOut, TimeOut
|
|
24
|
+
from .deprecation import deprecated_alias
|
|
25
|
+
from .fastFuncs import calcCSM
|
|
26
|
+
from .internal import digest
|
|
27
|
+
from .process import SamplesBuffer
|
|
28
|
+
from .spectra import BaseSpectra
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@deprecated_alias({'numfreqs': 'num_freqs', 'numsamples': 'num_samples'}, read_only=True)
|
|
32
|
+
class RFFT(BaseSpectra, SpectraOut):
|
|
33
|
+
"""Provides the one-sided Fast Fourier Transform (FFT) for real-valued multichannel time data.
|
|
34
|
+
|
|
35
|
+
The FFT is calculated block-wise, i.e. the input data is divided into blocks of length
|
|
36
|
+
:attr:`block_size` and the FFT is calculated for each block. Optionally, a window function
|
|
37
|
+
can be applied to the data before the FFT calculation via the :attr:`window` attribute.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
#: Data source; :class:`~acoular.base.SamplesGenerator` or derived object.
|
|
41
|
+
source = Instance(SamplesGenerator)
|
|
42
|
+
|
|
43
|
+
#: Number of workers to use for the FFT calculation. If negative values are used,
|
|
44
|
+
#: all available logical CPUs will be considered (``scipy.fft.rfft`` implementation wraps around
|
|
45
|
+
#: from ``os.cpu_count()``).
|
|
46
|
+
#: Default is `None` (handled by scipy)
|
|
47
|
+
workers = Union(Int(), None, default_value=None, desc='number of workers to use')
|
|
48
|
+
|
|
49
|
+
#: Scaling method, either 'amplitude', 'energy' or :code:`none`.
|
|
50
|
+
#: Default is :code:`none`.
|
|
51
|
+
#: 'energy': compensates for the energy loss due to truncation of the FFT result. The resulting
|
|
52
|
+
#: one-sided spectrum is multiplied by 2.0, except for the DC and Nyquist frequency.
|
|
53
|
+
#: 'amplitude': scales the one-sided spectrum so that the amplitude of discrete tones does not
|
|
54
|
+
#: depend on the block size.
|
|
55
|
+
scaling = Enum('none', 'energy', 'amplitude')
|
|
56
|
+
|
|
57
|
+
#: block size of the FFT. Default is 1024.
|
|
58
|
+
block_size = Property()
|
|
59
|
+
|
|
60
|
+
#: Number of frequencies in the output.
|
|
61
|
+
num_freqs = Property(depends_on=['_block_size'])
|
|
62
|
+
|
|
63
|
+
#: Number of snapshots in the output.
|
|
64
|
+
num_samples = Property(depends_on=['source.num_samples', '_block_size'])
|
|
65
|
+
|
|
66
|
+
#: 1-D array of FFT sample frequencies.
|
|
67
|
+
freqs = Property()
|
|
68
|
+
|
|
69
|
+
# internal block size variable
|
|
70
|
+
_block_size = Int(1024, desc='block size of the FFT')
|
|
71
|
+
|
|
72
|
+
# internal identifier
|
|
73
|
+
digest = Property(depends_on=['source.digest', 'scaling', 'precision', '_block_size', 'window', 'overlap'])
|
|
74
|
+
|
|
75
|
+
@cached_property
|
|
76
|
+
def _get_digest(self):
|
|
77
|
+
return digest(self)
|
|
78
|
+
|
|
79
|
+
@cached_property
|
|
80
|
+
def _get_num_freqs(self):
|
|
81
|
+
return int(self.block_size / 2 + 1)
|
|
82
|
+
|
|
83
|
+
@cached_property
|
|
84
|
+
def _get_num_samples(self):
|
|
85
|
+
if self.source.num_samples >= 0:
|
|
86
|
+
return int(np.floor(self.source.num_samples / self.block_size))
|
|
87
|
+
return -1
|
|
88
|
+
|
|
89
|
+
def _get_block_size(self):
|
|
90
|
+
return self._block_size
|
|
91
|
+
|
|
92
|
+
def _set_block_size(self, value):
|
|
93
|
+
if value % 2 != 0:
|
|
94
|
+
msg = 'Block size must be even'
|
|
95
|
+
raise ValueError(msg)
|
|
96
|
+
self._block_size = value
|
|
97
|
+
|
|
98
|
+
def _scale(self, data, scaling_value):
|
|
99
|
+
"""Corrects the energy of the one-sided FFT data."""
|
|
100
|
+
if self.scaling == 'amplitude' or self.scaling == 'energy':
|
|
101
|
+
data[1:-1] *= 2.0
|
|
102
|
+
data *= scaling_value
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
def _get_freqs(self):
|
|
106
|
+
"""Return the Discrete Fourier Transform sample frequencies.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
f : ndarray
|
|
111
|
+
1-D Array of length *block_size/2+1* containing the sample frequencies.
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
if self.source is not None:
|
|
115
|
+
return abs(fft.fftfreq(self.block_size, 1.0 / self.source.sample_freq)[: int(self.block_size / 2 + 1)])
|
|
116
|
+
return np.array([])
|
|
117
|
+
|
|
118
|
+
def result(self, num=1):
|
|
119
|
+
"""Python generator that yields the output block-wise.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
num : integer
|
|
124
|
+
This parameter defines the number of multi-channel spectra (i.e. snapshots) per block
|
|
125
|
+
returned by the generator.
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
Spectra block of shape (num, :attr:`num_channels` * :attr:`num_freqs`).
|
|
130
|
+
The last block may be shorter than num.
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
wind = self.window_(self.block_size)
|
|
134
|
+
if self.scaling == 'none' or self.scaling == 'energy': # only compensate for the window
|
|
135
|
+
svalue = 1 / np.sqrt(np.dot(wind, wind) / self.block_size)
|
|
136
|
+
elif self.scaling == 'amplitude': # compensates for the window and the energy loss
|
|
137
|
+
svalue = 1 / wind.sum()
|
|
138
|
+
wind = wind[:, np.newaxis]
|
|
139
|
+
fftdata = np.zeros((num, self.num_channels * self.num_freqs), dtype=self.precision)
|
|
140
|
+
j = 0
|
|
141
|
+
for i, data in enumerate(self._get_source_data()): # yields one block of time data
|
|
142
|
+
j = i % num
|
|
143
|
+
fftdata[j] = self._scale(
|
|
144
|
+
fft.rfft(data * wind, n=self.block_size, axis=0, workers=self.workers).astype(self.precision),
|
|
145
|
+
scaling_value=svalue,
|
|
146
|
+
).reshape(-1)
|
|
147
|
+
if j == num - 1:
|
|
148
|
+
yield fftdata
|
|
149
|
+
if j < num - 1: # yield remaining fft spectra
|
|
150
|
+
yield fftdata[: j + 1]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@deprecated_alias({'numsamples': 'num_samples'}, read_only=True)
|
|
154
|
+
class IRFFT(TimeOut):
|
|
155
|
+
"""Calculates the inverse Fast Fourier Transform (IFFT) for one-sided multi-channel spectra."""
|
|
156
|
+
|
|
157
|
+
#: Data source; :class:`~acoular.base.SpectraGenerator` or derived object.
|
|
158
|
+
source = Instance(SpectraGenerator)
|
|
159
|
+
|
|
160
|
+
#: Number of workers to use for the FFT calculation. If negative values are used,
|
|
161
|
+
#: all available logical CPUs will be considered (``scipy.fft.rfft`` implementation wraps around
|
|
162
|
+
#: from ``os.cpu_count()``).
|
|
163
|
+
#: Default is `None` (handled by scipy)
|
|
164
|
+
workers = Union(Int(), None, default_value=None, desc='number of workers to use')
|
|
165
|
+
|
|
166
|
+
#: The floating-number-precision of the resulting time signals, corresponding to numpy dtypes.
|
|
167
|
+
#: Default is 64 bit.
|
|
168
|
+
precision = Enum('float64', 'float32', desc='precision of the time signal after the ifft')
|
|
169
|
+
|
|
170
|
+
#: Number of time samples in the output.
|
|
171
|
+
num_samples = Property(depends_on=['source.num_samples', 'source._block_size'])
|
|
172
|
+
|
|
173
|
+
# internal time signal buffer to handle arbitrary output block sizes
|
|
174
|
+
_buffer = CArray(desc='signal buffer')
|
|
175
|
+
|
|
176
|
+
# internal identifier
|
|
177
|
+
digest = Property(depends_on=['source.digest', 'scaling', 'precision', '_block_size', 'window', 'overlap'])
|
|
178
|
+
|
|
179
|
+
def _get_num_samples(self):
|
|
180
|
+
if self.source.num_samples >= 0:
|
|
181
|
+
return int(self.source.num_samples * self.source.block_size)
|
|
182
|
+
return -1
|
|
183
|
+
|
|
184
|
+
@cached_property
|
|
185
|
+
def _get_digest(self):
|
|
186
|
+
return digest(self)
|
|
187
|
+
|
|
188
|
+
def _validate(self):
|
|
189
|
+
if not self.source.block_size or self.source.block_size < 0:
|
|
190
|
+
msg = (
|
|
191
|
+
f'Source of class {self.__class__.__name__} has an unknown blocksize: {self.source.block_size}.'
|
|
192
|
+
'This is likely due to incomplete spectral data from which the inverse FFT cannot be calculated.'
|
|
193
|
+
)
|
|
194
|
+
raise ValueError(msg)
|
|
195
|
+
if (self.source.num_freqs - 1) * 2 != self.source.block_size:
|
|
196
|
+
msg = (
|
|
197
|
+
f'Block size must be 2*(num_freqs-1) but is {self.source.block_size}.'
|
|
198
|
+
'This is likely due to incomplete spectral data from which the inverse FFT cannot be calculated.'
|
|
199
|
+
)
|
|
200
|
+
raise ValueError(msg)
|
|
201
|
+
if self.source.block_size % 2 != 0:
|
|
202
|
+
msg = f'Block size must be even but is {self.source.block_size}.'
|
|
203
|
+
raise ValueError(msg)
|
|
204
|
+
|
|
205
|
+
def result(self, num):
|
|
206
|
+
"""Python generator that yields the output block-wise.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
num : integer
|
|
211
|
+
This parameter defines the size of the blocks to be yielded
|
|
212
|
+
(i.e. the number of samples per block). The last block may be shorter than num.
|
|
213
|
+
|
|
214
|
+
Yields
|
|
215
|
+
------
|
|
216
|
+
numpy.ndarray
|
|
217
|
+
Yields blocks of shape (num, num_channels).
|
|
218
|
+
"""
|
|
219
|
+
self._validate()
|
|
220
|
+
bs = self.source.block_size
|
|
221
|
+
if num != bs:
|
|
222
|
+
buffer_length = (int(np.ceil(num / bs)) + 1) * bs
|
|
223
|
+
buffer = SamplesBuffer(source=self, source_num=bs, length=buffer_length, dtype=self.precision)
|
|
224
|
+
yield from buffer.result(num)
|
|
225
|
+
else:
|
|
226
|
+
for spectra in self.source.result(1):
|
|
227
|
+
yield fft.irfft(
|
|
228
|
+
spectra.reshape(self.source.num_freqs, self.num_channels), n=num, axis=0, workers=self.workers
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class AutoPowerSpectra(SpectraOut):
|
|
233
|
+
"""Calculates the real-valued auto-power spectra."""
|
|
234
|
+
|
|
235
|
+
#: Data source; :class:`~acoular.base.SpectraGenerator` or derived object.
|
|
236
|
+
source = Instance(SpectraGenerator)
|
|
237
|
+
|
|
238
|
+
#: Scaling method, either 'power' or 'psd' (Power Spectral Density).
|
|
239
|
+
#: Only relevant if the source is a :class:`~acoular.fprocess.FreqInOut` object.
|
|
240
|
+
scaling = Enum('power', 'psd')
|
|
241
|
+
|
|
242
|
+
#: Determines if the spectra yielded by the source are single-sided spectra.
|
|
243
|
+
single_sided = Bool(True, desc='single sided spectrum')
|
|
244
|
+
|
|
245
|
+
#: The floating-number-precision of entries, corresponding to numpy dtypes. Default is 64 bit.
|
|
246
|
+
precision = Enum('float64', 'float32', desc='floating-number-precision')
|
|
247
|
+
|
|
248
|
+
# internal identifier
|
|
249
|
+
digest = Property(depends_on=['source.digest', 'precision', 'scaling', 'single_sided'])
|
|
250
|
+
|
|
251
|
+
@cached_property
|
|
252
|
+
def _get_digest(self):
|
|
253
|
+
return digest(self)
|
|
254
|
+
|
|
255
|
+
def _get_scaling_value(self):
|
|
256
|
+
scale = 1 / self.block_size**2
|
|
257
|
+
if self.single_sided:
|
|
258
|
+
scale *= 2
|
|
259
|
+
if self.scaling == 'psd':
|
|
260
|
+
scale *= self.block_size * self.source.sample_freq
|
|
261
|
+
return scale
|
|
262
|
+
|
|
263
|
+
def result(self, num=1):
|
|
264
|
+
"""Python generator that yields the real-valued auto-power spectra.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
num : integer
|
|
269
|
+
This parameter defines the number of snapshots within each output data block.
|
|
270
|
+
|
|
271
|
+
Yields
|
|
272
|
+
------
|
|
273
|
+
numpy.ndarray
|
|
274
|
+
Yields blocks of shape (num, num_channels * num_freqs).
|
|
275
|
+
The last block may be shorter than num.
|
|
276
|
+
|
|
277
|
+
"""
|
|
278
|
+
scale = self._get_scaling_value()
|
|
279
|
+
for temp in self.source.result(num):
|
|
280
|
+
yield ((temp * temp.conjugate()).real * scale).astype(self.precision)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@deprecated_alias({'numchannels': 'num_channels'}, read_only=True)
|
|
284
|
+
class CrossPowerSpectra(AutoPowerSpectra):
|
|
285
|
+
"""Calculates the complex-valued auto- and cross-power spectra.
|
|
286
|
+
|
|
287
|
+
Receives the complex-valued spectra from the source and returns the cross-spectral matrix (CSM)
|
|
288
|
+
in a flattened representation (i.e. the auto- and cross-power spectra are concatenated along the
|
|
289
|
+
last axis). If :attr:`calc_mode` is 'full', the full CSM is calculated, if 'upper', only the
|
|
290
|
+
upper triangle is calculated.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
#: Data source; :class:`~acoular.base.SpectraGenerator` or derived object.
|
|
294
|
+
source = Instance(SpectraGenerator)
|
|
295
|
+
|
|
296
|
+
#: The floating-number-precision of entries of csm, eigenvalues and
|
|
297
|
+
#: eigenvectors, corresponding to numpy dtypes. Default is 64 bit.
|
|
298
|
+
precision = Enum('complex128', 'complex64', desc='precision of the fft')
|
|
299
|
+
|
|
300
|
+
#: Calculation mode, either 'full' or 'upper'.
|
|
301
|
+
#: 'full' calculates the full cross-spectral matrix, 'upper' calculates
|
|
302
|
+
# only the upper triangle. Default is 'full'.
|
|
303
|
+
calc_mode = Enum('full', 'upper', 'lower', desc='calculation mode')
|
|
304
|
+
|
|
305
|
+
#: Number of channels in output. If :attr:`calc_mode` is 'full', then
|
|
306
|
+
#: :attr:`num_channels` is :math:`n^2`, where :math:`n` is the number of
|
|
307
|
+
#: channels in the input. If :attr:`calc_mode` is 'upper', then
|
|
308
|
+
#: :attr:`num_channels` is :math:`n + n(n-1)/2`.
|
|
309
|
+
num_channels = Property(depends_on=['source.num_channels'])
|
|
310
|
+
|
|
311
|
+
# internal identifier
|
|
312
|
+
digest = Property(depends_on=['source.digest', 'precision', 'scaling', 'single_sided', 'calc_mode'])
|
|
313
|
+
|
|
314
|
+
@cached_property
|
|
315
|
+
def _get_num_channels(self):
|
|
316
|
+
n = self.source.num_channels
|
|
317
|
+
return n**2 if self.calc_mode == 'full' else int(n + n * (n - 1) / 2)
|
|
318
|
+
|
|
319
|
+
@cached_property
|
|
320
|
+
def _get_digest(self):
|
|
321
|
+
return digest(self)
|
|
322
|
+
|
|
323
|
+
def result(self, num=1):
|
|
324
|
+
"""Python generator that yields the output block-wise.
|
|
325
|
+
|
|
326
|
+
Parameters
|
|
327
|
+
----------
|
|
328
|
+
num : integer
|
|
329
|
+
This parameter defines the size of the blocks to be yielded
|
|
330
|
+
(i.e. the number of samples per block).
|
|
331
|
+
|
|
332
|
+
Yields
|
|
333
|
+
------
|
|
334
|
+
numpy.ndarray
|
|
335
|
+
Yields blocks of shape (num, num_channels * num_freqs).
|
|
336
|
+
"""
|
|
337
|
+
nc_src = self.source.num_channels
|
|
338
|
+
nc = self.num_channels
|
|
339
|
+
nf = self.num_freqs
|
|
340
|
+
scale = self._get_scaling_value()
|
|
341
|
+
|
|
342
|
+
csm_flat = np.zeros((num, nc * nf), dtype=self.precision)
|
|
343
|
+
csm_upper = np.zeros((nf, nc_src, nc_src), dtype=self.precision)
|
|
344
|
+
for data in self.source.result(num):
|
|
345
|
+
for i in range(data.shape[0]):
|
|
346
|
+
calcCSM(csm_upper, data[i].astype(self.precision).reshape(nf, nc_src))
|
|
347
|
+
if self.calc_mode == 'full':
|
|
348
|
+
csm_lower = csm_upper.conj().transpose(0, 2, 1)
|
|
349
|
+
[np.fill_diagonal(csm_lower[cntFreq, :, :], 0) for cntFreq in range(csm_lower.shape[0])]
|
|
350
|
+
csm_flat[i] = (csm_lower + csm_upper).reshape(-1)
|
|
351
|
+
elif self.calc_mode == 'upper':
|
|
352
|
+
csm_flat[i] = csm_upper[:, :nc].reshape(-1)
|
|
353
|
+
else: # lower
|
|
354
|
+
csm_lower = csm_upper.conj().transpose(0, 2, 1)
|
|
355
|
+
csm_flat[i] = csm_lower[:, :nc].reshape(-1)
|
|
356
|
+
csm_upper[...] = 0 # calcCSM adds cumulative
|
|
357
|
+
yield csm_flat[: i + 1] * scale
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class FFTSpectra(RFFT):
|
|
361
|
+
"""Provides the one-sided Fast Fourier Transform (FFT) for multichannel time data.
|
|
362
|
+
|
|
363
|
+
Alias for :class:`~acoular.fprocess.RFFT`.
|
|
364
|
+
|
|
365
|
+
.. deprecated:: 24.10
|
|
366
|
+
Using :class:`~acoular.fprocess.FFTSpectra` is deprecated and will be removed in Acoular
|
|
367
|
+
version 25.07. Use :class:`~acoular.fprocess.RFFT` instead.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
def __init__(self, *args, **kwargs):
|
|
371
|
+
super().__init__(*args, **kwargs)
|
|
372
|
+
warn(
|
|
373
|
+
'Using FFTSpectra is deprecated and will be removed in Acoular version 25.07. Use class RFFT instead.',
|
|
374
|
+
DeprecationWarning,
|
|
375
|
+
stacklevel=2,
|
|
376
|
+
)
|