acoular 24.5__py3-none-any.whl → 24.10__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 +17 -11
- acoular/base.py +312 -0
- acoular/configuration.py +23 -16
- acoular/demo/acoular_demo.py +28 -35
- acoular/environments.py +20 -15
- acoular/fastFuncs.py +40 -40
- acoular/fbeamform.py +1100 -1130
- acoular/fprocess.py +368 -0
- acoular/grids.py +36 -22
- acoular/h5cache.py +34 -34
- acoular/h5files.py +13 -13
- acoular/internal.py +3 -3
- acoular/process.py +464 -0
- acoular/sdinput.py +24 -4
- acoular/signals.py +20 -6
- acoular/sources.py +140 -56
- acoular/spectra.py +34 -53
- acoular/tbeamform.py +264 -142
- acoular/tfastfuncs.py +17 -18
- acoular/tools/__init__.py +2 -0
- acoular/tools/aiaa.py +7 -8
- acoular/tools/helpers.py +2 -2
- acoular/tools/metrics.py +1 -1
- acoular/tools/utils.py +210 -0
- acoular/tprocess.py +168 -532
- acoular/traitsviews.py +5 -3
- acoular/version.py +2 -2
- {acoular-24.5.dist-info → acoular-24.10.dist-info}/METADATA +49 -8
- acoular-24.10.dist-info/RECORD +54 -0
- {acoular-24.5.dist-info → acoular-24.10.dist-info}/WHEEL +1 -1
- acoular-24.5.dist-info/RECORD +0 -50
- {acoular-24.5.dist-info → acoular-24.10.dist-info}/licenses/AUTHORS.rst +0 -0
- {acoular-24.5.dist-info → acoular-24.10.dist-info}/licenses/LICENSE +0 -0
acoular/sources.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# ------------------------------------------------------------------------------
|
|
2
2
|
# Copyright (c) Acoular Development Team.
|
|
3
3
|
# ------------------------------------------------------------------------------
|
|
4
|
-
"""Measured multichannel data
|
|
4
|
+
"""Measured multichannel data management and simulation of acoustic sources.
|
|
5
5
|
|
|
6
6
|
.. autosummary::
|
|
7
7
|
:toctree: generated/
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
# imports from other packages
|
|
24
24
|
|
|
25
|
+
import contextlib
|
|
25
26
|
from os import path
|
|
26
27
|
from warnings import warn
|
|
27
28
|
|
|
@@ -50,7 +51,7 @@ from numpy import (
|
|
|
50
51
|
)
|
|
51
52
|
from numpy import min as npmin
|
|
52
53
|
from numpy.fft import fft, ifft
|
|
53
|
-
from
|
|
54
|
+
from scipy.linalg import norm
|
|
54
55
|
from scipy.special import sph_harm, spherical_jn, spherical_yn
|
|
55
56
|
from traits.api import (
|
|
56
57
|
Any,
|
|
@@ -75,6 +76,8 @@ from traits.api import (
|
|
|
75
76
|
on_trait_change,
|
|
76
77
|
)
|
|
77
78
|
|
|
79
|
+
from .base import SamplesGenerator
|
|
80
|
+
|
|
78
81
|
# acoular imports
|
|
79
82
|
from .calib import Calib
|
|
80
83
|
from .environments import Environment
|
|
@@ -82,7 +85,7 @@ from .h5files import H5FileBase, _get_h5file_class
|
|
|
82
85
|
from .internal import digest, ldigest
|
|
83
86
|
from .microphones import MicGeom
|
|
84
87
|
from .signals import SignalGenerator
|
|
85
|
-
from .tprocess import
|
|
88
|
+
from .tprocess import TimeConvolve
|
|
86
89
|
from .trajectory import Trajectory
|
|
87
90
|
|
|
88
91
|
|
|
@@ -104,7 +107,7 @@ def _fill_mic_signal_block(out, signal, rm, ind, blocksize, numchannels, up, pre
|
|
|
104
107
|
return out
|
|
105
108
|
|
|
106
109
|
|
|
107
|
-
def spherical_hn1(n, z
|
|
110
|
+
def spherical_hn1(n, z):
|
|
108
111
|
"""Spherical Hankel Function of the First Kind."""
|
|
109
112
|
return spherical_jn(n, z, derivative=False) + 1j * spherical_yn(n, z, derivative=False)
|
|
110
113
|
|
|
@@ -146,7 +149,7 @@ def get_radiation_angles(direction, mpos, sourceposition):
|
|
|
146
149
|
return azi, ele
|
|
147
150
|
|
|
148
151
|
|
|
149
|
-
def get_modes(lOrder, direction, mpos, sourceposition=None):
|
|
152
|
+
def get_modes(lOrder, direction, mpos, sourceposition=None): # noqa: N803
|
|
150
153
|
"""Returns Spherical Harmonic Radiation Pattern at the Microphones.
|
|
151
154
|
|
|
152
155
|
Parameters
|
|
@@ -170,9 +173,9 @@ def get_modes(lOrder, direction, mpos, sourceposition=None):
|
|
|
170
173
|
azi, ele = get_radiation_angles(direction, mpos, sourceposition) # angles between source and mics
|
|
171
174
|
modes = zeros((azi.shape[0], (lOrder + 1) ** 2), dtype=complex128)
|
|
172
175
|
i = 0
|
|
173
|
-
for
|
|
174
|
-
for m in range(-
|
|
175
|
-
modes[:, i] = sph_harm(m,
|
|
176
|
+
for lidx in range(lOrder + 1):
|
|
177
|
+
for m in range(-lidx, lidx + 1):
|
|
178
|
+
modes[:, i] = sph_harm(m, lidx, azi, ele)
|
|
176
179
|
if m < 0:
|
|
177
180
|
modes[:, i] = modes[:, i].conj() * 1j
|
|
178
181
|
i += 1
|
|
@@ -180,12 +183,44 @@ def get_modes(lOrder, direction, mpos, sourceposition=None):
|
|
|
180
183
|
|
|
181
184
|
|
|
182
185
|
class TimeSamples(SamplesGenerator):
|
|
183
|
-
"""Container for time data in `*.h5` format.
|
|
184
|
-
|
|
185
|
-
This class loads measured data from
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
"""Container for processing time data in `*.h5` or NumPy array format.
|
|
187
|
+
|
|
188
|
+
This class loads measured data from HDF5 files and provides information about this data.
|
|
189
|
+
It also serves as an interface where the data can be accessed (e.g. for use in a block chain) via the
|
|
190
|
+
:meth:`result` generator.
|
|
191
|
+
|
|
192
|
+
Examples
|
|
193
|
+
--------
|
|
194
|
+
Data can be loaded from a HDF5 file as follows:
|
|
195
|
+
|
|
196
|
+
>>> from acoular import TimeSamples
|
|
197
|
+
>>> name = <some_h5_file.h5> # doctest: +SKIP
|
|
198
|
+
>>> ts = TimeSamples(name=name) # doctest: +SKIP
|
|
199
|
+
>>> print(f'number of channels: {ts.numchannels}') # doctest: +SKIP
|
|
200
|
+
number of channels: 56 # doctest: +SKIP
|
|
201
|
+
|
|
202
|
+
Alternatively, the time data can be specified directly as a numpy array.
|
|
203
|
+
In this case, the :attr:`data` and :attr:`sample_freq` attributes must be set manually.
|
|
204
|
+
|
|
205
|
+
>>> import numpy as np
|
|
206
|
+
>>> data = np.random.rand(1000, 4)
|
|
207
|
+
>>> ts = TimeSamples(data=data, sample_freq=51200)
|
|
208
|
+
|
|
209
|
+
Chunks of the time data can be accessed iteratively via the :meth:`result` generator.
|
|
210
|
+
The last block will be shorter than the block size if the number of samples is not a multiple of the block size.
|
|
211
|
+
|
|
212
|
+
>>> blocksize = 512
|
|
213
|
+
>>> generator = ts.result(num=blocksize)
|
|
214
|
+
>>> for block in generator:
|
|
215
|
+
... print(block.shape)
|
|
216
|
+
(512, 4)
|
|
217
|
+
(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.
|
|
189
224
|
"""
|
|
190
225
|
|
|
191
226
|
#: Full name of the .h5 file with data.
|
|
@@ -219,7 +254,9 @@ class TimeSamples(SamplesGenerator):
|
|
|
219
254
|
_datachecksum = Property()
|
|
220
255
|
|
|
221
256
|
# internal identifier
|
|
222
|
-
digest = Property(
|
|
257
|
+
digest = Property(
|
|
258
|
+
depends_on=['basename', 'calib.digest', '_datachecksum', 'sample_freq', 'numchannels', 'numsamples']
|
|
259
|
+
)
|
|
223
260
|
|
|
224
261
|
def _get__datachecksum(self):
|
|
225
262
|
return self.data[0, :].sum()
|
|
@@ -233,31 +270,31 @@ class TimeSamples(SamplesGenerator):
|
|
|
233
270
|
return path.splitext(path.basename(self.name))[0]
|
|
234
271
|
|
|
235
272
|
@on_trait_change('basename')
|
|
236
|
-
def
|
|
273
|
+
def _load_data(self):
|
|
237
274
|
"""Open the .h5 file and set attributes."""
|
|
238
275
|
if not path.isfile(self.name):
|
|
239
|
-
# no file there
|
|
240
|
-
self.numsamples = 0
|
|
241
|
-
self.numchannels = 0
|
|
242
276
|
self.sample_freq = 0
|
|
243
277
|
raise OSError('No such file: %s' % self.name)
|
|
244
278
|
if self.h5f is not None:
|
|
245
|
-
|
|
279
|
+
with contextlib.suppress(OSError):
|
|
246
280
|
self.h5f.close()
|
|
247
|
-
except OSError:
|
|
248
|
-
pass
|
|
249
281
|
file = _get_h5file_class()
|
|
250
282
|
self.h5f = file(self.name)
|
|
251
|
-
self.
|
|
252
|
-
self.
|
|
283
|
+
self._load_timedata()
|
|
284
|
+
self._load_metadata()
|
|
253
285
|
|
|
254
|
-
|
|
286
|
+
@on_trait_change('data')
|
|
287
|
+
def _load_shapes(self):
|
|
288
|
+
"""Set numchannels and numsamples from data."""
|
|
289
|
+
if self.data is not None:
|
|
290
|
+
self.numsamples, self.numchannels = self.data.shape
|
|
291
|
+
|
|
292
|
+
def _load_timedata(self):
|
|
255
293
|
"""Loads timedata from .h5 file. Only for internal use."""
|
|
256
294
|
self.data = self.h5f.get_data_by_reference('time_data')
|
|
257
295
|
self.sample_freq = self.h5f.get_node_attribute(self.data, 'sample_freq')
|
|
258
|
-
(self.numsamples, self.numchannels) = self.data.shape
|
|
259
296
|
|
|
260
|
-
def
|
|
297
|
+
def _load_metadata(self):
|
|
261
298
|
"""Loads metadata from .h5 file. Only for internal use."""
|
|
262
299
|
self.metadata = {}
|
|
263
300
|
if '/metadata' in self.h5f:
|
|
@@ -266,15 +303,20 @@ class TimeSamples(SamplesGenerator):
|
|
|
266
303
|
def result(self, num=128):
|
|
267
304
|
"""Python generator that yields the output block-wise.
|
|
268
305
|
|
|
306
|
+
Reads the time data either from a HDF5 file or from a numpy array given
|
|
307
|
+
by :attr:`data` and iteratively returns a block of size `num` samples.
|
|
308
|
+
Calibrated data is returned if a calibration object is given by :attr:`calib`.
|
|
309
|
+
|
|
269
310
|
Parameters
|
|
270
311
|
----------
|
|
271
312
|
num : integer, defaults to 128
|
|
272
313
|
This parameter defines the size of the blocks to be yielded
|
|
273
|
-
(i.e. the number of samples per block)
|
|
314
|
+
(i.e. the number of samples per block).
|
|
274
315
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
316
|
+
Yields
|
|
317
|
+
------
|
|
318
|
+
numpy.ndarray
|
|
319
|
+
Samples in blocks of shape (num, numchannels).
|
|
278
320
|
The last block may be shorter than num.
|
|
279
321
|
|
|
280
322
|
"""
|
|
@@ -298,14 +340,40 @@ class TimeSamples(SamplesGenerator):
|
|
|
298
340
|
|
|
299
341
|
|
|
300
342
|
class MaskedTimeSamples(TimeSamples):
|
|
301
|
-
"""Container for time data in `*.h5` format.
|
|
302
|
-
|
|
303
|
-
This class loads measured data from
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
It also serves as an interface where the data can be accessed
|
|
307
|
-
|
|
308
|
-
|
|
343
|
+
"""Container for processing time data in `*.h5` or NumPy array format.
|
|
344
|
+
|
|
345
|
+
This class loads measured data from HDF5 files and provides information about this data.
|
|
346
|
+
It supports storing information about (in)valid samples and (in)valid channels and allows
|
|
347
|
+
to specify a start and stop index for the valid samples.
|
|
348
|
+
It also serves as an interface where the data can be accessed (e.g. for use in a block chain) via the
|
|
349
|
+
:meth:`result` generator.
|
|
350
|
+
|
|
351
|
+
Examples
|
|
352
|
+
--------
|
|
353
|
+
Data can be loaded from a HDF5 file and invalid channels can be specified as follows:
|
|
354
|
+
|
|
355
|
+
>>> from acoular import MaskedTimeSamples
|
|
356
|
+
>>> name = <some_h5_file.h5> # doctest: +SKIP
|
|
357
|
+
>>> ts = MaskedTimeSamples(name=name, invalid_channels=[0, 1]) # doctest: +SKIP
|
|
358
|
+
>>> print(f'number of valid channels: {ts.numchannels}') # doctest: +SKIP
|
|
359
|
+
number of valid channels: 54 # doctest: +SKIP
|
|
360
|
+
|
|
361
|
+
Alternatively, the time data can be specified directly as a numpy array.
|
|
362
|
+
In this case, the :attr:`data` and :attr:`sample_freq` attributes must be set manually.
|
|
363
|
+
|
|
364
|
+
>>> from acoular import MaskedTimeSamples
|
|
365
|
+
>>> import numpy as np
|
|
366
|
+
>>> data = np.random.rand(1000, 4)
|
|
367
|
+
>>> ts = MaskedTimeSamples(data=data, sample_freq=51200)
|
|
368
|
+
|
|
369
|
+
Chunks of the time data can be accessed iteratively via the :meth:`result` generator:
|
|
370
|
+
|
|
371
|
+
>>> blocksize = 512
|
|
372
|
+
>>> generator = ts.result(num=blocksize)
|
|
373
|
+
>>> for block in generator:
|
|
374
|
+
... print(block.shape)
|
|
375
|
+
(512, 4)
|
|
376
|
+
(488, 4)
|
|
309
377
|
"""
|
|
310
378
|
|
|
311
379
|
#: Index of the first sample to be considered valid.
|
|
@@ -362,26 +430,28 @@ class MaskedTimeSamples(TimeSamples):
|
|
|
362
430
|
return sli[1] - sli[0]
|
|
363
431
|
|
|
364
432
|
@on_trait_change('basename')
|
|
365
|
-
def
|
|
433
|
+
def _load_data(self):
|
|
366
434
|
# """ open the .h5 file and set attributes
|
|
367
435
|
# """
|
|
368
436
|
if not path.isfile(self.name):
|
|
369
437
|
# no file there
|
|
370
|
-
self.numsamples_total = 0
|
|
371
|
-
self.numchannels_total = 0
|
|
372
438
|
self.sample_freq = 0
|
|
373
439
|
raise OSError('No such file: %s' % self.name)
|
|
374
440
|
if self.h5f is not None:
|
|
375
|
-
|
|
441
|
+
with contextlib.suppress(OSError):
|
|
376
442
|
self.h5f.close()
|
|
377
|
-
except OSError:
|
|
378
|
-
pass
|
|
379
443
|
file = _get_h5file_class()
|
|
380
444
|
self.h5f = file(self.name)
|
|
381
|
-
self.
|
|
382
|
-
self.
|
|
445
|
+
self._load_timedata()
|
|
446
|
+
self._load_metadata()
|
|
447
|
+
|
|
448
|
+
@on_trait_change('data')
|
|
449
|
+
def _load_shapes(self):
|
|
450
|
+
"""Set numchannels and numsamples from data."""
|
|
451
|
+
if self.data is not None:
|
|
452
|
+
self.numsamples_total, self.numchannels_total = self.data.shape
|
|
383
453
|
|
|
384
|
-
def
|
|
454
|
+
def _load_timedata(self):
|
|
385
455
|
"""Loads timedata from .h5 file. Only for internal use."""
|
|
386
456
|
self.data = self.h5f.get_data_by_reference('time_data')
|
|
387
457
|
self.sample_freq = self.h5f.get_node_attribute(self.data, 'sample_freq')
|
|
@@ -390,15 +460,20 @@ class MaskedTimeSamples(TimeSamples):
|
|
|
390
460
|
def result(self, num=128):
|
|
391
461
|
"""Python generator that yields the output block-wise.
|
|
392
462
|
|
|
463
|
+
Reads the time data either from a HDF5 file or from a numpy array given
|
|
464
|
+
by :attr:`data` and iteratively returns a block of size `num` samples.
|
|
465
|
+
Calibrated data is returned if a calibration object is given by :attr:`calib`.
|
|
466
|
+
|
|
393
467
|
Parameters
|
|
394
468
|
----------
|
|
395
469
|
num : integer, defaults to 128
|
|
396
470
|
This parameter defines the size of the blocks to be yielded
|
|
397
471
|
(i.e. the number of samples per block).
|
|
398
472
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
473
|
+
Yields
|
|
474
|
+
------
|
|
475
|
+
numpy.ndarray
|
|
476
|
+
Samples in blocks of shape (num, numchannels).
|
|
402
477
|
The last block may be shorter than num.
|
|
403
478
|
|
|
404
479
|
"""
|
|
@@ -463,7 +538,11 @@ class PointSource(SamplesGenerator):
|
|
|
463
538
|
return self.mics
|
|
464
539
|
|
|
465
540
|
def _set_mpos(self, mpos):
|
|
466
|
-
|
|
541
|
+
msg = (
|
|
542
|
+
"Deprecated use of 'mpos' trait. Use 'mics' trait instead."
|
|
543
|
+
"The 'mpos' trait will be removed in version 25.01."
|
|
544
|
+
)
|
|
545
|
+
warn(msg, DeprecationWarning, stacklevel=2)
|
|
467
546
|
self.mics = mpos
|
|
468
547
|
|
|
469
548
|
# The speed of sound.
|
|
@@ -475,7 +554,8 @@ class PointSource(SamplesGenerator):
|
|
|
475
554
|
return self.env.c
|
|
476
555
|
|
|
477
556
|
def _set_c(self, c):
|
|
478
|
-
|
|
557
|
+
msg = "Deprecated use of 'c' trait. Use 'env' trait instead." "The 'c' trait will be removed in version 25.01."
|
|
558
|
+
warn(msg, DeprecationWarning, stacklevel=2)
|
|
479
559
|
self.env.c = c
|
|
480
560
|
|
|
481
561
|
# --- End of backwards compatibility traits --------------------------------------
|
|
@@ -588,7 +668,7 @@ class SphericalHarmonicSource(PointSource):
|
|
|
588
668
|
"""
|
|
589
669
|
|
|
590
670
|
#: Order of spherical harmonic source
|
|
591
|
-
lOrder = Int(0, desc='Order of spherical harmonic')
|
|
671
|
+
lOrder = Int(0, desc='Order of spherical harmonic') # noqa: N815
|
|
592
672
|
|
|
593
673
|
alpha = CArray(desc='coefficients of the (lOrder,) spherical harmonic mode')
|
|
594
674
|
|
|
@@ -1302,7 +1382,11 @@ class UncorrelatedNoiseSource(SamplesGenerator):
|
|
|
1302
1382
|
return self.mics
|
|
1303
1383
|
|
|
1304
1384
|
def _set_mpos(self, mpos):
|
|
1305
|
-
|
|
1385
|
+
msg = (
|
|
1386
|
+
"Deprecated use of 'mpos' trait. Use 'mics' trait instead."
|
|
1387
|
+
"The 'mpos' trait will be removed in version 25.01."
|
|
1388
|
+
)
|
|
1389
|
+
warn(msg, DeprecationWarning, stacklevel=2)
|
|
1306
1390
|
self.mics = mpos
|
|
1307
1391
|
|
|
1308
1392
|
# --- End of backwards compatibility traits --------------------------------------
|
|
@@ -1391,7 +1475,7 @@ class UncorrelatedNoiseSource(SamplesGenerator):
|
|
|
1391
1475
|
class SourceMixer(SamplesGenerator):
|
|
1392
1476
|
"""Mixes the signals from several sources."""
|
|
1393
1477
|
|
|
1394
|
-
#: List of :class:`~acoular.
|
|
1478
|
+
#: List of :class:`~acoular.base.SamplesGenerator` objects
|
|
1395
1479
|
#: to be mixed.
|
|
1396
1480
|
sources = List(Instance(SamplesGenerator, ()))
|
|
1397
1481
|
|
acoular/spectra.py
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
:toctree: generated/
|
|
8
8
|
|
|
9
9
|
BaseSpectra
|
|
10
|
-
FFTSpectra
|
|
11
10
|
PowerSpectra
|
|
12
11
|
synthetic
|
|
13
12
|
PowerSpectraImport
|
|
@@ -33,7 +32,6 @@ from numpy import (
|
|
|
33
32
|
ones,
|
|
34
33
|
real,
|
|
35
34
|
searchsorted,
|
|
36
|
-
sqrt,
|
|
37
35
|
sum,
|
|
38
36
|
zeros,
|
|
39
37
|
zeros_like,
|
|
@@ -54,13 +52,13 @@ from traits.api import (
|
|
|
54
52
|
property_depends_on,
|
|
55
53
|
)
|
|
56
54
|
|
|
55
|
+
from .base import SamplesGenerator
|
|
57
56
|
from .calib import Calib
|
|
58
57
|
from .configuration import config
|
|
59
58
|
from .fastFuncs import calcCSM
|
|
60
59
|
from .h5cache import H5cache
|
|
61
60
|
from .h5files import H5CacheFileBase
|
|
62
61
|
from .internal import digest
|
|
63
|
-
from .tprocess import SamplesGenerator, TimeInOut
|
|
64
62
|
|
|
65
63
|
|
|
66
64
|
class BaseSpectra(HasPrivateTraits):
|
|
@@ -105,8 +103,8 @@ class BaseSpectra(HasPrivateTraits):
|
|
|
105
103
|
desc='number of samples per FFT block',
|
|
106
104
|
)
|
|
107
105
|
|
|
108
|
-
#: The floating-number-precision of
|
|
109
|
-
#:
|
|
106
|
+
#: The floating-number-precision of the resulting spectra, corresponding to numpy dtypes.
|
|
107
|
+
#: Default is 'complex128'.
|
|
110
108
|
precision = Trait('complex128', 'complex64', desc='precision of the fft')
|
|
111
109
|
|
|
112
110
|
# internal identifier
|
|
@@ -130,7 +128,7 @@ class BaseSpectra(HasPrivateTraits):
|
|
|
130
128
|
return None
|
|
131
129
|
|
|
132
130
|
# generator that yields the time data blocks for every channel (with optional overlap)
|
|
133
|
-
def
|
|
131
|
+
def _get_source_data(self):
|
|
134
132
|
bs = self.block_size
|
|
135
133
|
temp = empty((2 * bs, self.numchannels))
|
|
136
134
|
pos = bs
|
|
@@ -146,48 +144,12 @@ class BaseSpectra(HasPrivateTraits):
|
|
|
146
144
|
pos -= bs
|
|
147
145
|
|
|
148
146
|
|
|
149
|
-
class FFTSpectra(BaseSpectra, TimeInOut):
|
|
150
|
-
"""Provides the spectra of multichannel time data.
|
|
151
|
-
|
|
152
|
-
Returns Spectra per block over a Generator.
|
|
153
|
-
"""
|
|
154
|
-
|
|
155
|
-
# internal identifier
|
|
156
|
-
digest = Property(depends_on=['source.digest', 'precision', 'block_size', 'window', 'overlap'])
|
|
157
|
-
|
|
158
|
-
@cached_property
|
|
159
|
-
def _get_digest(self):
|
|
160
|
-
return digest(self)
|
|
161
|
-
|
|
162
|
-
# generator that yields the fft for every channel
|
|
163
|
-
def result(self):
|
|
164
|
-
"""Python generator that yields the output block-wise.
|
|
165
|
-
|
|
166
|
-
Parameters
|
|
167
|
-
----------
|
|
168
|
-
num : integer
|
|
169
|
-
This parameter defines the size of the blocks to be yielded
|
|
170
|
-
(i.e. the number of samples per block).
|
|
171
|
-
|
|
172
|
-
Returns
|
|
173
|
-
-------
|
|
174
|
-
Samples in blocks of shape (numfreq, :attr:`numchannels`).
|
|
175
|
-
The last block may be shorter than num.
|
|
176
|
-
|
|
177
|
-
"""
|
|
178
|
-
wind = self.window_(self.block_size)
|
|
179
|
-
weight = sqrt(2) / self.block_size * sqrt(self.block_size / dot(wind, wind)) * wind[:, newaxis]
|
|
180
|
-
for data in self.get_source_data():
|
|
181
|
-
ft = fft.rfft(data * weight, None, 0).astype(self.precision)
|
|
182
|
-
yield ft
|
|
183
|
-
|
|
184
|
-
|
|
185
147
|
class PowerSpectra(BaseSpectra):
|
|
186
148
|
"""Provides the cross spectral matrix of multichannel time data
|
|
187
149
|
and its eigen-decomposition.
|
|
188
150
|
|
|
189
151
|
This class includes the efficient calculation of the full cross spectral
|
|
190
|
-
matrix using the Welch method with windows and overlap. It also contains
|
|
152
|
+
matrix using the Welch method with windows and overlap (:cite:`Welch1967`). It also contains
|
|
191
153
|
the CSM's eigenvalues and eigenvectors and additional properties.
|
|
192
154
|
|
|
193
155
|
The result is computed only when needed, that is when the :attr:`csm`,
|
|
@@ -206,7 +168,7 @@ class PowerSpectra(BaseSpectra):
|
|
|
206
168
|
#: Data source; :class:`~acoular.sources.SamplesGenerator` or derived object.
|
|
207
169
|
source = Property(_source, desc='time data object')
|
|
208
170
|
|
|
209
|
-
#: The :class:`~acoular.
|
|
171
|
+
#: The :class:`~acoular.base.SamplesGenerator` object that provides the data.
|
|
210
172
|
time_data = Property(
|
|
211
173
|
_source,
|
|
212
174
|
desc='deprecated attribute holding the time data object. Use PowerSpectra.source instead!',
|
|
@@ -215,9 +177,11 @@ class PowerSpectra(BaseSpectra):
|
|
|
215
177
|
#: The :class:`~acoular.calib.Calib` object that provides the calibration data,
|
|
216
178
|
#: defaults to no calibration, i.e. the raw time data is used.
|
|
217
179
|
#:
|
|
218
|
-
#: **deprecated**:
|
|
180
|
+
#: **deprecated, will be removed in version 25.01**: use :attr:`~acoular.sources.TimeSamples.calib` property of
|
|
219
181
|
#: :class:`~acoular.sources.TimeSamples` objects
|
|
220
|
-
calib =
|
|
182
|
+
calib = Property(desc='calibration object (deprecated, will be removed in version 25.01)')
|
|
183
|
+
|
|
184
|
+
_calib = Instance(Calib)
|
|
221
185
|
|
|
222
186
|
# Shadow trait, should not be set directly, for internal use.
|
|
223
187
|
_ind_low = Int(1, desc='index of lowest frequency line')
|
|
@@ -292,6 +256,18 @@ class PowerSpectra(BaseSpectra):
|
|
|
292
256
|
# hdf5 cache file
|
|
293
257
|
h5f = Instance(H5CacheFileBase, transient=True)
|
|
294
258
|
|
|
259
|
+
def _get_calib(self):
|
|
260
|
+
return self._calib
|
|
261
|
+
|
|
262
|
+
def _set_calib(self, calib):
|
|
263
|
+
msg = (
|
|
264
|
+
"Using 'calib' attribute is deprecated and will be removed in version 25.01. "
|
|
265
|
+
'use :attr:`~acoular.sources.TimeSamples.calib` property of '
|
|
266
|
+
':class:`~acoular.sources.TimeSamples` object instead.'
|
|
267
|
+
)
|
|
268
|
+
warn(msg, DeprecationWarning, stacklevel=2)
|
|
269
|
+
self._calib = calib
|
|
270
|
+
|
|
295
271
|
@property_depends_on('_source.numsamples, block_size, overlap')
|
|
296
272
|
def _get_num_blocks(self):
|
|
297
273
|
return self.overlap_ * self._source.numsamples / self.block_size - self.overlap_ + 1
|
|
@@ -341,6 +317,11 @@ class PowerSpectra(BaseSpectra):
|
|
|
341
317
|
self._ind_low = ind_low
|
|
342
318
|
|
|
343
319
|
def _set_time_data(self, time_data):
|
|
320
|
+
msg = (
|
|
321
|
+
"Using 'time_data' attribute is deprecated and will be removed in version 25.01. "
|
|
322
|
+
"Use 'source' attribute instead."
|
|
323
|
+
)
|
|
324
|
+
warn(msg, DeprecationWarning, stacklevel=2)
|
|
344
325
|
self._source = time_data
|
|
345
326
|
|
|
346
327
|
def _set_source(self, source):
|
|
@@ -383,7 +364,7 @@ class PowerSpectra(BaseSpectra):
|
|
|
383
364
|
wind = wind[newaxis, :].swapaxes(0, 1)
|
|
384
365
|
numfreq = int(self.block_size / 2 + 1)
|
|
385
366
|
csm_shape = (numfreq, t.numchannels, t.numchannels)
|
|
386
|
-
|
|
367
|
+
csm_upper = zeros(csm_shape, dtype=self.precision)
|
|
387
368
|
# print "num blocks", self.num_blocks
|
|
388
369
|
# for backward compatibility
|
|
389
370
|
if self.calib and self.calib.num_mics > 0:
|
|
@@ -392,13 +373,13 @@ class PowerSpectra(BaseSpectra):
|
|
|
392
373
|
else:
|
|
393
374
|
raise ValueError('Calibration data not compatible: %i, %i' % (self.calib.num_mics, t.numchannels))
|
|
394
375
|
# get time data blockwise
|
|
395
|
-
for data in self.
|
|
376
|
+
for data in self._get_source_data():
|
|
396
377
|
ft = fft.rfft(data * wind, None, 0).astype(self.precision)
|
|
397
|
-
calcCSM(
|
|
378
|
+
calcCSM(csm_upper, ft) # only upper triangular part of matrix is calculated (for speed reasons)
|
|
398
379
|
# create the full csm matrix via transposing and complex conj.
|
|
399
|
-
|
|
400
|
-
[fill_diagonal(
|
|
401
|
-
csm =
|
|
380
|
+
csm_lower = csm_upper.conj().transpose(0, 2, 1)
|
|
381
|
+
[fill_diagonal(csm_lower[cntFreq, :, :], 0) for cntFreq in range(csm_lower.shape[0])]
|
|
382
|
+
csm = csm_lower + csm_upper
|
|
402
383
|
# onesided spectrum: multiplication by 2.0=sqrt(2)^2
|
|
403
384
|
return csm * (2.0 / self.block_size / weight / self.num_blocks)
|
|
404
385
|
|
|
@@ -594,7 +575,7 @@ def synthetic(data, freqs, f, num=3):
|
|
|
594
575
|
each grid point (the sum of all values that are contained in the band).
|
|
595
576
|
Note that the frequency resolution and therefore the bandwidth
|
|
596
577
|
represented by a single frequency line depends on
|
|
597
|
-
the :attr:`sampling frequency<acoular.
|
|
578
|
+
the :attr:`sampling frequency<acoular.base.SamplesGenerator.sample_freq>`
|
|
598
579
|
and used :attr:`FFT block size<acoular.spectra.PowerSpectra.block_size>`.
|
|
599
580
|
|
|
600
581
|
"""
|