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/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
+ )