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/spectra.py CHANGED
@@ -8,10 +8,10 @@
8
8
 
9
9
  BaseSpectra
10
10
  PowerSpectra
11
- synthetic
12
11
  PowerSpectraImport
13
12
  """
14
13
 
14
+ from abc import abstractmethod
15
15
  from warnings import warn
16
16
 
17
17
  from numpy import (
@@ -25,75 +25,98 @@ from numpy import (
25
25
  hamming,
26
26
  hanning,
27
27
  imag,
28
- isscalar,
29
28
  linalg,
30
29
  ndarray,
31
30
  newaxis,
32
31
  ones,
33
32
  real,
34
33
  searchsorted,
35
- sum,
34
+ sum, # noqa A004
36
35
  zeros,
37
- zeros_like,
38
36
  )
39
37
  from scipy import fft
40
38
  from traits.api import (
39
+ ABCHasStrictTraits,
41
40
  Bool,
42
41
  CArray,
43
42
  Delegate,
44
43
  Enum,
45
44
  Float,
46
- HasPrivateTraits,
47
45
  Instance,
48
46
  Int,
47
+ Map,
49
48
  Property,
50
- Trait,
49
+ Union,
51
50
  cached_property,
52
51
  property_depends_on,
53
52
  )
54
53
 
54
+ # acoular imports
55
55
  from .base import SamplesGenerator
56
- from .calib import Calib
57
56
  from .configuration import config
57
+ from .deprecation import deprecated_alias
58
58
  from .fastFuncs import calcCSM
59
59
  from .h5cache import H5cache
60
60
  from .h5files import H5CacheFileBase
61
61
  from .internal import digest
62
+ from .tools.utils import find_basename
62
63
 
63
64
 
64
- class BaseSpectra(HasPrivateTraits):
65
- #: Data source; :class:`~acoular.sources.SamplesGenerator` or derived object.
66
- source = Trait(SamplesGenerator)
65
+ @deprecated_alias({'numchannels': 'num_channels'}, read_only=True)
66
+ @deprecated_alias({'time_data': 'source'}, read_only=False)
67
+ class BaseSpectra(ABCHasStrictTraits):
68
+ """
69
+ Base class for handling spectral data in Acoular.
70
+
71
+ This class defines the basic structure and functionality for computing and managing spectral
72
+ data derived from time-domain signals. It includes properties for configuring the Fast Fourier
73
+ Transformation (FFT), including overlap, and other parameters critical for spectral analysis.
74
+ """
67
75
 
68
- #: Sampling frequency of output signal, as given by :attr:`source`.
76
+ #: Data source; an instance of :class:`~acoular.base.SamplesGenerator` or derived object.
77
+ source = Instance(SamplesGenerator)
78
+
79
+ #: Sampling frequency of the output signal, delegated from :attr:`source`.
69
80
  sample_freq = Delegate('source')
70
81
 
71
- #: Number of time data channels
72
- numchannels = Delegate('source')
73
-
74
- #: Window function for FFT, one of:
75
- #: * 'Rectangular' (default)
76
- #: * 'Hanning'
77
- #: * 'Hamming'
78
- #: * 'Bartlett'
79
- #: * 'Blackman'
80
- window = Trait(
81
- 'Rectangular',
82
+ #: Number of time microphones, delegated from :attr:`source`.
83
+ num_channels = Delegate('source')
84
+
85
+ #: Window function applied during FFT. Can be one of:
86
+ #:
87
+ #: - ``'Rectangular'`` (default)
88
+ #:
89
+ #: - ``'Hanning'``
90
+ #:
91
+ #: - ``'Hamming'``
92
+ #:
93
+ #: - ``'Bartlett'``
94
+ #:
95
+ #: - ``'Blackman'``
96
+ window = Map(
82
97
  {'Rectangular': ones, 'Hanning': hanning, 'Hamming': hamming, 'Bartlett': bartlett, 'Blackman': blackman},
98
+ default_value='Rectangular',
83
99
  desc='type of window for FFT',
84
100
  )
85
101
 
86
- #: Overlap factor for averaging: 'None'(default), '50%', '75%', '87.5%'.
87
- overlap = Trait('None', {'None': 1, '50%': 2, '75%': 4, '87.5%': 8}, desc='overlap of FFT blocks')
102
+ #: Overlap factor for FFT block averaging. One of:
103
+ #:
104
+ #: - ``'None'`` (default)
105
+ #:
106
+ #: - ``'50%'``
107
+ #:
108
+ #: - ``'75%'``
109
+ #:
110
+ #: - ``'87.5%'``
111
+ overlap = Map({'None': 1, '50%': 2, '75%': 4, '87.5%': 8}, default_value='None', desc='overlap of FFT blocks')
88
112
 
89
- #: FFT block size, one of: 128, 256, 512, 1024, 2048 ... 65536,
90
- #: defaults to 1024.
91
- block_size = Trait(
113
+ #: FFT block size. Must be one of: ``128``, ``256``, ``512``, ``1024``, ... ``65536``.
114
+ #: Default is ``1024``.
115
+ block_size = Enum(
92
116
  1024,
93
117
  128,
94
118
  256,
95
119
  512,
96
- 1024,
97
120
  2048,
98
121
  4096,
99
122
  8192,
@@ -103,25 +126,53 @@ class BaseSpectra(HasPrivateTraits):
103
126
  desc='number of samples per FFT block',
104
127
  )
105
128
 
106
- #: The floating-number-precision of the resulting spectra, corresponding to numpy dtypes.
107
- #: Default is 'complex128'.
108
- precision = Trait('complex128', 'complex64', desc='precision of the fft')
129
+ #: Precision of the FFT, corresponding to NumPy dtypes. Default is ``'complex128'``.
130
+ precision = Enum('complex128', 'complex64', desc='precision of the fft')
109
131
 
110
- # internal identifier
132
+ #: A unique identifier for the spectra, based on its properties. (read-only)
111
133
  digest = Property(depends_on=['precision', 'block_size', 'window', 'overlap'])
112
134
 
113
- @cached_property
135
+ @abstractmethod
114
136
  def _get_digest(self):
115
- return digest(self)
137
+ """Return internal identifier."""
116
138
 
117
139
  def fftfreq(self):
118
- """Return the Discrete Fourier Transform sample frequencies.
140
+ """
141
+ Compute and return the Discrete Fourier Transform sample frequencies.
142
+
143
+ This method generates the frequency values corresponding to the FFT bins for the
144
+ configured :attr:`block_size` and sampling frequency from the data source.
119
145
 
120
146
  Returns
121
147
  -------
122
- f : ndarray
123
- Array of length *block_size/2+1* containing the sample frequencies.
124
-
148
+ :obj:`numpy.ndarray` or :obj:`None`
149
+ Array of shape ``(`` :attr:`block_size` ``/ 2 + 1,)`` containing the sample frequencies.
150
+ If :attr:`source` is not set, returns ``None``.
151
+
152
+ Examples
153
+ --------
154
+ Using normally distributed data for time samples as in
155
+ :class:`~acoular.sources.TimeSamples`.
156
+
157
+ >>> import numpy as np
158
+ >>> from acoular import TimeSamples
159
+ >>> from acoular.spectra import PowerSpectra
160
+ >>>
161
+ >>> data = np.random.rand(1000, 4)
162
+ >>> ts = TimeSamples(data=data, sample_freq=51200)
163
+ >>> print(ts.num_channels, ts.num_samples, ts.sample_freq)
164
+ 4 1000 51200.0
165
+ >>> ps = PowerSpectra(source=ts, block_size=128, window='Blackman')
166
+ >>> ps.fftfreq()
167
+ array([ 0., 400., 800., 1200., 1600., 2000., 2400., 2800.,
168
+ 3200., 3600., 4000., 4400., 4800., 5200., 5600., 6000.,
169
+ 6400., 6800., 7200., 7600., 8000., 8400., 8800., 9200.,
170
+ 9600., 10000., 10400., 10800., 11200., 11600., 12000., 12400.,
171
+ 12800., 13200., 13600., 14000., 14400., 14800., 15200., 15600.,
172
+ 16000., 16400., 16800., 17200., 17600., 18000., 18400., 18800.,
173
+ 19200., 19600., 20000., 20400., 20800., 21200., 21600., 22000.,
174
+ 22400., 22800., 23200., 23600., 24000., 24400., 24800., 25200.,
175
+ 25600.])
125
176
  """
126
177
  if self.source is not None:
127
178
  return abs(fft.fftfreq(self.block_size, 1.0 / self.source.sample_freq)[: int(self.block_size / 2 + 1)])
@@ -130,7 +181,7 @@ class BaseSpectra(HasPrivateTraits):
130
181
  # generator that yields the time data blocks for every channel (with optional overlap)
131
182
  def _get_source_data(self):
132
183
  bs = self.block_size
133
- temp = empty((2 * bs, self.numchannels))
184
+ temp = empty((2 * bs, self.num_channels))
134
185
  pos = bs
135
186
  posinc = bs / self.overlap_
136
187
  for data_block in self.source.result(bs):
@@ -145,134 +196,99 @@ class BaseSpectra(HasPrivateTraits):
145
196
 
146
197
 
147
198
  class PowerSpectra(BaseSpectra):
148
- """Provides the cross spectral matrix of multichannel time data
149
- and its eigen-decomposition.
150
-
151
- This class includes the efficient calculation of the full cross spectral
152
- matrix using the Welch method with windows and overlap (:cite:`Welch1967`). It also contains
153
- the CSM's eigenvalues and eigenvectors and additional properties.
154
-
155
- The result is computed only when needed, that is when the :attr:`csm`,
156
- :attr:`eva`, or :attr:`eve` attributes are acturally read.
157
- Any change in the input data or parameters leads to a new calculation,
158
- again triggered when an attribute is read. The result may be
159
- cached on disk in HDF5 files and need not to be recomputed during
160
- subsequent program runs with identical input data and parameters. The
161
- input data is taken to be identical if the source has identical parameters
162
- and the same file name in case of that the data is read from a file.
199
+ """
200
+ Provides the cross-spectral matrix of multichannel time-domain data and its eigen-decomposition.
201
+
202
+ This class is designed to compute the cross-spectral matrix (CSM) efficiently using the Welch
203
+ method :cite:`Welch1967` with support for windowing and overlapping data segments. It also
204
+ calculates the eigenvalues and eigenvectors of the CSM, allowing for spectral analysis and
205
+ advanced signal processing tasks.
206
+
207
+ Key features:
208
+ - **Efficient Calculation**: Computes the CSM using FFT-based methods.
209
+ - **Caching**: Results can be cached in HDF5 files to avoid redundant calculations for
210
+ identical inputs and parameters.
211
+ - **Lazy Evaluation**: Calculations are triggered only when attributes like :attr:`csm`,
212
+ :attr:`eva`, or :attr:`eve` are accessed.
213
+ - **Dynamic Input Handling**: Automatically recomputes results when the input data or
214
+ parameters change.
163
215
  """
164
216
 
165
- # Shadow trait, should not be set directly, for internal use.
166
- _source = Trait(SamplesGenerator)
167
-
168
- #: Data source; :class:`~acoular.sources.SamplesGenerator` or derived object.
169
- source = Property(_source, desc='time data object')
170
-
171
- #: The :class:`~acoular.base.SamplesGenerator` object that provides the data.
172
- time_data = Property(
173
- _source,
174
- desc='deprecated attribute holding the time data object. Use PowerSpectra.source instead!',
175
- )
176
-
177
- #: The :class:`~acoular.calib.Calib` object that provides the calibration data,
178
- #: defaults to no calibration, i.e. the raw time data is used.
179
- #:
180
- #: **deprecated, will be removed in version 25.01**: use :attr:`~acoular.sources.TimeSamples.calib` property of
181
- #: :class:`~acoular.sources.TimeSamples` objects
182
- calib = Property(desc='calibration object (deprecated, will be removed in version 25.01)')
183
-
184
- _calib = Instance(Calib)
217
+ #: The data source for the time-domain samples. It must be an instance of
218
+ #: :class:`SamplesGenerator<acoular.base.SamplesGenerator>` or a derived class.
219
+ source = Instance(SamplesGenerator)
185
220
 
186
221
  # Shadow trait, should not be set directly, for internal use.
187
222
  _ind_low = Int(1, desc='index of lowest frequency line')
188
223
 
189
224
  # Shadow trait, should not be set directly, for internal use.
190
- _ind_high = Trait(-1, (Int, None), desc='index of highest frequency line')
225
+ _ind_high = Union(Int(-1), None, desc='index of highest frequency line')
191
226
 
192
- #: Index of lowest frequency line to compute, integer, defaults to 1,
193
- #: is used only by objects that fetch the csm, PowerSpectra computes every
194
- #: frequency line.
227
+ #: Index of lowest frequency line to compute. Default is ``1``. Only used by objects that fetch
228
+ #: the CSM. PowerSpectra computes every frequency line.
195
229
  ind_low = Property(_ind_low, desc='index of lowest frequency line')
196
230
 
197
- #: Index of highest frequency line to compute, integer,
198
- #: defaults to -1 (last possible line for default block_size).
231
+ #: Index of highest frequency line to compute. Default is ``-1``
232
+ #: (last possible line for default :attr:`~BaseSpectra.block_size`).
199
233
  ind_high = Property(_ind_high, desc='index of lowest frequency line')
200
234
 
201
235
  # Stores the set lower frequency, for internal use, should not be set directly.
202
236
  _freqlc = Float(0)
203
237
 
204
238
  # Stores the set higher frequency, for internal use, should not be set directly.
205
- _freqhc = Trait(0, (Float, None))
239
+ _freqhc = Union(Float(0), None)
206
240
 
207
- # Saves whether the user set indices or frequencies last, for internal use only,
208
- # not to be set directly, if True (default), indices are used for setting
209
- # the freq_range interval.
241
+ # Saves whether the user set indices or frequencies last, for internal use only, not to be set
242
+ # directly, if ``True``, indices are used for setting the :attr:`freq_range` interval.
243
+ # Default is ``True``.
210
244
  _index_set_last = Bool(True)
211
245
 
212
- #: Flag, if true (default), the result is cached in h5 files and need not
213
- #: to be recomputed during subsequent program runs.
246
+ #: A flag indicating whether the result should be cached in HDF5 files. Default is ``True``.
214
247
  cached = Bool(True, desc='cached flag')
215
248
 
216
- #: Number of FFT blocks to average, readonly
217
- #: (set from block_size and overlap).
249
+ #: The number of FFT blocks used for averaging. This is derived from the
250
+ #: :attr:`~BaseSpectra.block_size` and :attr:`~BaseSpectra.overlap` parameters. (read-only)
218
251
  num_blocks = Property(desc='overall number of FFT blocks')
219
252
 
220
- #: 2-element array with the lowest and highest frequency. If set,
221
- #: will overwrite :attr:`_freqlc` and :attr:`_freqhc` according to
222
- #: the range.
223
- #: The freq_range interval will be the smallest discrete frequency
224
- #: inside the half-open interval [_freqlc, _freqhc[ and the smallest
225
- #: upper frequency outside of the interval.
226
- #: If user chooses the higher frequency larger than the max frequency,
227
- #: the max frequency will be the upper bound.
253
+ #: 2-element array with the lowest and highest frequency. If the higher frequency is larger than
254
+ #: the max frequency, the max frequency will be the upper bound.
228
255
  freq_range = Property(desc='frequency range')
256
+ # If set, will overwrite :attr:`_freqlc` and :attr:`_freqhc` according to the range. The
257
+ # freq_range interval will be the smallest discrete frequency inside the half-open interval
258
+ # [_freqlc, _freqhc[ and the smallest upper frequency outside of the interval.
229
259
 
230
- #: Array with a sequence of indices for all frequencies
231
- #: between :attr:`ind_low` and :attr:`ind_high` within the result, readonly.
260
+ #: The sequence of frequency indices between :attr:`ind_low` and :attr:`ind_high`. (read-only)
232
261
  indices = Property(desc='index range')
233
262
 
234
- #: Name of the cache file without extension, readonly.
235
- basename = Property(depends_on='_source.digest', desc='basename for cache file')
263
+ #: The name of the cache file (without the file extension) used for storing results. (read-only)
264
+ basename = Property(depends_on=['source.digest'], desc='basename for cache file')
236
265
 
237
- #: The cross spectral matrix,
238
- #: (number of frequencies, numchannels, numchannels) array of complex;
239
- #: readonly.
266
+ #: The cross-spectral matrix, represented as an array of shape ``(n, m, m)`` of complex values
267
+ #: for ``n`` frequencies and ``m`` channels as in :attr:`~BaseSpectra.num_channels`. (read-only)
240
268
  csm = Property(desc='cross spectral matrix')
241
269
 
242
- #: Eigenvalues of the cross spectral matrix as an
243
- #: (number of frequencies) array of floats, readonly.
270
+ #: The eigenvalues of the CSM, stored as an array of shape ``(n,)`` of floats for ``n``
271
+ #: frequencies. (read-only)
244
272
  eva = Property(desc='eigenvalues of cross spectral matrix')
245
273
 
246
- #: Eigenvectors of the cross spectral matrix as an
247
- #: (number of frequencies, numchannels, numchannels) array of floats,
248
- #: readonly.
274
+ #: The eigenvectors of the cross spectral matrix, stored as an array of shape ``(n, m, m)`` of
275
+ #: floats for ``n`` frequencies and ``m`` channels as in :attr:`~BaseSpectra.num_channels`.
276
+ #: (read-only)
249
277
  eve = Property(desc='eigenvectors of cross spectral matrix')
250
278
 
251
- # internal identifier
279
+ #: A unique identifier for the spectra, based on its properties. (read-only)
252
280
  digest = Property(
253
- depends_on=['_source.digest', 'calib.digest', 'block_size', 'window', 'overlap', 'precision'],
281
+ depends_on=['source.digest', 'block_size', 'window', 'overlap', 'precision'],
254
282
  )
255
283
 
256
- # hdf5 cache file
284
+ #: The HDF5 cache file used for storing the results if :attr:`cached` is set to ``True``.
257
285
  h5f = Instance(H5CacheFileBase, transient=True)
258
286
 
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
-
271
- @property_depends_on('_source.numsamples, block_size, overlap')
287
+ @property_depends_on(['source.num_samples', 'block_size', 'overlap'])
272
288
  def _get_num_blocks(self):
273
- return self.overlap_ * self._source.numsamples / self.block_size - self.overlap_ + 1
289
+ return self.overlap_ * self.source.num_samples / self.block_size - self.overlap_ + 1
274
290
 
275
- @property_depends_on('_source.sample_freq, block_size, ind_low, ind_high')
291
+ @property_depends_on(['source.sample_freq', 'block_size', 'ind_low', 'ind_high'])
276
292
  def _get_freq_range(self):
277
293
  fftfreq = self.fftfreq()
278
294
  if fftfreq is not None:
@@ -286,7 +302,7 @@ class PowerSpectra(BaseSpectra):
286
302
  self._freqlc = freq_range[0]
287
303
  self._freqhc = freq_range[1]
288
304
 
289
- @property_depends_on('_source.sample_freq, block_size, _ind_low, _freqlc')
305
+ @property_depends_on(['source.sample_freq', 'block_size', '_ind_low', '_freqlc'])
290
306
  def _get_ind_low(self):
291
307
  fftfreq = self.fftfreq()
292
308
  if fftfreq is not None:
@@ -295,7 +311,7 @@ class PowerSpectra(BaseSpectra):
295
311
  return searchsorted(fftfreq[:-1], self._freqlc)
296
312
  return None
297
313
 
298
- @property_depends_on('_source.sample_freq, block_size, _ind_high, _freqhc')
314
+ @property_depends_on(['source.sample_freq', 'block_size', '_ind_high', '_freqhc'])
299
315
  def _get_ind_high(self):
300
316
  fftfreq = self.fftfreq()
301
317
  if fftfreq is not None:
@@ -316,24 +332,7 @@ class PowerSpectra(BaseSpectra):
316
332
  self._index_set_last = True
317
333
  self._ind_low = ind_low
318
334
 
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)
325
- self._source = time_data
326
-
327
- def _set_source(self, source):
328
- self._source = source
329
-
330
- def _get_time_data(self):
331
- return self._source
332
-
333
- def _get_source(self):
334
- return self._source
335
-
336
- @property_depends_on('block_size, ind_low, ind_high')
335
+ @property_depends_on(['block_size', 'ind_low', 'ind_high'])
337
336
  def _get_indices(self):
338
337
  fftfreq = self.fftfreq()
339
338
  if fftfreq is not None:
@@ -352,26 +351,44 @@ class PowerSpectra(BaseSpectra):
352
351
 
353
352
  @cached_property
354
353
  def _get_basename(self):
355
- if 'basename' in self._source.all_trait_names():
356
- return self._source.basename
357
- return self._source.__class__.__name__ + self._source.digest
354
+ return find_basename(self.source, alternative_basename=self.source.__class__.__name__ + self.source.digest)
358
355
 
359
356
  def calc_csm(self):
360
- """Csm calculation."""
357
+ """
358
+ Calculate the CSM for the given source data.
359
+
360
+ This method computes the CSM by performing a block-wise Fast Fourier Transform (FFT) on the
361
+ source data, applying a window function, and averaging the results. Only the upper
362
+ triangular part of the matrix is computed for efficiency, and the lower triangular part is
363
+ constructed via transposition and complex conjugation.
364
+
365
+ Returns
366
+ -------
367
+ :obj:`numpy.ndarray`
368
+ The computed cross spectral matrix as an array of shape ``(n, m, m)`` of complex values
369
+ for ``n`` frequencies and ``m`` channels as in :attr:`~BaseSpectra.num_channels`.
370
+
371
+ Examples
372
+ --------
373
+ >>> import numpy as np
374
+ >>> from acoular import TimeSamples
375
+ >>> from acoular.spectra import PowerSpectra
376
+ >>>
377
+ >>> data = np.random.rand(1000, 4)
378
+ >>> ts = TimeSamples(data=data, sample_freq=51200)
379
+ >>> print(ts.num_channels, ts.num_samples, ts.sample_freq)
380
+ 4 1000 51200.0
381
+ >>> ps = PowerSpectra(source=ts, block_size=128, window='Blackman')
382
+ >>> ps.csm.shape
383
+ (65, 4, 4)
384
+ """
361
385
  t = self.source
362
386
  wind = self.window_(self.block_size)
363
387
  weight = dot(wind, wind)
364
388
  wind = wind[newaxis, :].swapaxes(0, 1)
365
389
  numfreq = int(self.block_size / 2 + 1)
366
- csm_shape = (numfreq, t.numchannels, t.numchannels)
390
+ csm_shape = (numfreq, t.num_channels, t.num_channels)
367
391
  csm_upper = zeros(csm_shape, dtype=self.precision)
368
- # print "num blocks", self.num_blocks
369
- # for backward compatibility
370
- if self.calib and self.calib.num_mics > 0:
371
- if self.calib.num_mics == t.numchannels:
372
- wind = wind * self.calib.data[newaxis, :]
373
- else:
374
- raise ValueError('Calibration data not compatible: %i, %i' % (self.calib.num_mics, t.numchannels))
375
392
  # get time data blockwise
376
393
  for data in self._get_source_data():
377
394
  ft = fft.rfft(data * wind, None, 0).astype(self.precision)
@@ -384,7 +401,43 @@ class PowerSpectra(BaseSpectra):
384
401
  return csm * (2.0 / self.block_size / weight / self.num_blocks)
385
402
 
386
403
  def calc_ev(self):
387
- """Eigenvalues / eigenvectors calculation."""
404
+ """
405
+ Calculate eigenvalues and eigenvectors of the CSM for each frequency.
406
+
407
+ The eigenvalues represent the spectral power, and the eigenvectors correspond to the
408
+ principal components of the matrix. This calculation is performed for all frequency slices
409
+ of the CSM.
410
+
411
+ Returns
412
+ -------
413
+ :class:`tuple` of :obj:`numpy.ndarray`
414
+ A tuple containing:
415
+ - :attr:`eva` (:obj:`numpy.ndarray`): Eigenvalues as a 2D array of shape ``(n, m)``,
416
+ where ``n`` is the number of frequencies and ``m`` is the number of channels. The
417
+ datatype depends on the precision.
418
+ - :attr:`eve` (:obj:`numpy.ndarray`): Eigenvectors as a 3D array of shape
419
+ ``(n, m, m)``. The datatype is consistent with the precision of the input data.
420
+
421
+ Notes
422
+ -----
423
+ - The precision of the eigenvalues is determined by :attr:`~BaseSpectra.precision`
424
+ (``'float64'`` for ``complex128`` precision and ``'float32'`` for ``complex64``
425
+ precision).
426
+ - This method assumes the CSM is already computed and accessible via :attr:`csm`.
427
+
428
+ Examples
429
+ --------
430
+ >>> import numpy as np
431
+ >>> from acoular import TimeSamples
432
+ >>> from acoular.spectra import PowerSpectra
433
+ >>>
434
+ >>> data = np.random.rand(1000, 4)
435
+ >>> ts = TimeSamples(data=data, sample_freq=51200)
436
+ >>> ps = PowerSpectra(source=ts, block_size=128, window='Hanning')
437
+ >>> eva, eve = ps.calc_ev()
438
+ >>> print(eva.shape, eve.shape)
439
+ (65, 4) (65, 4, 4)
440
+ """
388
441
  if self.precision == 'complex128':
389
442
  eva_dtype = 'float64'
390
443
  elif self.precision == 'complex64':
@@ -398,38 +451,51 @@ class PowerSpectra(BaseSpectra):
398
451
  return (eva, eve)
399
452
 
400
453
  def calc_eva(self):
401
- """Calculates eigenvalues of csm."""
454
+ """
455
+ Calculate eigenvalues of the CSM.
456
+
457
+ This method computes and returns the eigenvalues of the CSM for all frequency slices.
458
+
459
+ Returns
460
+ -------
461
+ :obj:`numpy.ndarray`
462
+ A 2D array of shape ``(n, m)`` containing the eigenvalues for ``n`` frequencies and
463
+ ``m`` channels. The datatype depends on :attr:`~BaseSpectra.precision` (``'float64'``
464
+ for ``complex128`` precision and ``'float32'`` for ``complex64`` precision).
465
+
466
+ Notes
467
+ -----
468
+ This method internally calls :meth:`calc_ev` and extracts only the eigenvalues.
469
+ """
402
470
  return self.calc_ev()[0]
403
471
 
404
472
  def calc_eve(self):
405
- """Calculates eigenvectors of csm."""
406
- return self.calc_ev()[1]
473
+ """
474
+ Calculate eigenvectors of the Cross Spectral Matrix (CSM).
407
475
 
408
- def _handle_dual_calibration(self):
409
- obj = self.source # start with time_data obj
410
- while obj:
411
- if 'calib' in obj.all_trait_names(): # at original source?
412
- if obj.calib and self.calib:
413
- if obj.calib.digest == self.calib.digest:
414
- self.calib = None # ignore it silently
415
- else:
416
- msg = 'Non-identical dual calibration for both TimeSamples and PowerSpectra object'
417
- raise ValueError(msg)
418
- obj = None
419
- else:
420
- try:
421
- obj = obj.source # traverse down until original data source
422
- except AttributeError:
423
- obj = None
476
+ This method computes and returns the eigenvectors of the CSM for all frequency slices.
424
477
 
425
- def _get_filecache(self, traitname):
426
- """Function handles result caching of csm, eigenvectors and eigenvalues
427
- calculation depending on global/local caching behaviour.
478
+ Returns
479
+ -------
480
+ :obj:`numpy.ndarray`
481
+ A 3D array of shape ``(n, m, m)`` containing the eigenvectors for ``n`` frequencies and
482
+ ``m`` channels. Each slice ``eve[f]`` represents an ``(m, m)`` matrix of eigenvectors
483
+ for frequency ``f``. The datatype matches the :attr:`~BaseSpectra.precision` of the CSM
484
+ (``complex128`` or ``complex64``).
485
+
486
+ Notes
487
+ -----
488
+ This method internally calls :meth:`calc_ev()` and extracts only the eigenvectors.
428
489
  """
490
+ return self.calc_ev()[1]
491
+
492
+ def _get_filecache(self, traitname):
493
+ # Handle caching of results for CSM, eigenvalues, and eigenvectors.
494
+ # Returns the requested data (``csm``, ``eva``, or ``eve``) as a NumPy array.
429
495
  if traitname == 'csm':
430
496
  func = self.calc_csm
431
497
  numfreq = int(self.block_size / 2 + 1)
432
- shape = (numfreq, self._source.numchannels, self._source.numchannels)
498
+ shape = (numfreq, self.source.num_channels, self.source.num_channels)
433
499
  precision = self.precision
434
500
  elif traitname == 'eva':
435
501
  func = self.calc_eva
@@ -465,45 +531,44 @@ class PowerSpectra(BaseSpectra):
465
531
  self.h5f.flush()
466
532
  return ac
467
533
 
468
- @property_depends_on('digest')
534
+ @property_depends_on(['digest'])
469
535
  def _get_csm(self):
470
- """Main work is done here:
471
- Cross spectral matrix is either loaded from cache file or
472
- calculated and then additionally stored into cache.
473
- """
474
- self._handle_dual_calibration()
475
536
  if config.global_caching == 'none' or (config.global_caching == 'individual' and self.cached is False):
476
537
  return self.calc_csm()
477
538
  return self._get_filecache('csm')
478
539
 
479
- @property_depends_on('digest')
540
+ @property_depends_on(['digest'])
480
541
  def _get_eva(self):
481
- """Eigenvalues of cross spectral matrix are either loaded from cache file or
482
- calculated and then additionally stored into cache.
483
- """
484
542
  if config.global_caching == 'none' or (config.global_caching == 'individual' and self.cached is False):
485
543
  return self.calc_eva()
486
544
  return self._get_filecache('eva')
487
545
 
488
- @property_depends_on('digest')
546
+ @property_depends_on(['digest'])
489
547
  def _get_eve(self):
490
- """Eigenvectors of cross spectral matrix are either loaded from cache file or
491
- calculated and then additionally stored into cache.
492
- """
493
548
  if config.global_caching == 'none' or (config.global_caching == 'individual' and self.cached is False):
494
549
  return self.calc_eve()
495
550
  return self._get_filecache('eve')
496
551
 
497
552
  def synthetic_ev(self, freq, num=0):
498
- """Return synthesized frequency band values of the eigenvalues.
553
+ """
554
+ Retrieve synthetic eigenvalues for a specified frequency or frequency range.
555
+
556
+ This method calculates the eigenvalues of the CSM for a single frequency or a synthetic
557
+ frequency range. If ``num`` is set to ``0``, it retrieves the eigenvalues at the exact
558
+ frequency. Otherwise, it averages eigenvalues across a range determined by ``freq`` and
559
+ ``num``.
499
560
 
500
561
  Parameters
501
562
  ----------
502
- freq : float
503
- Band center frequency for which to return the results.
504
- num : integer
505
- Controls the width of the frequency bands considered; defaults to
506
- 3 (third-octave band).
563
+ freq : :class:`float`
564
+ The target frequency for which the eigenvalues are calculated. This is the center
565
+ frequency for synthetic averaging.
566
+ num : :class:`int`, optional
567
+ The number of subdivisions in the logarithmic frequency space around the center
568
+ frequency ``freq``.
569
+
570
+ - ``0`` (default): Only the eigenvalues for the exact frequency line are returned.
571
+ - Non-zero:
507
572
 
508
573
  === =====================
509
574
  num frequency band width
@@ -516,10 +581,25 @@ class PowerSpectra(BaseSpectra):
516
581
 
517
582
  Returns
518
583
  -------
519
- float
520
- Synthesized frequency band value of the eigenvalues (the sum of
521
- all values that are contained in the band).
522
-
584
+ :obj:`numpy.ndarray`
585
+ An array of eigenvalues. If ``num == 0``, the eigenvalues for the single frequency are
586
+ returned. For ``num > 0``, a summed array of eigenvalues across the synthetic frequency
587
+ range is returned.
588
+
589
+ Examples
590
+ --------
591
+ >>> import numpy as np
592
+ >>> from acoular import TimeSamples
593
+ >>> from acoular.spectra import PowerSpectra
594
+ >>> np.random.seed(0)
595
+ >>>
596
+ >>> data = np.random.rand(1000, 4)
597
+ >>> ts = TimeSamples(data=data, sample_freq=51200)
598
+ >>> ps = PowerSpectra(source=ts, block_size=128, window='Hamming')
599
+ >>> ps.synthetic_ev(freq=5000, num=5)
600
+ array([0.00048803, 0.0010141 , 0.00234248, 0.00457097])
601
+ >>> ps.synthetic_ev(freq=5000)
602
+ array([0.00022468, 0.0004589 , 0.00088059, 0.00245989])
523
603
  """
524
604
  f = self.fftfreq()
525
605
  if num == 0:
@@ -532,169 +612,67 @@ class PowerSpectra(BaseSpectra):
532
612
  return sum(self.eva[f1:f2], 0)
533
613
 
534
614
 
535
- def synthetic(data, freqs, f, num=3):
536
- """Returns synthesized frequency band values of spectral data.
537
-
538
- If used with :meth:`Beamformer.result()<acoular.fbeamform.BeamformerBase.result>`
539
- and only one frequency band, the output is identical to the result of the intrinsic
540
- :meth:`Beamformer.synthetic<acoular.fbeamform.BeamformerBase.synthetic>` method.
541
- It can, however, also be used with the
542
- :meth:`Beamformer.integrate<acoular.fbeamform.BeamformerBase.integrate>`
543
- output and more frequency bands.
544
-
545
- Parameters
546
- ----------
547
- data : array of floats
548
- The spectral data (squared sound pressures in Pa^2) in an array with one value
549
- per frequency line.
550
- The number of entries must be identical to the number of
551
- grid points.
552
- freq : array of floats
553
- The frequencies that correspon to the input *data* (as yielded by
554
- the :meth:`PowerSpectra.fftfreq<acoular.spectra.PowerSpectra.fftfreq>`
555
- method).
556
- f : float or list of floats
557
- Band center frequency/frequencies for which to return the results.
558
- num : integer
559
- Controls the width of the frequency bands considered; defaults to
560
- 3 (third-octave band).
561
-
562
- === =====================
563
- num frequency band width
564
- === =====================
565
- 0 single frequency line
566
- 1 octave band
567
- 3 third-octave band
568
- n 1/n-octave band
569
- === =====================
570
-
571
- Returns
572
- -------
573
- array of floats
574
- Synthesized frequency band values of the beamforming result at
575
- each grid point (the sum of all values that are contained in the band).
576
- Note that the frequency resolution and therefore the bandwidth
577
- represented by a single frequency line depends on
578
- the :attr:`sampling frequency<acoular.base.SamplesGenerator.sample_freq>`
579
- and used :attr:`FFT block size<acoular.spectra.PowerSpectra.block_size>`.
580
-
581
- """
582
- if isscalar(f):
583
- f = (f,)
584
- if num == 0:
585
- # single frequency lines
586
- res = []
587
- for i in f:
588
- ind = searchsorted(freqs, i)
589
- if ind >= len(freqs):
590
- warn(
591
- 'Queried frequency (%g Hz) not in resolved frequency range. Returning zeros.' % i,
592
- Warning,
593
- stacklevel=2,
594
- )
595
- h = zeros_like(data[0])
596
- else:
597
- if freqs[ind] != i:
598
- warn(
599
- f'Queried frequency ({i:g} Hz) not in set of '
600
- 'discrete FFT sample frequencies. '
601
- f'Using frequency {freqs[ind]:g} Hz instead.',
602
- Warning,
603
- stacklevel=2,
604
- )
605
- h = data[ind]
606
- res += [h]
607
- else:
608
- # fractional octave bands
609
- res = []
610
- for i in f:
611
- f1 = i * 2.0 ** (-0.5 / num)
612
- f2 = i * 2.0 ** (+0.5 / num)
613
- ind1 = searchsorted(freqs, f1)
614
- ind2 = searchsorted(freqs, f2)
615
- if ind1 == ind2:
616
- warn(
617
- f'Queried frequency band ({f1:g} to {f2:g} Hz) does not '
618
- 'include any discrete FFT sample frequencies. '
619
- 'Returning zeros.',
620
- Warning,
621
- stacklevel=2,
622
- )
623
- h = zeros_like(data[0])
624
- else:
625
- h = sum(data[ind1:ind2], 0)
626
- res += [h]
627
- return array(res)
628
-
629
-
630
615
  class PowerSpectraImport(PowerSpectra):
631
- """Provides a dummy class for using pre-calculated cross-spectral
632
- matrices.
633
-
634
- This class does not calculate the cross-spectral matrix. Instead,
635
- the user can inject one or multiple existing CSMs by setting the
636
- :attr:`csm` attribute. This can be useful when algorithms shall be
637
- evaluated with existing CSM matrices.
638
- The frequency or frequencies contained by the CSM must be set via the
639
- attr:`frequencies` attribute. The attr:`numchannels` attributes
640
- is determined on the basis of the CSM shape.
641
- In contrast to the PowerSpectra object, the attributes
642
- :attr:`sample_freq`, :attr:`time_data`, :attr:`source`,
643
- :attr:`block_size`, :attr:`calib`, :attr:`window`,
644
- :attr:`overlap`, :attr:`cached`, and :attr:`num_blocks`
645
- have no functionality.
616
+ """
617
+ Provides a dummy class for using pre-calculated CSMs.
618
+
619
+ This class does not calculate the CSM. Instead, the user can inject one or multiple existing
620
+ CSMs by setting the :attr:`csm` attribute. This can be useful when algorithms shall be
621
+ evaluated with existing CSMs. The frequency or frequencies contained by the CSM must be set via
622
+ the :attr:`frequencies` attribute. The attr:`num_channels` attributes is determined on the basis
623
+ of the CSM shape. In contrast to the :class:`PowerSpectra` object, the attributes
624
+ :attr:`sample_freq`, :attr:`source`, :attr:`block_size`, :attr:`window`, :attr:`overlap`,
625
+ :attr:`cached`, and :attr:`num_blocks` have no functionality.
646
626
  """
647
627
 
648
- #: The cross spectral matrix,
649
- #: (number of frequencies, numchannels, numchannels) array of complex;
628
+ #: The cross-spectral matrix stored in an array of shape ``(n, m, m)`` of complex for ``n``
629
+ #: frequencies and ``m`` channels.
650
630
  csm = Property(desc='cross spectral matrix')
651
631
 
652
- #: frequencies included in the cross-spectral matrix in ascending order.
653
- #: Compound trait that accepts arguments of type list, array, and float
654
- frequencies = Trait(None, (CArray, Float), desc='frequencies included in the cross-spectral matrix')
655
-
656
- #: Number of time data channels
657
- numchannels = Property(depends_on=['digest'])
632
+ #: The frequencies included in the CSM in ascending order. Accepts list, array, or a single
633
+ #: float value.
634
+ frequencies = Union(CArray, Float, desc='frequencies included in the cross-spectral matrix')
658
635
 
659
- time_data = Enum(None, desc='PowerSpectraImport cannot consume time data')
636
+ #: Number of time data channels, inferred from the shape of the CSM.
637
+ num_channels = Property(depends_on=['digest'])
660
638
 
639
+ #: :class:`PowerSpectraImport` does not consume time data; source is always ``None``.
661
640
  source = Enum(None, desc='PowerSpectraImport cannot consume time data')
662
641
 
663
- # Sampling frequency of the signal, defaults to None
642
+ #: Sampling frequency of the signal. Default is ``None``
664
643
  sample_freq = Enum(None, desc='sampling frequency')
665
644
 
645
+ #: Block size for FFT, non-functional in this class.
666
646
  block_size = Enum(None, desc='PowerSpectraImport does not operate on blocks of time data')
667
647
 
668
- calib = Enum(None, desc='PowerSpectraImport cannot calibrate the time data')
669
-
648
+ #: Windowing method, non-functional in this class.
670
649
  window = Enum(None, desc='PowerSpectraImport does not perform windowing')
671
650
 
651
+ #: Overlap between blocks, non-functional in this class.
672
652
  overlap = Enum(None, desc='PowerSpectraImport does not consume time data')
673
653
 
654
+ #: Caching capability, always disabled.
674
655
  cached = Enum(False, desc='PowerSpectraImport has no caching capabilities')
675
656
 
657
+ #: Number of FFT blocks, always ``None``.
676
658
  num_blocks = Enum(None, desc='PowerSpectraImport cannot determine the number of blocks')
677
659
 
678
660
  # Shadow trait, should not be set directly, for internal use.
679
661
  _ind_low = Int(0, desc='index of lowest frequency line')
680
662
 
681
663
  # Shadow trait, should not be set directly, for internal use.
682
- _ind_high = Trait(None, (Int, None), desc='index of highest frequency line')
664
+ _ind_high = Union(None, Int, desc='index of highest frequency line')
683
665
 
684
- # internal identifier
685
- digest = Property(
686
- depends_on=[
687
- '_csmsum',
688
- ],
689
- )
666
+ #: A unique identifier for the spectra, based on its properties. (read-only)
667
+ digest = Property(depends_on=['_csmsum'])
690
668
 
691
- #: Name of the cache file without extension, readonly.
692
- basename = Property(depends_on='digest', desc='basename for cache file')
669
+ #: Name of the cache file without extension. (read-only)
670
+ basename = Property(depends_on=['digest'], desc='basename for cache file')
693
671
 
694
- # csm shadow trait, only for internal use.
672
+ # Shadow trait for storing the CSM, for internal use only.
695
673
  _csm = CArray()
696
674
 
697
- # CSM checksum to trigger digest calculation, only for internal use.
675
+ # Checksum for the CSM to trigger digest calculation, for internal use only.
698
676
  _csmsum = Float()
699
677
 
700
678
  def _get_basename(self):
@@ -704,7 +682,7 @@ class PowerSpectraImport(PowerSpectra):
704
682
  def _get_digest(self):
705
683
  return digest(self)
706
684
 
707
- def _get_numchannels(self):
685
+ def _get_num_channels(self):
708
686
  return self.csm.shape[1]
709
687
 
710
688
  def _get_csm(self):
@@ -712,33 +690,31 @@ class PowerSpectraImport(PowerSpectra):
712
690
 
713
691
  def _set_csm(self, csm):
714
692
  if (len(csm.shape) != 3) or (csm.shape[1] != csm.shape[2]):
715
- msg = 'The cross spectral matrix must have the following shape: (number of frequencies, numchannels, numchannels)!'
693
+ msg = 'The cross spectral matrix must have the following shape: \
694
+ (number of frequencies, num_channels, num_channels)!'
716
695
  raise ValueError(msg)
717
696
  self._csmsum = real(self._csm).sum() + (imag(self._csm) ** 2).sum() # to trigger new digest creation
718
697
  self._csm = csm
719
698
 
720
- @property_depends_on('digest')
699
+ @property_depends_on(['digest'])
721
700
  def _get_eva(self):
722
- """Eigenvalues of cross spectral matrix are either loaded from cache file or
723
- calculated and then additionally stored into cache.
724
- """
725
701
  return self.calc_eva()
726
702
 
727
- @property_depends_on('digest')
703
+ @property_depends_on(['digest'])
728
704
  def _get_eve(self):
729
- """Eigenvectors of cross spectral matrix are either loaded from cache file or
730
- calculated and then additionally stored into cache.
731
- """
732
705
  return self.calc_eve()
733
706
 
734
707
  def fftfreq(self):
735
- """Return the Discrete Fourier Transform sample frequencies.
708
+ """
709
+ Return the Discrete Fourier Transform sample frequencies.
710
+
711
+ The method checks the type of :attr:`frequencies` and returns the corresponding frequency
712
+ array. If :attr:`frequencies` is not defined, a warning is raised.
736
713
 
737
714
  Returns
738
715
  -------
739
- f : ndarray
716
+ :obj:`numpy.ndarray`
740
717
  Array containing the frequencies.
741
-
742
718
  """
743
719
  if isinstance(self.frequencies, float):
744
720
  return array([self.frequencies])