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/fbeamform.py CHANGED
@@ -55,10 +55,11 @@ from numpy import (
55
55
  full,
56
56
  hsplit,
57
57
  hstack,
58
+ index_exp,
58
59
  inf,
60
+ integer,
59
61
  invert,
60
62
  isscalar,
61
- linalg,
62
63
  log10,
63
64
  ndarray,
64
65
  newaxis,
@@ -80,16 +81,14 @@ from numpy import (
80
81
  zeros,
81
82
  zeros_like,
82
83
  )
83
- from numpy.linalg import norm
84
84
  from packaging.version import parse
85
- from scipy.linalg import eigh, eigvals, fractional_matrix_power, inv
85
+ from scipy.linalg import eigh, eigvals, fractional_matrix_power, inv, norm
86
86
  from scipy.optimize import fmin_l_bfgs_b, linprog, nnls, shgo
87
87
  from sklearn.linear_model import LassoLars, LassoLarsCV, LassoLarsIC, LinearRegression, OrthogonalMatchingPursuitCV
88
88
  from traits.api import (
89
89
  Any,
90
90
  Bool,
91
91
  CArray,
92
- Delegate,
93
92
  Dict,
94
93
  Enum,
95
94
  Float,
@@ -116,14 +115,20 @@ from .h5files import H5CacheFileBase
116
115
  from .internal import digest
117
116
  from .microphones import MicGeom
118
117
  from .spectra import PowerSpectra
118
+ from .tfastfuncs import _steer_I, _steer_II, _steer_III, _steer_IV
119
119
 
120
120
  sklearn_ndict = {}
121
121
  if parse(sklearn.__version__) < parse('1.4'):
122
122
  sklearn_ndict['normalize'] = False
123
123
 
124
+ BEAMFORMER_BASE_DIGEST_DEPENDENCIES = ['freq_data.digest', 'r_diag', 'r_diag_norm', 'precision', '_steer_obj.digest']
125
+
124
126
 
125
127
  class SteeringVector(HasPrivateTraits):
126
- """Basic class for implementing steering vectors with monopole source transfer models."""
128
+ """Basic class for implementing steering vectors with monopole source transfer models.
129
+
130
+ Handles four different steering vector formulations. See :cite:`Sarradj2012` for details.
131
+ """
127
132
 
128
133
  #: :class:`~acoular.grids.Grid`-derived object that provides the grid locations.
129
134
  grid = Trait(Grid, desc='beamforming grid')
@@ -131,7 +136,7 @@ class SteeringVector(HasPrivateTraits):
131
136
  #: :class:`~acoular.microphones.MicGeom` object that provides the microphone locations.
132
137
  mics = Trait(MicGeom, desc='microphone geometry')
133
138
 
134
- #: Type of steering vectors, see also :ref:`Sarradj, 2012<Sarradj2012>`. Defaults to 'true level'.
139
+ #: Type of steering vectors, see also :cite:`Sarradj2012`. Defaults to 'true level'.
135
140
  steer_type = Trait('true level', 'true location', 'classic', 'inverse', desc='type of steering vectors used')
136
141
 
137
142
  #: :class:`~acoular.environments.Environment` or derived object,
@@ -139,14 +144,6 @@ class SteeringVector(HasPrivateTraits):
139
144
  #: Defaults to standard :class:`~acoular.environments.Environment` object.
140
145
  env = Instance(Environment(), Environment)
141
146
 
142
- # TODO: add caching capability for transfer function
143
- # Flag, if "True" (not default), the transfer function is
144
- # cached in h5 files and does not have to be recomputed during subsequent
145
- # program runs.
146
- # Be aware that setting this to "True" may result in high memory usage.
147
- # cached = Bool(False,
148
- # desc="cache flag for transfer function")
149
-
150
147
  # Sound travel distances from microphone array center to grid
151
148
  # points or reference position (readonly). Feature may change.
152
149
  r0 = Property(desc='array center to grid distances')
@@ -165,12 +162,32 @@ class SteeringVector(HasPrivateTraits):
165
162
  #: Defaults to [0.,0.,0.].
166
163
  ref = Property(desc='reference position or distance')
167
164
 
165
+ _steer_funcs_freq = Dict(
166
+ {
167
+ 'classic': lambda x: x / absolute(x) / x.shape[-1],
168
+ 'inverse': lambda x: 1.0 / x.conj() / x.shape[-1],
169
+ 'true level': lambda x: x / einsum('ij,ij->i', x, x.conj())[:, newaxis],
170
+ 'true location': lambda x: x / sqrt(einsum('ij,ij->i', x, x.conj()) * x.shape[-1])[:, newaxis],
171
+ },
172
+ desc='dictionary of frequency domain steering vector functions',
173
+ )
174
+
175
+ _steer_funcs_time = Dict(
176
+ {
177
+ 'classic': _steer_I,
178
+ 'inverse': _steer_II,
179
+ 'true level': _steer_III,
180
+ 'true location': _steer_IV,
181
+ },
182
+ desc='dictionary of time domain steering vector functions',
183
+ )
184
+
168
185
  def _set_ref(self, ref):
169
186
  if isscalar(ref):
170
187
  try:
171
188
  self._ref = absolute(float(ref))
172
- except:
173
- raise TraitError(args=self, name='ref', info='Float or CArray(3,)', value=ref)
189
+ except ValueError as ve:
190
+ raise TraitError(args=self, name='ref', info='Float or CArray(3,)', value=ref) from ve
174
191
  elif len(ref) == 3:
175
192
  self._ref = array(ref, dtype=float)
176
193
  else:
@@ -191,11 +208,11 @@ class SteeringVector(HasPrivateTraits):
191
208
  if self.ref > 0:
192
209
  return full((self.grid.size,), self.ref)
193
210
  return self.env._r(self.grid.pos())
194
- return self.env._r(self.grid.pos(), self.ref[:, newaxis])
211
+ return self.env._r(self.grid.gpos, self.ref[:, newaxis])
195
212
 
196
213
  @property_depends_on('grid.digest, mics.digest, env.digest')
197
214
  def _get_rm(self):
198
- return atleast_2d(self.env._r(self.grid.pos(), self.mics.mpos))
215
+ return atleast_2d(self.env._r(self.grid.gpos, self.mics.mpos))
199
216
 
200
217
  @cached_property
201
218
  def _get_digest(self):
@@ -236,8 +253,9 @@ class SteeringVector(HasPrivateTraits):
236
253
  return trans
237
254
 
238
255
  def steer_vector(self, f, ind=None):
239
- """Calculates the steering vectors based on the transfer function
240
- See also :ref:`Sarradj, 2012<Sarradj2012>`.
256
+ """Calculates the steering vectors based on the transfer function.
257
+
258
+ See also :cite:`Sarradj2012`.
241
259
 
242
260
  Parameters
243
261
  ----------
@@ -254,15 +272,38 @@ class SteeringVector(HasPrivateTraits):
254
272
  array of shape (ngridpts, nmics) containing the steering vectors for the given frequency
255
273
 
256
274
  """
257
- func = {
258
- 'classic': lambda x: x / absolute(x) / x.shape[-1],
259
- 'inverse': lambda x: 1.0 / x.conj() / x.shape[-1],
260
- 'true level': lambda x: x / einsum('ij,ij->i', x, x.conj())[:, newaxis],
261
- 'true location': lambda x: x / sqrt(einsum('ij,ij->i', x, x.conj()) * x.shape[-1])[:, newaxis],
262
- }[self.steer_type]
275
+ func = self._steer_funcs_freq[self.steer_type]
263
276
  return func(self.transfer(f, ind))
264
277
 
265
278
 
279
+ class LazyBfResult:
280
+ """Manages lazy per-frequency evaluation."""
281
+
282
+ # Internal helper class which works together with BeamformerBase to provide
283
+ # calculation on demand; provides an 'intelligent' [] operator. This is
284
+ # implemented as an extra class instead of as a method of BeamformerBase to
285
+ # properly control the BeamformerBase.result attribute. Might be migrated to
286
+ # be a method of BeamformerBase in the future.
287
+
288
+ def __init__(self, bf):
289
+ self.bf = bf
290
+
291
+ def __getitem__(self, key):
292
+ # 'intelligent' [] operator checks if results are available and triggers calculation
293
+ sl = index_exp[key][0]
294
+ if isinstance(sl, (int, integer)):
295
+ sl = slice(sl, sl + 1)
296
+ # indices which are missing
297
+ missingind = arange(*sl.indices(self.bf._numfreq))[self.bf._fr[sl] == 0]
298
+ # calc if needed
299
+ if missingind.size:
300
+ self.bf._calc(missingind)
301
+ if self.bf.h5f:
302
+ self.bf.h5f.flush()
303
+
304
+ return self.bf._ac.__getitem__(key)
305
+
306
+
266
307
  class BeamformerBase(HasPrivateTraits):
267
308
  """Beamforming using the basic delay-and-sum algorithm in the frequency domain."""
268
309
 
@@ -282,10 +323,15 @@ class BeamformerBase(HasPrivateTraits):
282
323
  if isinstance(steer, SteeringVector):
283
324
  self._steer_obj = steer
284
325
  elif steer in ('true level', 'true location', 'classic', 'inverse'):
285
- # Type of steering vectors, see also :ref:`Sarradj, 2012<Sarradj2012>`.
326
+ # Type of steering vectors, see also :cite:`Sarradj2012`.
327
+ msg = (
328
+ "Deprecated use of 'steer' trait. Please use the 'steer' with an object of class "
329
+ "'SteeringVector'. Using a string to specify the steer type will be removed in "
330
+ 'version 25.01.'
331
+ )
286
332
  warn(
287
- "Deprecated use of 'steer' trait. Please use object of class 'SteeringVector' in the future.",
288
- Warning,
333
+ msg,
334
+ DeprecationWarning,
289
335
  stacklevel=2,
290
336
  )
291
337
  self._steer_obj.steer_type = steer
@@ -303,7 +349,11 @@ class BeamformerBase(HasPrivateTraits):
303
349
  return self._steer_obj.env
304
350
 
305
351
  def _set_env(self, env):
306
- warn("Deprecated use of 'env' trait. ", Warning, stacklevel=2)
352
+ msg = (
353
+ "Deprecated use of 'env' trait. Please use the 'steer' trait with an object of class"
354
+ "'SteeringVector'. The 'env' trait will be removed in version 25.01."
355
+ )
356
+ warn(msg, DeprecationWarning, stacklevel=2)
307
357
  self._steer_obj.env = env
308
358
 
309
359
  # The speed of sound.
@@ -315,7 +365,12 @@ class BeamformerBase(HasPrivateTraits):
315
365
  return self._steer_obj.env.c
316
366
 
317
367
  def _set_c(self, c):
318
- warn("Deprecated use of 'c' trait. ", Warning, stacklevel=2)
368
+ msg = (
369
+ "Deprecated use of 'c' trait. Please use the 'steer' trait with an object of class"
370
+ "'SteeringVector' that holds an 'Environment' instance."
371
+ "The 'c' trait will be removed in version 25.01."
372
+ )
373
+ warn(msg, DeprecationWarning, stacklevel=2)
319
374
  self._steer_obj.env.c = c
320
375
 
321
376
  # :class:`~acoular.grids.Grid`-derived object that provides the grid locations.
@@ -327,7 +382,11 @@ class BeamformerBase(HasPrivateTraits):
327
382
  return self._steer_obj.grid
328
383
 
329
384
  def _set_grid(self, grid):
330
- warn("Deprecated use of 'grid' trait. ", Warning, stacklevel=2)
385
+ msg = (
386
+ "Deprecated use of 'grid' trait. Please use the 'steer' trait with an object of class"
387
+ "'SteeringVector'. The 'grid' trait will be removed in version 25.01."
388
+ )
389
+ warn(msg, DeprecationWarning, stacklevel=2)
331
390
  self._steer_obj.grid = grid
332
391
 
333
392
  # :class:`~acoular.microphones.MicGeom` object that provides the microphone locations.
@@ -339,7 +398,11 @@ class BeamformerBase(HasPrivateTraits):
339
398
  return self._steer_obj.mics
340
399
 
341
400
  def _set_mpos(self, mpos):
342
- warn("Deprecated use of 'mpos' trait. ", Warning, stacklevel=2)
401
+ msg = (
402
+ "Deprecated use of 'mpos' trait. Please use the 'steer' trait with an object of class"
403
+ "'SteeringVector'. The 'mpos' trait will be removed in version 25.01."
404
+ )
405
+ warn(msg, DeprecationWarning, stacklevel=2)
343
406
  self._steer_obj.mics = mpos
344
407
 
345
408
  # Sound travel distances from microphone array center to grid points (r0)
@@ -349,11 +412,21 @@ class BeamformerBase(HasPrivateTraits):
349
412
  r0 = Property()
350
413
 
351
414
  def _get_r0(self):
415
+ msg = (
416
+ "Deprecated use of 'r0' trait. Please use the 'steer' trait with an object of class"
417
+ "'SteeringVector'. The 'r0' trait will be removed in version 25.01."
418
+ )
419
+ warn(msg, DeprecationWarning, stacklevel=2)
352
420
  return self._steer_obj.r0
353
421
 
354
422
  rm = Property()
355
423
 
356
424
  def _get_rm(self):
425
+ msg = (
426
+ "Deprecated use of 'rm' trait. Please use the 'steer' trait with an object of class"
427
+ "'SteeringVector'. The 'rm' trait will be removed in version 25.01."
428
+ )
429
+ warn(msg, DeprecationWarning, stacklevel=2)
357
430
  return self._steer_obj.rm
358
431
 
359
432
  # --- End of backwards compatibility traits --------------------------------------
@@ -387,25 +460,17 @@ class BeamformerBase(HasPrivateTraits):
387
460
 
388
461
  #: The beamforming result as squared sound pressure values
389
462
  #: at all grid point locations (readonly).
390
- #: Returns a (number of frequencies, number of gridpoints) array of floats.
463
+ #: Returns a (number of frequencies, number of gridpoints) array-like
464
+ #: of floats. Values can only be accessed via the index operator [].
391
465
  result = Property(desc='beamforming result')
392
466
 
393
467
  # internal identifier
394
- digest = Property(depends_on=['freq_data.digest', 'r_diag', 'r_diag_norm', 'precision', '_steer_obj.digest'])
395
-
396
- # internal identifier
397
- ext_digest = Property(
398
- depends_on=['digest', 'freq_data.ind_low', 'freq_data.ind_high'],
399
- )
468
+ digest = Property(depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES)
400
469
 
401
470
  @cached_property
402
471
  def _get_digest(self):
403
472
  return digest(self)
404
473
 
405
- @cached_property
406
- def _get_ext_digest(self):
407
- return digest(self, 'ext_digest')
408
-
409
474
  def _get_filecache(self):
410
475
  """Function collects cached results from file depending on
411
476
  global/local caching behaviour. Returns (None, None) if no cachefile/data
@@ -438,6 +503,13 @@ class BeamformerBase(HasPrivateTraits):
438
503
  if isinstance(self, BeamformerAdaptiveGrid):
439
504
  self.h5f.create_compressible_array('gpos', (3, self.size), 'float64', group)
440
505
  self.h5f.create_compressible_array('result', (numfreq, self.size), self.precision, group)
506
+ elif isinstance(self, BeamformerSODIX):
507
+ self.h5f.create_compressible_array(
508
+ 'result',
509
+ (numfreq, self.steer.grid.size * self.steer.mics.num_mics),
510
+ self.precision,
511
+ group,
512
+ )
441
513
  else:
442
514
  self.h5f.create_compressible_array('result', (numfreq, self.steer.grid.size), self.precision, group)
443
515
 
@@ -454,58 +526,51 @@ class BeamformerBase(HasPrivateTraits):
454
526
  if numchannels != self.steer.mics.num_mics or numchannels == 0:
455
527
  raise ValueError('%i channels do not fit %i mics' % (numchannels, self.steer.mics.num_mics))
456
528
 
457
- @property_depends_on('ext_digest')
529
+ @property_depends_on('digest')
458
530
  def _get_result(self):
459
531
  """Implements the :attr:`result` getter routine.
460
532
  The beamforming result is either loaded or calculated.
461
533
  """
462
- f = self.freq_data
463
- numfreq = f.fftfreq().shape[0] # block_size/2 + 1steer_obj
464
- _digest = ''
465
- while self.digest != _digest:
466
- _digest = self.digest
467
- self._assert_equal_channels()
468
- ac, fr = (None, None)
469
- if not ( # if result caching is active
470
- config.global_caching == 'none' or (config.global_caching == 'individual' and not self.cached)
471
- ):
472
- # print("get filecache..")
473
- (ac, fr, gpos) = self._get_filecache()
474
- if gpos:
475
- self._gpos = gpos
476
- if ac and fr:
477
- # print("cached data existent")
478
- if not fr[f.ind_low : f.ind_high].all():
479
- # print("calculate missing results")
480
- if config.global_caching == 'readonly':
481
- (ac, fr) = (ac[:], fr[:])
482
- self.calc(ac, fr)
483
- self.h5f.flush()
484
- # else:
485
- # print("cached results are complete! return.")
534
+ # store locally for performance
535
+ self._f = self.freq_data.fftfreq()
536
+ self._numfreq = self._f.shape[0]
537
+ self._assert_equal_channels()
538
+ ac, fr = (None, None)
539
+ if not ( # if result caching is active
540
+ config.global_caching == 'none' or (config.global_caching == 'individual' and not self.cached)
541
+ ):
542
+ (ac, fr, gpos) = self._get_filecache() # can also be (None, None, None)
543
+ if gpos: # we have an adaptive grid
544
+ self._gpos = gpos
545
+ if ac and fr: # cached data is available
546
+ if config.global_caching == 'readonly':
547
+ (ac, fr) = (ac[:], fr[:]) # so never write back to disk
548
+ else:
549
+ # no caching or not activated, init numpy arrays
550
+ if isinstance(self, BeamformerAdaptiveGrid):
551
+ self._gpos = zeros((3, self.size), dtype=self.precision)
552
+ ac = zeros((self._numfreq, self.size), dtype=self.precision)
553
+ elif isinstance(self, BeamformerSODIX):
554
+ ac = zeros((self._numfreq, self.steer.grid.size * self.steer.mics.num_mics), dtype=self.precision)
486
555
  else:
487
- # print("no caching or not activated, calculate result")
488
- if isinstance(self, BeamformerAdaptiveGrid):
489
- self._gpos = zeros((3, self.size), dtype=self.precision)
490
- ac = zeros((numfreq, self.size), dtype=self.precision)
491
- else:
492
- ac = zeros((numfreq, self.steer.grid.size), dtype=self.precision)
493
- fr = zeros(numfreq, dtype='int8')
494
- self.calc(ac, fr)
495
- return ac
556
+ ac = zeros((self._numfreq, self.steer.grid.size), dtype=self.precision)
557
+ fr = zeros(self._numfreq, dtype='int8')
558
+ self._ac = ac
559
+ self._fr = fr
560
+ return LazyBfResult(self)
496
561
 
497
562
  def sig_loss_norm(self):
498
563
  """If the diagonal of the CSM is removed one has to handle the loss
499
564
  of signal energy --> Done via a normalization factor.
500
565
  """
501
566
  if not self.r_diag: # Full CSM --> no normalization needed
502
- normFactor = 1.0
567
+ normfactor = 1.0
503
568
  elif self.r_diag_norm == 0.0: # Removed diag: standard normalization factor
504
569
  nMics = float(self.freq_data.numchannels)
505
- normFactor = nMics / (nMics - 1)
570
+ normfactor = nMics / (nMics - 1)
506
571
  elif self.r_diag_norm != 0.0: # Removed diag: user defined normalization factor
507
- normFactor = self.r_diag_norm
508
- return normFactor
572
+ normfactor = self.r_diag_norm
573
+ return normfactor
509
574
 
510
575
  def _beamformer_params(self):
511
576
  """Manages the parameters for calling of the core beamformer functionality.
@@ -528,9 +593,8 @@ class BeamformerBase(HasPrivateTraits):
528
593
  param_steer_func = self.steer.steer_vector
529
594
  return param_type, param_steer_func
530
595
 
531
- def calc(self, ac, fr):
532
- """Calculates the delay-and-sum beamforming result for the frequencies
533
- defined by :attr:`freq_data`.
596
+ def _calc(self, ind):
597
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
534
598
 
535
599
  This is an internal helper function that is automatically called when
536
600
  accessing the beamformer's :attr:`result` or calling
@@ -538,39 +602,33 @@ class BeamformerBase(HasPrivateTraits):
538
602
 
539
603
  Parameters
540
604
  ----------
541
- ac : array of floats
542
- This array of dimension ([number of frequencies]x[number of gridpoints])
543
- is used as call-by-reference parameter and contains the calculated
544
- value after calling this method.
545
- fr : array of booleans
546
- The entries of this [number of frequencies]-sized array are either
547
- 'True' (if the result for this frequency has already been calculated)
548
- or 'False' (for the frequencies where the result has yet to be calculated).
549
- After the calculation at a certain frequency the value will be set
550
- to 'True'
605
+ ind : array of int
606
+ This array contains all frequency indices for which (re)calculation is
607
+ to be performed
551
608
 
552
609
  Returns
553
610
  -------
554
- This method only returns values through the *ac* and *fr* parameters
611
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
555
612
 
556
613
  """
557
- f = self.freq_data.fftfreq() # [inds]
614
+ f = self._f
615
+ normfactor = self.sig_loss_norm()
558
616
  param_steer_type, steer_vector = self._beamformer_params()
559
- for i in self.freq_data.indices:
560
- if not fr[i]:
561
- csm = array(self.freq_data.csm[i], dtype='complex128')
562
- beamformerOutput = beamformerFreq(
563
- param_steer_type,
564
- self.r_diag,
565
- self.sig_loss_norm(),
566
- steer_vector(f[i]),
567
- csm,
568
- )[0]
569
- if self.r_diag: # set (unphysical) negative output values to 0
570
- indNegSign = sign(beamformerOutput) < 0
571
- beamformerOutput[indNegSign] = 0.0
572
- ac[i] = beamformerOutput
573
- fr[i] = 1
617
+ for i in ind:
618
+ # print(f'compute{i}')
619
+ csm = array(self.freq_data.csm[i], dtype='complex128')
620
+ beamformerOutput = beamformerFreq(
621
+ param_steer_type,
622
+ self.r_diag,
623
+ normfactor,
624
+ steer_vector(f[i]),
625
+ csm,
626
+ )[0]
627
+ if self.r_diag: # set (unphysical) negative output values to 0
628
+ indNegSign = sign(beamformerOutput) < 0
629
+ beamformerOutput[indNegSign] = 0.0
630
+ self._ac[i] = beamformerOutput
631
+ self._fr[i] = 1
574
632
 
575
633
  def synthetic(self, f, num=0):
576
634
  """Evaluates the beamforming result for an arbitrary frequency band.
@@ -599,7 +657,7 @@ class BeamformerBase(HasPrivateTraits):
599
657
  each grid point .
600
658
  Note that the frequency resolution and therefore the bandwidth
601
659
  represented by a single frequency line depends on
602
- the :attr:`sampling frequency<acoular.tprocess.SamplesGenerator.sample_freq>` and
660
+ the :attr:`sampling frequency<acoular.base.SamplesGenerator.sample_freq>` and
603
661
  used :attr:`FFT block size<acoular.spectra.PowerSpectra.block_size>`.
604
662
 
605
663
  """
@@ -608,8 +666,6 @@ class BeamformerBase(HasPrivateTraits):
608
666
  if len(freq) == 0:
609
667
  return None
610
668
 
611
- indices = self.freq_data.indices
612
-
613
669
  if num == 0:
614
670
  # single frequency line
615
671
  ind = searchsorted(freq, f)
@@ -629,14 +685,6 @@ class BeamformerBase(HasPrivateTraits):
629
685
  Warning,
630
686
  stacklevel=2,
631
687
  )
632
- if ind not in indices:
633
- warn(
634
- 'Beamforming result may not have been calculated '
635
- 'for queried frequency. Check '
636
- 'freq_data.ind_low and freq_data.ind_high!',
637
- Warning,
638
- stacklevel=2,
639
- )
640
688
  h = res[ind]
641
689
  else:
642
690
  # fractional octave band
@@ -659,46 +707,56 @@ class BeamformerBase(HasPrivateTraits):
659
707
  h = zeros_like(res[0])
660
708
  else:
661
709
  h = sum(res[ind1:ind2], 0)
662
- if not ((ind1 in indices) and (ind2 in indices)):
663
- warn(
664
- 'Beamforming result may not have been calculated '
665
- 'for all queried frequencies. Check '
666
- 'freq_data.ind_low and freq_data.ind_high!',
667
- Warning,
668
- stacklevel=2,
669
- )
670
710
  if isinstance(self, BeamformerAdaptiveGrid):
671
711
  return h
712
+ if isinstance(self, BeamformerSODIX):
713
+ return h.reshape((self.steer.grid.size, self.steer.mics.num_mics))
672
714
  return h.reshape(self.steer.grid.shape)
673
715
 
674
- def integrate(self, sector):
716
+ def integrate(self, sector, frange=None, num=0):
675
717
  """Integrates result map over a given sector.
676
718
 
677
719
  Parameters
678
720
  ----------
679
- sector: array of floats
680
- Tuple with arguments for the 'indices' method
681
- of a :class:`~acoular.grids.Grid`-derived class
682
- (e.g. :meth:`RectGrid.indices<acoular.grids.RectGrid.indices>`
683
- or :meth:`RectGrid3D.indices<acoular.grids.RectGrid3D.indices>`).
684
- Possible sectors would be *array([xmin, ymin, xmax, ymax])*
685
- or *array([x, y, radius])*.
721
+ sector: array of floats or :class:`~acoular.grids.Sector`
722
+ either an array, tuple or list with arguments for the 'indices'
723
+ method of a :class:`~acoular.grids.Grid`-derived class
724
+ (e.g. :meth:`RectGrid.indices<acoular.grids.RectGrid.indices>`
725
+ or :meth:`RectGrid3D.indices<acoular.grids.RectGrid3D.indices>`).
726
+ Possible sectors would be *array([xmin, ymin, xmax, ymax])*
727
+ or *array([x, y, radius])* or an instance of a
728
+ :class:`~acoular.grids.Sector`-derived class
729
+
730
+ frange: tuple or None
731
+ a tuple of (fmin,fmax) frequencies to include in the result if *num*==0,
732
+ or band center frequency/frequencies for which to return the results
733
+ if *num*>0; if None, then the frequency range is determined from
734
+ the settings of the :attr:`PowerSpectra.ind_low` and
735
+ :attr:`PowerSpectra.ind_high` of :attr:`freq_data`
736
+
737
+ num : integer
738
+ Controls the width of the frequency bands considered; defaults to
739
+ 0 (single frequency line). Only considered if *frange* is not None.
740
+
741
+ === =====================
742
+ num frequency band width
743
+ === =====================
744
+ 0 single frequency line
745
+ 1 octave band
746
+ 3 third-octave band
747
+ n 1/n-octave band
748
+ === =====================
749
+
686
750
 
687
751
  Returns
688
752
  -------
689
- array of floats
690
- The spectrum (all calculated frequency bands) for the integrated sector.
691
-
753
+ res or (f, res): array of floats or tuple(array of floats, array of floats)
754
+ If *frange*==None or *num*>0, the spectrum (all calculated frequency bands)
755
+ for the integrated sector is returned as *res*. The dimension of this array is the
756
+ number of frequencies given by :attr:`freq_data` and entries not computed are zero.
757
+ If *frange*!=None and *num*==0, then (f, res) is returned where *f* are the (band)
758
+ frequencies and the dimension of both arrays is determined from *frange*
692
759
  """
693
- # resp. array([rmin, phimin, rmax, phimax]), array([r, phi, radius]).
694
-
695
- # ind = self.grid.indices(*sector)
696
- # gshape = self.grid.shape
697
- # r = self.result
698
- # rshape = r.shape
699
- # mapshape = (rshape[0], ) + gshape
700
- # h = r[:].reshape(mapshape)[ (s_[:], ) + ind ]
701
- # return h.reshape(h.shape[0], prod(h.shape[1:])).sum(axis=1)
702
760
  if isinstance(sector, Sector):
703
761
  ind = self.steer.grid.subdomain(sector)
704
762
  elif hasattr(self.steer.grid, 'indices'):
@@ -713,154 +771,185 @@ class BeamformerBase(HasPrivateTraits):
713
771
  msg,
714
772
  )
715
773
  gshape = self.steer.grid.shape
716
- r = self.result
717
- h = zeros(r.shape[0])
718
- for i in range(r.shape[0]):
719
- h[i] = r[i].reshape(gshape)[ind].sum()
774
+ if num == 0 or frange is None:
775
+ if frange is None:
776
+ ind_low = self.freq_data.ind_low
777
+ ind_high = self.freq_data.ind_high
778
+ if ind_low is None:
779
+ ind_low = 0
780
+ if ind_low < 0:
781
+ ind_low += self._numfreq
782
+ if ind_high is None:
783
+ ind_high = self._numfreq
784
+ if ind_high < 0:
785
+ ind_high += self._numfreq
786
+ irange = (ind_low, ind_high)
787
+ num = 0
788
+ elif len(frange) == 2:
789
+ irange = (searchsorted(self._f, frange[0]), searchsorted(self._f, frange[1]))
790
+ else:
791
+ msg = 'Only a tuple of length 2 is allowed for frange if num==0'
792
+ raise TypeError(
793
+ msg,
794
+ )
795
+ h = zeros(self._numfreq, dtype=float)
796
+ sl = slice(*irange)
797
+ r = self.result[sl]
798
+ for i in range(*irange):
799
+ # we do this per frequency because r might not have fancy indexing
800
+ h[i] = r[i - sl.start].reshape(gshape)[ind].sum()
801
+ if frange is None:
802
+ return h
803
+ return self._f[sl], h[sl]
804
+
805
+ h = zeros(len(frange), dtype=float)
806
+ for i, f in enumerate(frange):
807
+ h[i] = self.synthetic(f, num).reshape(gshape)[ind].sum()
720
808
  return h
721
809
 
722
810
 
723
811
  class BeamformerFunctional(BeamformerBase):
724
- """Functional beamforming after :ref:`Dougherty, 2014<Dougherty2014>`."""
812
+ """Functional beamforming algorithm.
813
+
814
+ See :cite:`Dougherty2014` for details.
815
+ """
725
816
 
726
817
  #: Functional exponent, defaults to 1 (= Classic Beamforming).
727
818
  gamma = Float(1, desc='functional exponent')
728
819
 
729
- # internal identifier
730
- digest = Property(depends_on=['freq_data.digest', '_steer_obj.digest', 'r_diag', 'gamma'])
731
-
732
820
  #: Functional Beamforming is only well defined for full CSM
733
821
  r_diag = Enum(False, desc='False, as Functional Beamformer is only well defined for the full CSM')
734
822
 
823
+ #: Normalization factor in case of CSM diagonal removal. Defaults to 1.0 since Functional Beamforming is only well defined for full CSM.
824
+ r_diag_norm = Enum(
825
+ 1.0,
826
+ desc='No normalization needed. Functional Beamforming is only well defined for full CSM.',
827
+ )
828
+
829
+ # internal identifier
830
+ digest = Property(depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['gamma'])
831
+
735
832
  @cached_property
736
833
  def _get_digest(self):
737
834
  return digest(self)
738
835
 
739
- def calc(self, ac, fr):
740
- """Calculates the Functional Beamformer result for the frequencies defined by :attr:`freq_data`.
836
+ def _calc(self, ind):
837
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
741
838
 
742
839
  This is an internal helper function that is automatically called when
743
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
744
- its :meth:`~BeamformerBase.synthetic` method.
840
+ accessing the beamformer's :attr:`result` or calling
841
+ its :meth:`synthetic` method.
745
842
 
746
843
  Parameters
747
844
  ----------
748
- ac : array of floats
749
- This array of dimension ([number of frequencies]x[number of gridpoints])
750
- is used as call-by-reference parameter and contains the calculated
751
- value after calling this method.
752
- fr : array of booleans
753
- The entries of this [number of frequencies]-sized array are either
754
- 'True' (if the result for this frequency has already been calculated)
755
- or 'False' (for the frequencies where the result has yet to be calculated).
756
- After the calculation at a certain frequency the value will be set
757
- to 'True'
845
+ ind : array of int
846
+ This array contains all frequency indices for which (re)calculation is
847
+ to be performed
758
848
 
759
849
  Returns
760
850
  -------
761
- This method only returns values through the *ac* and *fr* parameters
851
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
762
852
 
763
853
  """
764
- f = self.freq_data.fftfreq()
765
- normFactor = self.sig_loss_norm()
854
+ f = self._f
855
+ normfactor = self.sig_loss_norm()
766
856
  param_steer_type, steer_vector = self._beamformer_params()
767
- for i in self.freq_data.indices:
768
- if not fr[i]:
769
- if self.r_diag:
770
- # This case is not used at the moment (see Trait r_diag)
771
- # It would need some testing as structural changes were not tested...
772
- # ==============================================================================
773
- # One cannot use spectral decomposition when diagonal of csm is removed,
774
- # as the resulting modified eigenvectors are not orthogonal to each other anymore.
775
- # Therefor potentiating cannot be applied only to the eigenvalues.
776
- # --> To avoid this the root of the csm (removed diag) is calculated directly.
777
- # WATCH OUT: This doesn't really produce good results.
778
- # ==============================================================================
779
- csm = self.freq_data.csm[i]
780
- fill_diagonal(csm, 0)
781
- csmRoot = fractional_matrix_power(csm, 1.0 / self.gamma)
782
- beamformerOutput, steerNorm = beamformerFreq(
783
- param_steer_type,
784
- self.r_diag,
785
- 1.0,
786
- steer_vector(f[i]),
787
- csmRoot,
788
- )
789
- beamformerOutput /= steerNorm # take normalized steering vec
857
+ for i in ind:
858
+ if self.r_diag:
859
+ # This case is not used at the moment (see Trait r_diag)
860
+ # It would need some testing as structural changes were not tested...
861
+ # ==============================================================================
862
+ # One cannot use spectral decomposition when diagonal of csm is removed,
863
+ # as the resulting modified eigenvectors are not orthogonal to each other anymore.
864
+ # Therefor potentiating cannot be applied only to the eigenvalues.
865
+ # --> To avoid this the root of the csm (removed diag) is calculated directly.
866
+ # WATCH OUT: This doesn't really produce good results.
867
+ # ==============================================================================
868
+ csm = self.freq_data.csm[i]
869
+ fill_diagonal(csm, 0)
870
+ csmRoot = fractional_matrix_power(csm, 1.0 / self.gamma)
871
+ beamformerOutput, steerNorm = beamformerFreq(
872
+ param_steer_type,
873
+ self.r_diag,
874
+ normfactor,
875
+ steer_vector(f[i]),
876
+ csmRoot,
877
+ )
878
+ beamformerOutput /= steerNorm # take normalized steering vec
790
879
 
791
- # set (unphysical) negative output values to 0
792
- indNegSign = sign(beamformerOutput) < 0
793
- beamformerOutput[indNegSign] = 0.0
794
- else:
795
- eva = array(self.freq_data.eva[i], dtype='float64') ** (1.0 / self.gamma)
796
- eve = array(self.freq_data.eve[i], dtype='complex128')
797
- beamformerOutput, steerNorm = beamformerFreq(
798
- param_steer_type,
799
- self.r_diag,
800
- 1.0,
801
- steer_vector(f[i]),
802
- (eva, eve),
803
- )
804
- beamformerOutput /= steerNorm # take normalized steering vec
805
- ac[i] = (
806
- (beamformerOutput**self.gamma) * steerNorm * normFactor
807
- ) # the normalization must be done outside the beamformer
808
- fr[i] = 1
880
+ # set (unphysical) negative output values to 0
881
+ indNegSign = sign(beamformerOutput) < 0
882
+ beamformerOutput[indNegSign] = 0.0
883
+ else:
884
+ eva = array(self.freq_data.eva[i], dtype='float64') ** (1.0 / self.gamma)
885
+ eve = array(self.freq_data.eve[i], dtype='complex128')
886
+ beamformerOutput, steerNorm = beamformerFreq(
887
+ param_steer_type,
888
+ self.r_diag,
889
+ 1.0,
890
+ steer_vector(f[i]),
891
+ (eva, eve),
892
+ )
893
+ beamformerOutput /= steerNorm # take normalized steering vec
894
+ self._ac[i] = (
895
+ (beamformerOutput**self.gamma) * steerNorm * normfactor
896
+ ) # the normalization must be done outside the beamformer
897
+ self._fr[i] = 1
809
898
 
810
899
 
811
900
  class BeamformerCapon(BeamformerBase):
812
- """Beamforming using the Capon (Mininimum Variance) algorithm,
813
- see :ref:`Capon, 1969<Capon1969>`.
901
+ """Beamforming using the Capon (Mininimum Variance) algorithm.
902
+
903
+ See :cite:`Capon1969` for details.
814
904
  """
815
905
 
816
906
  # Boolean flag, if 'True', the main diagonal is removed before beamforming;
817
907
  # for Capon beamforming r_diag is set to 'False'.
818
908
  r_diag = Enum(False, desc='removal of diagonal')
819
909
 
820
- def calc(self, ac, fr):
821
- """Calculates the Capon result for the frequencies defined by :attr:`freq_data`.
910
+ #: Normalization factor in case of CSM diagonal removal. Defaults to 1.0 since Beamformer Capon is only well defined for full CSM.
911
+ r_diag_norm = Enum(
912
+ 1.0,
913
+ desc='No normalization. BeamformerCapon is only well defined for full CSM.',
914
+ )
915
+
916
+ def _calc(self, ind):
917
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
822
918
 
823
919
  This is an internal helper function that is automatically called when
824
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
825
- its :meth:`~BeamformerBase.synthetic` method.
920
+ accessing the beamformer's :attr:`result` or calling
921
+ its :meth:`synthetic` method.
826
922
 
827
923
  Parameters
828
924
  ----------
829
- ac : array of floats
830
- This array of dimension ([number of frequencies]x[number of gridpoints])
831
- is used as call-by-reference parameter and contains the calculated
832
- value after calling this method.
833
- fr : array of booleans
834
- The entries of this [number of frequencies]-sized array are either
835
- 'True' (if the result for this frequency has already been calculated)
836
- or 'False' (for the frequencies where the result has yet to be calculated).
837
- After the calculation at a certain frequency the value will be set
838
- to 'True'
925
+ ind : array of int
926
+ This array contains all frequency indices for which (re)calculation is
927
+ to be performed
839
928
 
840
929
  Returns
841
930
  -------
842
- This method only returns values through the *ac* and *fr* parameters
931
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
843
932
 
844
933
  """
845
- f = self.freq_data.fftfreq()
934
+ f = self._f
846
935
  nMics = self.freq_data.numchannels
847
- normFactor = self.sig_loss_norm() * nMics**2
936
+ normfactor = self.sig_loss_norm() * nMics**2
848
937
  param_steer_type, steer_vector = self._beamformer_params()
849
- for i in self.freq_data.indices:
850
- if not fr[i]:
851
- csm = array(linalg.inv(array(self.freq_data.csm[i], dtype='complex128')), order='C')
852
- beamformerOutput = beamformerFreq(param_steer_type, self.r_diag, normFactor, steer_vector(f[i]), csm)[0]
853
- ac[i] = 1.0 / beamformerOutput
854
- fr[i] = 1
938
+ for i in ind:
939
+ csm = array(inv(array(self.freq_data.csm[i], dtype='complex128')), order='C')
940
+ beamformerOutput = beamformerFreq(param_steer_type, self.r_diag, normfactor, steer_vector(f[i]), csm)[0]
941
+ self._ac[i] = 1.0 / beamformerOutput
942
+ self._fr[i] = 1
855
943
 
856
944
 
857
945
  class BeamformerEig(BeamformerBase):
858
- """Beamforming using eigenvalue and eigenvector techniques,
859
- see :ref:`Sarradj et al., 2005<Sarradj2005>`.
946
+ """Beamforming using eigenvalue and eigenvector techniques.
947
+
948
+ See :cite:`Sarradj2005` for details.
860
949
  """
861
950
 
862
951
  #: Number of component to calculate:
863
- #: 0 (smallest) ... :attr:`~acoular.tprocess.SamplesGenerator.numchannels`-1;
952
+ #: 0 (smallest) ... :attr:`~acoular.base.SamplesGenerator.numchannels`-1;
864
953
  #: defaults to -1, i.e. numchannels-1
865
954
  n = Int(-1, desc='No. of eigenvalue')
866
955
 
@@ -868,7 +957,7 @@ class BeamformerEig(BeamformerBase):
868
957
  na = Property(desc='No. of eigenvalue')
869
958
 
870
959
  # internal identifier
871
- digest = Property(depends_on=['freq_data.digest', '_steer_obj.digest', 'r_diag', 'n'])
960
+ digest = Property(depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['n'])
872
961
 
873
962
  @cached_property
874
963
  def _get_digest(self):
@@ -882,107 +971,100 @@ class BeamformerEig(BeamformerBase):
882
971
  na = max(nm + na, 0)
883
972
  return min(nm - 1, na)
884
973
 
885
- def calc(self, ac, fr):
974
+ def _calc(self, ind):
886
975
  """Calculates the result for the frequencies defined by :attr:`freq_data`.
887
976
 
888
977
  This is an internal helper function that is automatically called when
889
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
890
- its :meth:`~BeamformerBase.synthetic` method.
978
+ accessing the beamformer's :attr:`result` or calling
979
+ its :meth:`synthetic` method.
891
980
 
892
981
  Parameters
893
982
  ----------
894
- ac : array of floats
895
- This array of dimension ([number of frequencies]x[number of gridpoints])
896
- is used as call-by-reference parameter and contains the calculated
897
- value after calling this method.
898
- fr : array of booleans
899
- The entries of this [number of frequencies]-sized array are either
900
- 'True' (if the result for this frequency has already been calculated)
901
- or 'False' (for the frequencies where the result has yet to be calculated).
902
- After the calculation at a certain frequency the value will be set
903
- to 'True'
983
+ ind : array of int
984
+ This array contains all frequency indices for which (re)calculation is
985
+ to be performed
904
986
 
905
987
  Returns
906
988
  -------
907
- This method only returns values through the *ac* and *fr* parameters
989
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
908
990
 
909
991
  """
910
- f = self.freq_data.fftfreq()
992
+ f = self._f
911
993
  na = int(self.na) # eigenvalue taken into account
912
- normFactor = self.sig_loss_norm()
994
+ normfactor = self.sig_loss_norm()
913
995
  param_steer_type, steer_vector = self._beamformer_params()
914
- for i in self.freq_data.indices:
915
- if not fr[i]:
916
- eva = array(self.freq_data.eva[i], dtype='float64')
917
- eve = array(self.freq_data.eve[i], dtype='complex128')
918
- beamformerOutput = beamformerFreq(
919
- param_steer_type,
920
- self.r_diag,
921
- normFactor,
922
- steer_vector(f[i]),
923
- (eva[na : na + 1], eve[:, na : na + 1]),
924
- )[0]
925
- if self.r_diag: # set (unphysical) negative output values to 0
926
- indNegSign = sign(beamformerOutput) < 0
927
- beamformerOutput[indNegSign] = 0
928
- ac[i] = beamformerOutput
929
- fr[i] = 1
996
+ for i in ind:
997
+ eva = array(self.freq_data.eva[i], dtype='float64')
998
+ eve = array(self.freq_data.eve[i], dtype='complex128')
999
+ beamformerOutput = beamformerFreq(
1000
+ param_steer_type,
1001
+ self.r_diag,
1002
+ normfactor,
1003
+ steer_vector(f[i]),
1004
+ (eva[na : na + 1], eve[:, na : na + 1]),
1005
+ )[0]
1006
+ if self.r_diag: # set (unphysical) negative output values to 0
1007
+ indNegSign = sign(beamformerOutput) < 0
1008
+ beamformerOutput[indNegSign] = 0
1009
+ self._ac[i] = beamformerOutput
1010
+ self._fr[i] = 1
930
1011
 
931
1012
 
932
1013
  class BeamformerMusic(BeamformerEig):
933
- """Beamforming using the MUSIC algorithm, see :ref:`Schmidt, 1986<Schmidt1986>`."""
1014
+ """Beamforming using the MUSIC algorithm.
1015
+
1016
+ See :cite:`Schmidt1986` for details.
1017
+ """
934
1018
 
935
1019
  # Boolean flag, if 'True', the main diagonal is removed before beamforming;
936
1020
  # for MUSIC beamforming r_diag is set to 'False'.
937
1021
  r_diag = Enum(False, desc='removal of diagonal')
938
1022
 
1023
+ #: Normalization factor in case of CSM diagonal removal. Defaults to 1.0 since BeamformerMusic is only well defined for full CSM.
1024
+ r_diag_norm = Enum(
1025
+ 1.0,
1026
+ desc='No normalization. BeamformerMusic is only well defined for full CSM.',
1027
+ )
1028
+
939
1029
  # assumed number of sources, should be set to a value not too small
940
1030
  # defaults to 1
941
1031
  n = Int(1, desc='assumed number of sources')
942
1032
 
943
- def calc(self, ac, fr):
944
- """Calculates the MUSIC result for the frequencies defined by :attr:`freq_data`.
1033
+ def _calc(self, ind):
1034
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
945
1035
 
946
1036
  This is an internal helper function that is automatically called when
947
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
948
- its :meth:`~BeamformerBase.synthetic` method.
1037
+ accessing the beamformer's :attr:`result` or calling
1038
+ its :meth:`synthetic` method.
949
1039
 
950
1040
  Parameters
951
1041
  ----------
952
- ac : array of floats
953
- This array of dimension ([number of frequencies]x[number of gridpoints])
954
- is used as call-by-reference parameter and contains the calculated
955
- value after calling this method.
956
- fr : array of booleans
957
- The entries of this [number of frequencies]-sized array are either
958
- 'True' (if the result for this frequency has already been calculated)
959
- or 'False' (for the frequencies where the result has yet to be calculated).
960
- After the calculation at a certain frequency the value will be set
961
- to 'True'
1042
+ ind : array of int
1043
+ This array contains all frequency indices for which (re)calculation is
1044
+ to be performed
962
1045
 
963
1046
  Returns
964
1047
  -------
965
- This method only returns values through the *ac* and *fr* parameters
1048
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
966
1049
 
967
1050
  """
968
- f = self.freq_data.fftfreq()
1051
+ f = self._f
969
1052
  nMics = self.freq_data.numchannels
970
1053
  n = int(self.steer.mics.num_mics - self.na)
971
- normFactor = self.sig_loss_norm() * nMics**2
1054
+ normfactor = self.sig_loss_norm() * nMics**2
972
1055
  param_steer_type, steer_vector = self._beamformer_params()
973
- for i in self.freq_data.indices:
974
- if not fr[i]:
975
- eva = array(self.freq_data.eva[i], dtype='float64')
976
- eve = array(self.freq_data.eve[i], dtype='complex128')
977
- beamformerOutput = beamformerFreq(
978
- param_steer_type,
979
- self.r_diag,
980
- normFactor,
981
- steer_vector(f[i]),
982
- (eva[:n], eve[:, :n]),
983
- )[0]
984
- ac[i] = 4e-10 * beamformerOutput.min() / beamformerOutput
985
- fr[i] = 1
1056
+ for i in ind:
1057
+ eva = array(self.freq_data.eva[i], dtype='float64')
1058
+ eve = array(self.freq_data.eve[i], dtype='complex128')
1059
+ beamformerOutput = beamformerFreq(
1060
+ param_steer_type,
1061
+ self.r_diag,
1062
+ normfactor,
1063
+ steer_vector(f[i]),
1064
+ (eva[:n], eve[:, :n]),
1065
+ )[0]
1066
+ self._ac[i] = 4e-10 * beamformerOutput.min() / beamformerOutput
1067
+ self._fr[i] = 1
986
1068
 
987
1069
 
988
1070
  class PointSpreadFunction(HasPrivateTraits):
@@ -1010,9 +1092,13 @@ class PointSpreadFunction(HasPrivateTraits):
1010
1092
  if isinstance(steer, SteeringVector):
1011
1093
  self._steer_obj = steer
1012
1094
  elif steer in ('true level', 'true location', 'classic', 'inverse'):
1013
- # Type of steering vectors, see also :ref:`Sarradj, 2012<Sarradj2012>`.
1095
+ msg = (
1096
+ "Deprecated use of 'steer' trait. Please use object of class 'SteeringVector'."
1097
+ "The functionality of using string values for 'steer' will be removed in version 25.01."
1098
+ )
1099
+ # Type of steering vectors, see also :cite:`Sarradj2012`.
1014
1100
  warn(
1015
- "Deprecated use of 'steer' trait. Please use object of class 'SteeringVector' in the future.",
1101
+ msg,
1016
1102
  Warning,
1017
1103
  stacklevel=2,
1018
1104
  )
@@ -1031,7 +1117,11 @@ class PointSpreadFunction(HasPrivateTraits):
1031
1117
  return self._steer_obj.env
1032
1118
 
1033
1119
  def _set_env(self, env):
1034
- warn("Deprecated use of 'env' trait. ", Warning, stacklevel=2)
1120
+ msg = (
1121
+ "Deprecated use of 'env' trait. Please use the 'steer' trait with an object of class"
1122
+ "'SteeringVector'. The 'env' trait will be removed in version 25.01."
1123
+ )
1124
+ warn(msg, DeprecationWarning, stacklevel=2)
1035
1125
  self._steer_obj.env = env
1036
1126
 
1037
1127
  # The speed of sound.
@@ -1043,7 +1133,11 @@ class PointSpreadFunction(HasPrivateTraits):
1043
1133
  return self._steer_obj.env.c
1044
1134
 
1045
1135
  def _set_c(self, c):
1046
- warn("Deprecated use of 'c' trait. ", Warning, stacklevel=2)
1136
+ msg = (
1137
+ "Deprecated use of 'c' trait. Please use the 'steer' trait with an object of class"
1138
+ "'SteeringVector'. The 'c' trait will be removed in version 25.01."
1139
+ )
1140
+ warn(msg, DeprecationWarning, stacklevel=2)
1047
1141
  self._steer_obj.env.c = c
1048
1142
 
1049
1143
  # :class:`~acoular.grids.Grid`-derived object that provides the grid locations.
@@ -1055,7 +1149,11 @@ class PointSpreadFunction(HasPrivateTraits):
1055
1149
  return self._steer_obj.grid
1056
1150
 
1057
1151
  def _set_grid(self, grid):
1058
- warn("Deprecated use of 'grid' trait. ", Warning, stacklevel=2)
1152
+ msg = (
1153
+ "Deprecated use of 'grid' trait. Please use the 'steer' trait with an object of class"
1154
+ "'SteeringVector'. The 'grid' trait will be removed in version 25.01."
1155
+ )
1156
+ warn(msg, DeprecationWarning, stacklevel=2)
1059
1157
  self._steer_obj.grid = grid
1060
1158
 
1061
1159
  # :class:`~acoular.microphones.MicGeom` object that provides the microphone locations.
@@ -1067,7 +1165,11 @@ class PointSpreadFunction(HasPrivateTraits):
1067
1165
  return self._steer_obj.mics
1068
1166
 
1069
1167
  def _set_mpos(self, mpos):
1070
- warn("Deprecated use of 'mpos' trait. ", Warning, stacklevel=2)
1168
+ msg = (
1169
+ "Deprecated use of 'mpos' trait. Please use the 'steer' trait with an object of class"
1170
+ "'SteeringVector'. The 'mpos' trait will be removed in version 25.01."
1171
+ )
1172
+ warn(msg, DeprecationWarning, stacklevel=2)
1071
1173
  self._steer_obj.mics = mpos
1072
1174
 
1073
1175
  # Sound travel distances from microphone array center to grid points (r0)
@@ -1077,11 +1179,21 @@ class PointSpreadFunction(HasPrivateTraits):
1077
1179
  r0 = Property()
1078
1180
 
1079
1181
  def _get_r0(self):
1182
+ msg = (
1183
+ "Deprecated use of 'r0' trait. Please use the 'steer' trait with an object of class"
1184
+ "'SteeringVector'. The 'r0' trait will be removed in version 25.01."
1185
+ )
1186
+ warn(msg, DeprecationWarning, stacklevel=2)
1080
1187
  return self._steer_obj.r0
1081
1188
 
1082
1189
  rm = Property()
1083
1190
 
1084
1191
  def _get_rm(self):
1192
+ msg = (
1193
+ "Deprecated use of 'rm' trait. Please use the 'steer' trait with an object of class"
1194
+ "'SteeringVector'. The 'rm' trait will be removed in version 25.01."
1195
+ )
1196
+ warn(msg, DeprecationWarning, stacklevel=2)
1085
1197
  return self._steer_obj.rm
1086
1198
 
1087
1199
  # --- End of backwards compatibility traits --------------------------------------
@@ -1206,19 +1318,19 @@ class PointSpreadFunction(HasPrivateTraits):
1206
1318
 
1207
1319
  if self.calcmode == 'single': # calculate selected psfs one-by-one
1208
1320
  for ind in g_ind_calc:
1209
- ac[:, ind] = self._psfCall([ind])[:, 0]
1321
+ ac[:, ind] = self._psf_call([ind])[:, 0]
1210
1322
  gp[ind] = 1
1211
1323
  elif self.calcmode == 'full': # calculate all psfs in one go
1212
1324
  gp[:] = 1
1213
- ac[:] = self._psfCall(arange(self.steer.grid.size))
1325
+ ac[:] = self._psf_call(arange(self.steer.grid.size))
1214
1326
  else: # 'block' # calculate selected psfs in one go
1215
- hh = self._psfCall(g_ind_calc)
1327
+ hh = self._psf_call(g_ind_calc)
1216
1328
  for indh, ind in enumerate(g_ind_calc):
1217
1329
  gp[ind] = 1
1218
1330
  ac[:, ind] = hh[:, indh]
1219
1331
  indh += 1
1220
1332
 
1221
- def _psfCall(self, ind):
1333
+ def _psf_call(self, ind):
1222
1334
  """Manages the calling of the core psf functionality.
1223
1335
 
1224
1336
  Parameters
@@ -1249,27 +1361,18 @@ class PointSpreadFunction(HasPrivateTraits):
1249
1361
 
1250
1362
 
1251
1363
  class BeamformerDamas(BeamformerBase):
1252
- """DAMAS deconvolution, see :ref:`Brooks and Humphreys, 2006<BrooksHumphreys2006>`.
1253
- Needs a-priori delay-and-sum beamforming (:class:`BeamformerBase`).
1254
- """
1255
-
1256
- #: :class:`BeamformerBase` object that provides data for deconvolution.
1257
- beamformer = Trait(BeamformerBase)
1258
-
1259
- #: :class:`~acoular.spectra.PowerSpectra` object that provides the cross spectral matrix;
1260
- #: is set automatically.
1261
- freq_data = Delegate('beamformer')
1364
+ """DAMAS deconvolution algorithm.
1262
1365
 
1263
- #: Boolean flag, if 'True' (default), the main diagonal is removed before beamforming;
1264
- #: is set automatically.
1265
- r_diag = Delegate('beamformer')
1366
+ See :cite:`Brooks2006` for details.
1367
+ """
1266
1368
 
1267
- #: instance of :class:`~acoular.fbeamform.SteeringVector` or its derived classes,
1268
- #: that contains information about the steering vector. Is set automatically.
1269
- steer = Delegate('beamformer')
1369
+ #: (only for backward compatibility) :class:`BeamformerBase` object
1370
+ #: if set, provides :attr:`freq_data`, :attr:`steer`, :attr:`r_diag`
1371
+ #: if not set, these have to be set explicitly.
1372
+ beamformer = Property()
1270
1373
 
1271
- #: Floating point precision of result, is set automatically.
1272
- precision = Delegate('beamformer')
1374
+ # private storage of beamformer instance
1375
+ _beamformer = Trait(BeamformerBase)
1273
1376
 
1274
1377
  #: The floating-number-precision of the PSFs. Default is 64 bit.
1275
1378
  psf_precision = Trait('float64', 'float32', desc='precision of PSF')
@@ -1286,64 +1389,79 @@ class BeamformerDamas(BeamformerBase):
1286
1389
 
1287
1390
  # internal identifier
1288
1391
  digest = Property(
1289
- depends_on=['beamformer.digest', 'n_iter', 'damp', 'psf_precision'],
1392
+ depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['n_iter', 'damp', 'psf_precision'],
1290
1393
  )
1291
1394
 
1292
- # internal identifier
1293
- ext_digest = Property(
1294
- depends_on=['digest', 'beamformer.ext_digest'],
1295
- )
1395
+ def _get_beamformer(self):
1396
+ return self._beamformer
1397
+
1398
+ def _set_beamformer(self, beamformer):
1399
+ msg = (
1400
+ f"Deprecated use of 'beamformer' trait in class {self.__class__.__name__}. "
1401
+ 'Please set :attr:`freq_data`, :attr:`steer`, :attr:`r_diag` directly. '
1402
+ "Using the 'beamformer' trait will be removed in version 25.07."
1403
+ )
1404
+ warn(
1405
+ msg,
1406
+ DeprecationWarning,
1407
+ stacklevel=2,
1408
+ )
1409
+ self._beamformer = beamformer
1296
1410
 
1297
1411
  @cached_property
1298
1412
  def _get_digest(self):
1299
1413
  return digest(self)
1300
1414
 
1301
- @cached_property
1302
- def _get_ext_digest(self):
1303
- return digest(self, 'ext_digest')
1415
+ @on_trait_change('_beamformer.digest')
1416
+ def delegate_beamformer_traits(self):
1417
+ self.freq_data = self.beamformer.freq_data
1418
+ self.r_diag = self.beamformer.r_diag
1419
+ self.steer = self.beamformer.steer
1304
1420
 
1305
- def calc(self, ac, fr):
1306
- """Calculates the DAMAS result for the frequencies defined by :attr:`freq_data`.
1421
+ def _calc(self, ind):
1422
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1307
1423
 
1308
1424
  This is an internal helper function that is automatically called when
1309
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
1310
- its :meth:`~BeamformerBase.synthetic` method.
1311
- A Gauss-Seidel algorithm implemented in C is used for computing the result.
1425
+ accessing the beamformer's :attr:`result` or calling
1426
+ its :meth:`synthetic` method.
1312
1427
 
1313
1428
  Parameters
1314
1429
  ----------
1315
- ac : array of floats
1316
- This array of dimension ([number of frequencies]x[number of gridpoints])
1317
- is used as call-by-reference parameter and contains the calculated
1318
- value after calling this method.
1319
- fr : array of booleans
1320
- The entries of this [number of frequencies]-sized array are either
1321
- 'True' (if the result for this frequency has already been calculated)
1322
- or 'False' (for the frequencies where the result has yet to be calculated).
1323
- After the calculation at a certain frequency the value will be set
1324
- to 'True'
1430
+ ind : array of int
1431
+ This array contains all frequency indices for which (re)calculation is
1432
+ to be performed
1325
1433
 
1326
1434
  Returns
1327
1435
  -------
1328
- This method only returns values through the *ac* and *fr* parameters
1436
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1329
1437
 
1330
1438
  """
1331
- f = self.freq_data.fftfreq()
1439
+ f = self._f
1440
+ normfactor = self.sig_loss_norm()
1332
1441
  p = PointSpreadFunction(steer=self.steer, calcmode=self.calcmode, precision=self.psf_precision)
1333
- for i in self.freq_data.indices:
1334
- if not fr[i]:
1335
- y = array(self.beamformer.result[i])
1336
- x = y.copy()
1337
- p.freq = f[i]
1338
- psf = p.psf[:]
1339
- damasSolverGaussSeidel(psf, y, self.n_iter, self.damp, x)
1340
- ac[i] = x
1341
- fr[i] = 1
1442
+ param_steer_type, steer_vector = self._beamformer_params()
1443
+ for i in ind:
1444
+ csm = array(self.freq_data.csm[i], dtype='complex128')
1445
+ y = beamformerFreq(
1446
+ param_steer_type,
1447
+ self.r_diag,
1448
+ normfactor,
1449
+ steer_vector(f[i]),
1450
+ csm,
1451
+ )[0]
1452
+ if self.r_diag: # set (unphysical) negative output values to 0
1453
+ indNegSign = sign(y) < 0
1454
+ y[indNegSign] = 0.0
1455
+ x = y.copy()
1456
+ p.freq = f[i]
1457
+ psf = p.psf[:]
1458
+ damasSolverGaussSeidel(psf, y, self.n_iter, self.damp, x)
1459
+ self._ac[i] = x
1460
+ self._fr[i] = 1
1342
1461
 
1343
1462
 
1344
1463
  class BeamformerDamasPlus(BeamformerDamas):
1345
- """DAMAS deconvolution, see :ref:`Brooks and Humphreys, 2006<BrooksHumphreys2006>`,
1346
- for solving the system of equations, instead of the original Gauss-Seidel
1464
+ """DAMAS deconvolution :cite:`Brooks2006` for solving the system of equations, instead of the original Gauss-Seidel
1347
1465
  iterations, this class employs the NNLS or linear programming solvers from
1348
1466
  scipy.optimize or one of several optimization algorithms from the scikit-learn module.
1349
1467
  Needs a-priori delay-and-sum beamforming (:class:`BeamformerBase`).
@@ -1374,109 +1492,109 @@ class BeamformerDamasPlus(BeamformerDamas):
1374
1492
 
1375
1493
  # internal identifier
1376
1494
  digest = Property(
1377
- depends_on=['beamformer.digest', 'alpha', 'method', 'max_iter', 'unit_mult'],
1378
- )
1379
-
1380
- # internal identifier
1381
- ext_digest = Property(
1382
- depends_on=['digest', 'beamformer.ext_digest'],
1495
+ depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['alpha', 'method', 'max_iter', 'unit_mult'],
1383
1496
  )
1384
1497
 
1385
1498
  @cached_property
1386
1499
  def _get_digest(self):
1387
1500
  return digest(self)
1388
1501
 
1389
- @cached_property
1390
- def _get_ext_digest(self):
1391
- return digest(self, 'ext_digest')
1392
-
1393
- def calc(self, ac, fr):
1394
- """Calculates the DAMAS result for the frequencies defined by :attr:`freq_data`.
1502
+ def _calc(self, ind):
1503
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1395
1504
 
1396
1505
  This is an internal helper function that is automatically called when
1397
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
1398
- its :meth:`~BeamformerBase.synthetic` method.
1506
+ accessing the beamformer's :attr:`result` or calling
1507
+ its :meth:`synthetic` method.
1399
1508
 
1400
1509
  Parameters
1401
1510
  ----------
1402
- ac : array of floats
1403
- This array of dimension ([number of frequencies]x[number of gridpoints])
1404
- is used as call-by-reference parameter and contains the calculated
1405
- value after calling this method.
1406
- fr : array of booleans
1407
- The entries of this [number of frequencies]-sized array are either
1408
- 'True' (if the result for this frequency has already been calculated)
1409
- or 'False' (for the frequencies where the result has yet to be calculated).
1410
- After the calculation at a certain frequency the value will be set
1411
- to 'True'
1511
+ ind : array of int
1512
+ This array contains all frequency indices for which (re)calculation is
1513
+ to be performed
1412
1514
 
1413
1515
  Returns
1414
1516
  -------
1415
- This method only returns values through the *ac* and *fr* parameters
1517
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1416
1518
 
1417
1519
  """
1418
- f = self.freq_data.fftfreq()
1520
+ f = self._f
1419
1521
  p = PointSpreadFunction(steer=self.steer, calcmode=self.calcmode, precision=self.psf_precision)
1420
1522
  unit = self.unit_mult
1421
- for i in self.freq_data.indices:
1422
- if not fr[i]:
1423
- y = self.beamformer.result[i] * unit
1424
- p.freq = f[i]
1425
- psf = p.psf[:]
1426
-
1427
- if self.method == 'NNLS':
1428
- ac[i] = nnls(psf, y)[0] / unit
1429
- elif self.method == 'LP': # linear programming (Dougherty)
1430
- if self.r_diag:
1431
- warn(
1432
- 'Linear programming solver may fail when CSM main '
1433
- 'diagonal is removed for delay-and-sum beamforming.',
1434
- Warning,
1435
- stacklevel=5,
1436
- )
1437
- cT = -1 * psf.sum(1) # turn the minimization into a maximization
1438
- ac[i] = linprog(c=cT, A_ub=psf, b_ub=y).x / unit # defaults to simplex method and non-negative x
1523
+ normfactor = self.sig_loss_norm()
1524
+ param_steer_type, steer_vector = self._beamformer_params()
1525
+ for i in ind:
1526
+ csm = array(self.freq_data.csm[i], dtype='complex128')
1527
+ y = beamformerFreq(
1528
+ param_steer_type,
1529
+ self.r_diag,
1530
+ normfactor,
1531
+ steer_vector(f[i]),
1532
+ csm,
1533
+ )[0]
1534
+ if self.r_diag: # set (unphysical) negative output values to 0
1535
+ indNegSign = sign(y) < 0
1536
+ y[indNegSign] = 0.0
1537
+ y *= unit
1538
+ p.freq = f[i]
1539
+ psf = p.psf[:]
1540
+
1541
+ if self.method == 'NNLS':
1542
+ self._ac[i] = nnls(psf, y)[0] / unit
1543
+ elif self.method == 'LP': # linear programming (Dougherty)
1544
+ if self.r_diag:
1545
+ warn(
1546
+ 'Linear programming solver may fail when CSM main '
1547
+ 'diagonal is removed for delay-and-sum beamforming.',
1548
+ Warning,
1549
+ stacklevel=5,
1550
+ )
1551
+ cT = -1 * psf.sum(1) # turn the minimization into a maximization
1552
+ self._ac[i] = linprog(c=cT, A_ub=psf, b_ub=y).x / unit # defaults to simplex method and non-negative x
1553
+ else:
1554
+ if self.method == 'LassoLars':
1555
+ model = LassoLars(
1556
+ alpha=self.alpha * unit,
1557
+ max_iter=self.max_iter,
1558
+ )
1559
+ elif self.method == 'OMPCV':
1560
+ model = OrthogonalMatchingPursuitCV()
1439
1561
  else:
1440
- if self.method == 'LassoLars':
1441
- model = LassoLars(
1442
- alpha=self.alpha * unit,
1443
- max_iter=self.max_iter,
1444
- )
1445
- elif self.method == 'OMPCV':
1446
- model = OrthogonalMatchingPursuitCV()
1447
- else:
1448
- msg = f'Method {self.method} not implemented.'
1449
- raise NotImplementedError(msg)
1450
- model.normalize = False
1451
- # from sklearn 1.2, normalize=True does not work the same way anymore and the pipeline approach
1452
- # with StandardScaler does scale in a different way, thus we monkeypatch the code and normalize
1453
- # ourselves to make results the same over different sklearn versions
1454
- norms = norm(psf, axis=0)
1455
- # get rid of annoying sklearn warnings that appear
1456
- # for sklearn<1.2 despite any settings
1457
- with warnings.catch_warnings():
1458
- warnings.simplefilter('ignore', category=FutureWarning)
1459
- # normalized psf
1460
- model.fit(psf / norms, y)
1461
- # recover normalization in the coef's
1462
- ac[i] = model.coef_[:] / norms / unit
1463
-
1464
- fr[i] = 1
1562
+ msg = f'Method {self.method} not implemented.'
1563
+ raise NotImplementedError(msg)
1564
+ model.normalize = False
1565
+ # from sklearn 1.2, normalize=True does not work the same way anymore and the pipeline approach
1566
+ # with StandardScaler does scale in a different way, thus we monkeypatch the code and normalize
1567
+ # ourselves to make results the same over different sklearn versions
1568
+ norms = norm(psf, axis=0)
1569
+ # get rid of annoying sklearn warnings that appear
1570
+ # for sklearn<1.2 despite any settings
1571
+ with warnings.catch_warnings():
1572
+ warnings.simplefilter('ignore', category=FutureWarning)
1573
+ # normalized psf
1574
+ model.fit(psf / norms, y)
1575
+ # recover normalization in the coef's
1576
+ self._ac[i] = model.coef_[:] / norms / unit
1577
+ self._fr[i] = 1
1465
1578
 
1466
1579
 
1467
1580
  class BeamformerOrth(BeamformerBase):
1468
- """Orthogonal deconvolution, see :ref:`Sarradj, 2010<Sarradj2010>`.
1581
+ """Orthogonal deconvolution algorithm.
1582
+
1583
+ See :cite:`Sarradj2010` for details.
1469
1584
  New faster implementation without explicit (:class:`BeamformerEig`).
1470
1585
  """
1471
1586
 
1472
1587
  #: (only for backward compatibility) :class:`BeamformerEig` object
1473
1588
  #: if set, provides :attr:`freq_data`, :attr:`steer`, :attr:`r_diag`
1474
- #: if not set, these have to be set explicitly
1475
- beamformer = Trait(BeamformerEig)
1589
+ #: if not set, these have to be set explicitly.
1590
+ beamformer = Property()
1591
+
1592
+ # private storage of beamformer instance
1593
+ _beamformer = Trait(BeamformerEig)
1476
1594
 
1477
1595
  #: List of components to consider, use this to directly set the eigenvalues
1478
1596
  #: used in the beamformer. Alternatively, set :attr:`n`.
1479
- eva_list = CArray(dtype=int, desc='components')
1597
+ eva_list = CArray(dtype=int, value=array([-1]), desc='components')
1480
1598
 
1481
1599
  #: Number of components to consider, defaults to 1. If set,
1482
1600
  #: :attr:`eva_list` will contain
@@ -1486,18 +1604,30 @@ class BeamformerOrth(BeamformerBase):
1486
1604
 
1487
1605
  # internal identifier
1488
1606
  digest = Property(
1489
- depends_on=['freq_data.digest', '_steer_obj.digest', 'r_diag', 'eva_list'],
1607
+ depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['eva_list'],
1490
1608
  )
1491
1609
 
1610
+ def _get_beamformer(self):
1611
+ return self._beamformer
1612
+
1613
+ def _set_beamformer(self, beamformer):
1614
+ msg = (
1615
+ f"Deprecated use of 'beamformer' trait in class {self.__class__.__name__}. "
1616
+ 'Please set :attr:`freq_data`, :attr:`steer`, :attr:`r_diag` directly. '
1617
+ "Using the 'beamformer' trait will be removed in version 25.07."
1618
+ )
1619
+ warn(
1620
+ msg,
1621
+ DeprecationWarning,
1622
+ stacklevel=2,
1623
+ )
1624
+ self._beamformer = beamformer
1625
+
1492
1626
  @cached_property
1493
1627
  def _get_digest(self):
1494
1628
  return digest(self)
1495
1629
 
1496
- @cached_property
1497
- def _get_ext_digest(self):
1498
- return digest(self, 'ext_digest')
1499
-
1500
- @on_trait_change('beamformer.digest')
1630
+ @on_trait_change('_beamformer.digest')
1501
1631
  def delegate_beamformer_traits(self):
1502
1632
  self.freq_data = self.beamformer.freq_data
1503
1633
  self.r_diag = self.beamformer.r_diag
@@ -1508,55 +1638,47 @@ class BeamformerOrth(BeamformerBase):
1508
1638
  """Sets the list of eigenvalues to consider."""
1509
1639
  self.eva_list = arange(-1, -1 - self.n, -1)
1510
1640
 
1511
- def calc(self, ac, fr):
1512
- """Calculates the Orthogonal Beamforming result for the frequencies
1513
- defined by :attr:`freq_data`.
1641
+ def _calc(self, ind):
1642
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1514
1643
 
1515
1644
  This is an internal helper function that is automatically called when
1516
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
1517
- its :meth:`~BeamformerBase.synthetic` method.
1645
+ accessing the beamformer's :attr:`result` or calling
1646
+ its :meth:`synthetic` method.
1518
1647
 
1519
1648
  Parameters
1520
1649
  ----------
1521
- ac : array of floats
1522
- This array of dimension ([number of frequencies]x[number of gridpoints])
1523
- is used as call-by-reference parameter and contains the calculated
1524
- value after calling this method.
1525
- fr : array of booleans
1526
- The entries of this [number of frequencies]-sized array are either
1527
- 'True' (if the result for this frequency has already been calculated)
1528
- or 'False' (for the frequencies where the result has yet to be calculated).
1529
- After the calculation at a certain frequency the value will be set
1530
- to 'True'
1650
+ ind : array of int
1651
+ This array contains all frequency indices for which (re)calculation is
1652
+ to be performed
1531
1653
 
1532
1654
  Returns
1533
1655
  -------
1534
- This method only returns values through the *ac* and *fr* parameters
1656
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1535
1657
 
1536
1658
  """
1537
- # prepare calculation
1538
- f = self.freq_data.fftfreq()
1659
+ f = self._f
1539
1660
  numchannels = self.freq_data.numchannels
1540
- normFactor = self.sig_loss_norm()
1661
+ normfactor = self.sig_loss_norm()
1541
1662
  param_steer_type, steer_vector = self._beamformer_params()
1542
- for i in self.freq_data.indices:
1543
- if not fr[i]:
1544
- eva = array(self.freq_data.eva[i], dtype='float64')
1545
- eve = array(self.freq_data.eve[i], dtype='complex128')
1546
- for n in self.eva_list:
1547
- beamformerOutput = beamformerFreq(
1548
- param_steer_type,
1549
- self.r_diag,
1550
- normFactor,
1551
- steer_vector(f[i]),
1552
- (ones(1), eve[:, n].reshape((-1, 1))),
1553
- )[0]
1554
- ac[i, beamformerOutput.argmax()] += eva[n] / numchannels
1555
- fr[i] = 1
1663
+ for i in ind:
1664
+ eva = array(self.freq_data.eva[i], dtype='float64')
1665
+ eve = array(self.freq_data.eve[i], dtype='complex128')
1666
+ for n in self.eva_list:
1667
+ beamformerOutput = beamformerFreq(
1668
+ param_steer_type,
1669
+ self.r_diag,
1670
+ normfactor,
1671
+ steer_vector(f[i]),
1672
+ (ones(1), eve[:, n].reshape((-1, 1))),
1673
+ )[0]
1674
+ self._ac[i, beamformerOutput.argmax()] += eva[n] / numchannels
1675
+ self._fr[i] = 1
1556
1676
 
1557
1677
 
1558
1678
  class BeamformerCleansc(BeamformerBase):
1559
- """CLEAN-SC deconvolution, see :ref:`Sijtsma, 2007<Sijtsma2007>`.
1679
+ """CLEAN-SC deconvolution algorithm.
1680
+
1681
+ See :cite:`Sijtsma2007` for details.
1560
1682
  Classic delay-and-sum beamforming is already included.
1561
1683
  """
1562
1684
 
@@ -1574,105 +1696,88 @@ class BeamformerCleansc(BeamformerBase):
1574
1696
  stopn = Int(3, desc='stop criterion index')
1575
1697
 
1576
1698
  # internal identifier
1577
- digest = Property(depends_on=['freq_data.digest', '_steer_obj.digest', 'r_diag', 'n', 'damp', 'stopn'])
1699
+ digest = Property(depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['n', 'damp', 'stopn'])
1578
1700
 
1579
1701
  @cached_property
1580
1702
  def _get_digest(self):
1581
1703
  return digest(self)
1582
1704
 
1583
- def calc(self, ac, fr):
1584
- """Calculates the CLEAN-SC result for the frequencies defined by :attr:`freq_data`.
1705
+ def _calc(self, ind):
1706
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1585
1707
 
1586
1708
  This is an internal helper function that is automatically called when
1587
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
1588
- its :meth:`~BeamformerBase.synthetic` method.
1709
+ accessing the beamformer's :attr:`result` or calling
1710
+ its :meth:`synthetic` method.
1589
1711
 
1590
1712
  Parameters
1591
1713
  ----------
1592
- ac : array of floats
1593
- This array of dimension ([number of frequencies]x[number of gridpoints])
1594
- is used as call-by-reference parameter and contains the calculated
1595
- value after calling this method.
1596
- fr : array of booleans
1597
- The entries of this [number of frequencies]-sized array are either
1598
- 'True' (if the result for this frequency has already been calculated)
1599
- or 'False' (for the frequencies where the result has yet to be calculated).
1600
- After the calculation at a certain frequency the value will be set
1601
- to 'True'
1714
+ ind : array of int
1715
+ This array contains all frequency indices for which (re)calculation is
1716
+ to be performed
1602
1717
 
1603
1718
  Returns
1604
1719
  -------
1605
- This method only returns values through the *ac* and *fr* parameters
1720
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1606
1721
 
1607
1722
  """
1608
- # prepare calculation
1609
- normFactor = self.sig_loss_norm()
1723
+ f = self._f
1724
+ normfactor = self.sig_loss_norm()
1610
1725
  numchannels = self.freq_data.numchannels
1611
- f = self.freq_data.fftfreq()
1612
1726
  result = zeros((self.steer.grid.size), 'f')
1613
- normFac = self.sig_loss_norm()
1614
1727
  J = numchannels * 2 if not self.n else self.n
1615
1728
  powers = zeros(J, 'd')
1616
1729
 
1617
1730
  param_steer_type, steer_vector = self._beamformer_params()
1618
- for i in self.freq_data.indices:
1619
- if not fr[i]:
1620
- csm = array(self.freq_data.csm[i], dtype='complex128', copy=1)
1621
- # h = self.steer._beamformerCall(f[i], self.r_diag, normFactor, (csm,))[0]
1622
- h = beamformerFreq(param_steer_type, self.r_diag, normFactor, steer_vector(f[i]), csm)[0]
1623
- # CLEANSC Iteration
1624
- result *= 0.0
1625
- for j in range(J):
1626
- xi_max = h.argmax() # index of maximum
1627
- powers[j] = hmax = h[xi_max] # maximum
1628
- result[xi_max] += self.damp * hmax
1629
- if j > self.stopn and hmax > powers[j - self.stopn]:
1630
- break
1631
- wmax = self.steer.steer_vector(f[i], xi_max) * sqrt(normFac)
1632
- wmax = wmax[0].conj() # as old code worked with conjugated csm..should be updated
1633
- hh = wmax.copy()
1634
- D1 = dot(csm.T - diag(diag(csm)), wmax) / hmax
1635
- ww = wmax.conj() * wmax
1636
- for _m in range(20):
1637
- H = hh.conj() * hh
1638
- hh = (D1 + H * wmax) / sqrt(1 + dot(ww, H))
1639
- hh = hh[:, newaxis]
1640
- csm1 = hmax * (hh * hh.conj().T)
1641
-
1642
- # h1 = self.steer._beamformerCall(f[i], self.r_diag, normFactor, (array((hmax, ))[newaxis, :], hh[newaxis, :].conjugate()))[0]
1643
- h1 = beamformerFreq(
1644
- param_steer_type,
1645
- self.r_diag,
1646
- normFactor,
1647
- steer_vector(f[i]),
1648
- (array((hmax,)), hh.conj()),
1649
- )[0]
1650
- h -= self.damp * h1
1651
- csm -= self.damp * csm1.T # transpose(0,2,1)
1652
- ac[i] = result
1653
- fr[i] = 1
1731
+ for i in ind:
1732
+ csm = array(self.freq_data.csm[i], dtype='complex128', copy=1)
1733
+ # h = self.steer._beamformerCall(f[i], self.r_diag, normfactor, (csm,))[0]
1734
+ h = beamformerFreq(param_steer_type, self.r_diag, normfactor, steer_vector(f[i]), csm)[0]
1735
+ # CLEANSC Iteration
1736
+ result *= 0.0
1737
+ for j in range(J):
1738
+ xi_max = h.argmax() # index of maximum
1739
+ powers[j] = hmax = h[xi_max] # maximum
1740
+ result[xi_max] += self.damp * hmax
1741
+ if j > self.stopn and hmax > powers[j - self.stopn]:
1742
+ break
1743
+ wmax = self.steer.steer_vector(f[i], xi_max) * sqrt(normfactor)
1744
+ wmax = wmax[0].conj() # as old code worked with conjugated csm..should be updated
1745
+ hh = wmax.copy()
1746
+ D1 = dot(csm.T - diag(diag(csm)), wmax) / hmax
1747
+ ww = wmax.conj() * wmax
1748
+ for _m in range(20):
1749
+ H = hh.conj() * hh
1750
+ hh = (D1 + H * wmax) / sqrt(1 + dot(ww, H))
1751
+ hh = hh[:, newaxis]
1752
+ csm1 = hmax * (hh * hh.conj().T)
1753
+
1754
+ # h1 = self.steer._beamformerCall(f[i], self.r_diag, normfactor, (array((hmax, ))[newaxis, :], hh[newaxis, :].conjugate()))[0]
1755
+ h1 = beamformerFreq(
1756
+ param_steer_type,
1757
+ self.r_diag,
1758
+ normfactor,
1759
+ steer_vector(f[i]),
1760
+ (array((hmax,)), hh.conj()),
1761
+ )[0]
1762
+ h -= self.damp * h1
1763
+ csm -= self.damp * csm1.T # transpose(0,2,1)
1764
+ self._ac[i] = result
1765
+ self._fr[i] = 1
1654
1766
 
1655
1767
 
1656
1768
  class BeamformerClean(BeamformerBase):
1657
- """CLEAN deconvolution, see :ref:`Hoegbom, 1974<Hoegbom1974>`.
1658
- Needs a-priori delay-and-sum beamforming (:class:`BeamformerBase`).
1659
- """
1660
-
1661
- # BeamformerBase object that provides data for deconvolution
1662
- beamformer = Trait(BeamformerBase)
1769
+ """CLEAN deconvolution algorithm.
1663
1770
 
1664
- # PowerSpectra object that provides the cross spectral matrix
1665
- freq_data = Delegate('beamformer')
1666
-
1667
- # flag, if true (default), the main diagonal is removed before beamforming
1668
- # r_diag = Delegate('beamformer')
1771
+ See :cite:`Hoegbom1974` for details.
1772
+ """
1669
1773
 
1670
- #: instance of :class:`~acoular.fbeamform.SteeringVector` or its derived classes,
1671
- #: that contains information about the steering vector. Is set automatically.
1672
- steer = Delegate('beamformer')
1774
+ #: (only for backward compatibility) :class:`BeamformerBase` object
1775
+ #: if set, provides :attr:`freq_data`, :attr:`steer`, :attr:`r_diag`
1776
+ #: if not set, these have to be set explicitly.
1777
+ beamformer = Property()
1673
1778
 
1674
- #: Floating point precision of result, is set automatically.
1675
- precision = Delegate('beamformer')
1779
+ # private storage of beamformer instance
1780
+ _beamformer = Trait(BeamformerBase)
1676
1781
 
1677
1782
  #: The floating-number-precision of the PSFs. Default is 64 bit.
1678
1783
  psf_precision = Trait('float64', 'float32', desc='precision of PSF.')
@@ -1689,49 +1794,56 @@ class BeamformerClean(BeamformerBase):
1689
1794
 
1690
1795
  # internal identifier
1691
1796
  digest = Property(
1692
- depends_on=['beamformer.digest', 'n_iter', 'damp', 'psf_precision'],
1693
- )
1694
-
1695
- # internal identifier
1696
- ext_digest = Property(
1697
- depends_on=['digest', 'beamformer.ext_digest'],
1797
+ depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['n_iter', 'damp', 'psf_precision'],
1698
1798
  )
1699
1799
 
1700
1800
  @cached_property
1701
1801
  def _get_digest(self):
1702
1802
  return digest(self)
1703
1803
 
1704
- @cached_property
1705
- def _get_ext_digest(self):
1706
- return digest(self, 'ext_digest')
1804
+ def _get_beamformer(self):
1805
+ return self._beamformer
1707
1806
 
1708
- def calc(self, ac, fr):
1709
- """Calculates the CLEAN result for the frequencies defined by :attr:`freq_data`.
1807
+ def _set_beamformer(self, beamformer):
1808
+ msg = (
1809
+ f"Deprecated use of 'beamformer' trait in class {self.__class__.__name__}. "
1810
+ 'Please set :attr:`freq_data`, :attr:`steer`, :attr:`r_diag` directly. '
1811
+ "Using the 'beamformer' trait will be removed in version 25.07."
1812
+ )
1813
+ warn(
1814
+ msg,
1815
+ DeprecationWarning,
1816
+ stacklevel=2,
1817
+ )
1818
+ self._beamformer = beamformer
1819
+
1820
+ @on_trait_change('_beamformer.digest')
1821
+ def delegate_beamformer_traits(self):
1822
+ self.freq_data = self.beamformer.freq_data
1823
+ self.r_diag = self.beamformer.r_diag
1824
+ self.steer = self.beamformer.steer
1825
+
1826
+ def _calc(self, ind):
1827
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1710
1828
 
1711
1829
  This is an internal helper function that is automatically called when
1712
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
1713
- its :meth:`~BeamformerBase.synthetic` method.
1830
+ accessing the beamformer's :attr:`result` or calling
1831
+ its :meth:`synthetic` method.
1714
1832
 
1715
1833
  Parameters
1716
1834
  ----------
1717
- ac : array of floats
1718
- This array of dimension ([number of frequencies]x[number of gridpoints])
1719
- is used as call-by-reference parameter and contains the calculated
1720
- value after calling this method.
1721
- fr : array of booleans
1722
- The entries of this [number of frequencies]-sized array are either
1723
- 'True' (if the result for this frequency has already been calculated)
1724
- or 'False' (for the frequencies where the result has yet to be calculated).
1725
- After the calculation at a certain frequency the value will be set
1726
- to 'True'
1835
+ ind : array of int
1836
+ This array contains all frequency indices for which (re)calculation is
1837
+ to be performed
1727
1838
 
1728
1839
  Returns
1729
1840
  -------
1730
- This method only returns values through the *ac* and *fr* parameters
1841
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1731
1842
 
1732
1843
  """
1733
- f = self.freq_data.fftfreq()
1844
+ f = self._f
1734
1845
  gs = self.steer.grid.size
1846
+ normfactor = self.sig_loss_norm()
1735
1847
 
1736
1848
  if self.calcmode == 'full':
1737
1849
  warn(
@@ -1740,33 +1852,44 @@ class BeamformerClean(BeamformerBase):
1740
1852
  stacklevel=2,
1741
1853
  )
1742
1854
  p = PointSpreadFunction(steer=self.steer, calcmode=self.calcmode, precision=self.psf_precision)
1743
- for i in self.freq_data.indices:
1744
- if not fr[i]:
1745
- p.freq = f[i]
1746
- dirty = self.beamformer.result[i].copy()
1747
- clean = zeros(gs, dtype=dirty.dtype)
1748
-
1749
- i_iter = 0
1750
- flag = True
1751
- while flag:
1752
- # TODO: negative werte!!!
1753
- dirty_sum = abs(dirty).sum(0)
1754
- next_max = dirty.argmax(0)
1755
- p.grid_indices = array([next_max])
1756
- psf = p.psf.reshape(gs)
1757
- new_amp = self.damp * dirty[next_max] # / psf[next_max]
1758
- clean[next_max] += new_amp
1759
- dirty -= psf * new_amp
1760
- i_iter += 1
1761
- flag = dirty_sum > abs(dirty).sum(0) and i_iter < self.n_iter and max(dirty) > 0
1762
-
1763
- ac[i] = clean
1764
- fr[i] = 1
1855
+ param_steer_type, steer_vector = self._beamformer_params()
1856
+ for i in ind:
1857
+ p.freq = f[i]
1858
+ csm = array(self.freq_data.csm[i], dtype='complex128')
1859
+ dirty = beamformerFreq(
1860
+ param_steer_type,
1861
+ self.r_diag,
1862
+ normfactor,
1863
+ steer_vector(f[i]),
1864
+ csm,
1865
+ )[0]
1866
+ if self.r_diag: # set (unphysical) negative output values to 0
1867
+ indNegSign = sign(dirty) < 0
1868
+ dirty[indNegSign] = 0.0
1869
+
1870
+ clean = zeros(gs, dtype=dirty.dtype)
1871
+ i_iter = 0
1872
+ flag = True
1873
+ while flag:
1874
+ dirty_sum = abs(dirty).sum(0)
1875
+ next_max = dirty.argmax(0)
1876
+ p.grid_indices = array([next_max])
1877
+ psf = p.psf.reshape(gs)
1878
+ new_amp = self.damp * dirty[next_max] # / psf[next_max]
1879
+ clean[next_max] += new_amp
1880
+ dirty -= psf * new_amp
1881
+ i_iter += 1
1882
+ flag = dirty_sum > abs(dirty).sum(0) and i_iter < self.n_iter and max(dirty) > 0
1883
+
1884
+ self._ac[i] = clean
1885
+ self._fr[i] = 1
1765
1886
 
1766
1887
 
1767
1888
  class BeamformerCMF(BeamformerBase):
1768
- """Covariance Matrix Fitting, see :ref:`Yardibi et al., 2008<Yardibi2008>`.
1889
+ """Covariance Matrix Fitting algorithm.
1890
+
1769
1891
  This is not really a beamformer, but an inverse method.
1892
+ See :cite:`Yardibi2008` for details.
1770
1893
  """
1771
1894
 
1772
1895
  #: Type of fit method to be used ('LassoLars', 'LassoLarsBIC',
@@ -1804,9 +1927,24 @@ class BeamformerCMF(BeamformerBase):
1804
1927
  #: If True, shows the status of the PyLops solver. Only relevant in case of FISTA or Split_Bregman
1805
1928
  show = Bool(False, desc='show output of PyLops solvers')
1806
1929
 
1930
+ #: Energy normalization in case of diagonal removal not implemented for inverse methods.
1931
+ r_diag_norm = Enum(
1932
+ None,
1933
+ desc='Energy normalization in case of diagonal removal not implemented for inverse methods',
1934
+ )
1935
+
1807
1936
  # internal identifier
1808
1937
  digest = Property(
1809
- depends_on=['freq_data.digest', 'alpha', 'method', 'max_iter', 'unit_mult', 'r_diag', 'steer.inv_digest'],
1938
+ depends_on=[
1939
+ 'freq_data.digest',
1940
+ 'alpha',
1941
+ 'method',
1942
+ 'max_iter',
1943
+ 'unit_mult',
1944
+ 'r_diag',
1945
+ 'precision',
1946
+ 'steer.inv_digest',
1947
+ ],
1810
1948
  )
1811
1949
 
1812
1950
  @cached_property
@@ -1822,172 +1960,160 @@ class BeamformerCMF(BeamformerBase):
1822
1960
  )
1823
1961
  raise ImportError(msg)
1824
1962
 
1825
- def calc(self, ac, fr):
1826
- """Calculates the CMF result for the frequencies defined by :attr:`freq_data`.
1963
+ def _calc(self, ind):
1964
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1827
1965
 
1828
1966
  This is an internal helper function that is automatically called when
1829
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
1830
- its :meth:`~BeamformerBase.synthetic` method.
1967
+ accessing the beamformer's :attr:`result` or calling
1968
+ its :meth:`synthetic` method.
1831
1969
 
1832
1970
  Parameters
1833
1971
  ----------
1834
- ac : array of floats
1835
- This array of dimension ([number of frequencies]x[number of gridpoints])
1836
- is used as call-by-reference parameter and contains the calculated
1837
- value after calling this method.
1838
- fr : array of booleans
1839
- The entries of this [number of frequencies]-sized array are either
1840
- 'True' (if the result for this frequency has already been calculated)
1841
- or 'False' (for the frequencies where the result has yet to be calculated).
1842
- After the calculation at a certain frequency the value will be set
1843
- to 'True'
1972
+ ind : array of int
1973
+ This array contains all frequency indices for which (re)calculation is
1974
+ to be performed
1844
1975
 
1845
1976
  Returns
1846
1977
  -------
1847
- This method only returns values through the *ac* and *fr* parameters
1978
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1848
1979
 
1849
1980
  """
1981
+ f = self._f
1850
1982
 
1851
1983
  # function to repack complex matrices to deal with them in real number space
1852
- def realify(M):
1853
- return vstack([M.real, M.imag])
1984
+ def realify(matrix):
1985
+ return vstack([matrix.real, matrix.imag])
1854
1986
 
1855
1987
  # prepare calculation
1856
- i = self.freq_data.indices
1857
- f = self.freq_data.fftfreq()
1858
1988
  nc = self.freq_data.numchannels
1859
1989
  numpoints = self.steer.grid.size
1860
1990
  unit = self.unit_mult
1861
1991
 
1862
- for i in self.freq_data.indices:
1863
- if not fr[i]:
1864
- csm = array(self.freq_data.csm[i], dtype='complex128', copy=1)
1992
+ for i in ind:
1993
+ csm = array(self.freq_data.csm[i], dtype='complex128', copy=1)
1865
1994
 
1866
- h = self.steer.transfer(f[i]).T
1995
+ h = self.steer.transfer(f[i]).T
1867
1996
 
1868
- # reduced Kronecker product (only where solution matrix != 0)
1869
- Bc = (h[:, :, newaxis] * h.conjugate().T[newaxis, :, :]).transpose(2, 0, 1)
1870
- Ac = Bc.reshape(nc * nc, numpoints)
1997
+ # reduced Kronecker product (only where solution matrix != 0)
1998
+ Bc = (h[:, :, newaxis] * h.conjugate().T[newaxis, :, :]).transpose(2, 0, 1)
1999
+ Ac = Bc.reshape(nc * nc, numpoints)
1871
2000
 
1872
- # get indices for upper triangular matrices (use tril b/c transposed)
1873
- ind = reshape(tril(ones((nc, nc))), (nc * nc,)) > 0
2001
+ # get indices for upper triangular matrices (use tril b/c transposed)
2002
+ ind = reshape(tril(ones((nc, nc))), (nc * nc,)) > 0
1874
2003
 
1875
- ind_im0 = (reshape(eye(nc), (nc * nc,)) == 0)[ind]
1876
- if self.r_diag:
1877
- # omit main diagonal for noise reduction
1878
- ind_reim = hstack([ind_im0, ind_im0])
1879
- else:
1880
- # take all real parts -- also main diagonal
1881
- ind_reim = hstack([ones(size(ind_im0)) > 0, ind_im0])
1882
- ind_reim[0] = True # why this ?
1883
-
1884
- A = realify(Ac[ind, :])[ind_reim, :]
1885
- # use csm.T for column stacking reshape!
1886
- R = realify(reshape(csm.T, (nc * nc, 1))[ind, :])[ind_reim, :] * unit
1887
- # choose method
1888
- if self.method == 'LassoLars':
1889
- model = LassoLars(alpha=self.alpha * unit, max_iter=self.max_iter, **sklearn_ndict)
1890
- elif self.method == 'LassoLarsBIC':
1891
- model = LassoLarsIC(criterion='bic', max_iter=self.max_iter, **sklearn_ndict)
1892
- elif self.method == 'OMPCV':
1893
- model = OrthogonalMatchingPursuitCV(**sklearn_ndict)
1894
- elif self.method == 'NNLS':
1895
- model = LinearRegression(positive=True)
1896
-
1897
- if self.method == 'Split_Bregman' and config.have_pylops:
1898
- from pylops import Identity, MatrixMult, SplitBregman
1899
-
1900
- Oop = MatrixMult(A) # tranfer operator
1901
- Iop = self.alpha * Identity(numpoints) # regularisation
1902
- ac[i], iterations = SplitBregman(
1903
- Oop,
1904
- [Iop],
1905
- R[:, 0],
1906
- niter_outer=self.max_iter,
1907
- niter_inner=5,
1908
- RegsL2=None,
1909
- dataregsL2=None,
1910
- mu=1.0,
1911
- epsRL1s=[1],
1912
- tol=1e-10,
1913
- tau=1.0,
1914
- show=self.show,
1915
- )
1916
- ac[i] /= unit
1917
-
1918
- elif self.method == 'FISTA' and config.have_pylops:
1919
- from pylops import FISTA, MatrixMult
1920
-
1921
- Oop = MatrixMult(A) # tranfer operator
1922
- ac[i], iterations = FISTA(
1923
- Op=Oop,
1924
- data=R[:, 0],
1925
- niter=self.max_iter,
1926
- eps=self.alpha,
1927
- alpha=None,
1928
- eigsiter=None,
1929
- eigstol=0,
1930
- tol=1e-10,
1931
- show=self.show,
1932
- )
1933
- ac[i] /= unit
1934
- elif self.method == 'fmin_l_bfgs_b':
1935
- # function to minimize
1936
- def function(x):
1937
- # function
1938
- func = x.T @ A.T @ A @ x - 2 * R.T @ A @ x + R.T @ R
1939
- # derivitaive
1940
- der = 2 * A.T @ A @ x.T[:, newaxis] - 2 * A.T @ R
1941
- return func[0].T, der[:, 0]
1942
-
1943
- # initial guess
1944
- x0 = ones([numpoints])
1945
- # boundarys - set to non negative
1946
- boundarys = tile((0, +inf), (len(x0), 1))
1947
-
1948
- # optimize
1949
- ac[i], yval, dicts = fmin_l_bfgs_b(
1950
- function,
1951
- x0,
1952
- fprime=None,
1953
- args=(),
1954
- approx_grad=0,
1955
- bounds=boundarys,
1956
- m=10,
1957
- factr=10000000.0,
1958
- pgtol=1e-05,
1959
- epsilon=1e-08,
1960
- iprint=-1,
1961
- maxfun=15000,
1962
- maxiter=self.max_iter,
1963
- disp=None,
1964
- callback=None,
1965
- maxls=20,
1966
- )
2004
+ ind_im0 = (reshape(eye(nc), (nc * nc,)) == 0)[ind]
2005
+ if self.r_diag:
2006
+ # omit main diagonal for noise reduction
2007
+ ind_reim = hstack([ind_im0, ind_im0])
2008
+ else:
2009
+ # take all real parts -- also main diagonal
2010
+ ind_reim = hstack([ones(size(ind_im0)) > 0, ind_im0])
2011
+ ind_reim[0] = True # why this ?
2012
+
2013
+ A = realify(Ac[ind, :])[ind_reim, :]
2014
+ # use csm.T for column stacking reshape!
2015
+ R = realify(reshape(csm.T, (nc * nc, 1))[ind, :])[ind_reim, :] * unit
2016
+ # choose method
2017
+ if self.method == 'LassoLars':
2018
+ model = LassoLars(alpha=self.alpha * unit, max_iter=self.max_iter, **sklearn_ndict)
2019
+ elif self.method == 'LassoLarsBIC':
2020
+ model = LassoLarsIC(criterion='bic', max_iter=self.max_iter, **sklearn_ndict)
2021
+ elif self.method == 'OMPCV':
2022
+ model = OrthogonalMatchingPursuitCV(**sklearn_ndict)
2023
+ elif self.method == 'NNLS':
2024
+ model = LinearRegression(positive=True)
2025
+
2026
+ if self.method == 'Split_Bregman' and config.have_pylops:
2027
+ from pylops import Identity, MatrixMult, SplitBregman
2028
+
2029
+ Oop = MatrixMult(A) # tranfer operator
2030
+ Iop = self.alpha * Identity(numpoints) # regularisation
2031
+ self._ac[i], iterations = SplitBregman(
2032
+ Oop,
2033
+ [Iop],
2034
+ R[:, 0],
2035
+ niter_outer=self.max_iter,
2036
+ niter_inner=5,
2037
+ RegsL2=None,
2038
+ dataregsL2=None,
2039
+ mu=1.0,
2040
+ epsRL1s=[1],
2041
+ tol=1e-10,
2042
+ tau=1.0,
2043
+ show=self.show,
2044
+ )
2045
+ self._ac[i] /= unit
2046
+
2047
+ elif self.method == 'FISTA' and config.have_pylops:
2048
+ from pylops import FISTA, MatrixMult
2049
+
2050
+ Oop = MatrixMult(A) # tranfer operator
2051
+ self._ac[i], iterations = FISTA(
2052
+ Op=Oop,
2053
+ data=R[:, 0],
2054
+ niter=self.max_iter,
2055
+ eps=self.alpha,
2056
+ alpha=None,
2057
+ eigsiter=None,
2058
+ eigstol=0,
2059
+ tol=1e-10,
2060
+ show=self.show,
2061
+ )
2062
+ self._ac[i] /= unit
2063
+ elif self.method == 'fmin_l_bfgs_b':
2064
+ # function to minimize
2065
+ def function(x):
2066
+ # function
2067
+ func = x.T @ A.T @ A @ x - 2 * R.T @ A @ x + R.T @ R
2068
+ # derivitaive
2069
+ der = 2 * A.T @ A @ x.T[:, newaxis] - 2 * A.T @ R
2070
+ return func[0].T, der[:, 0]
2071
+
2072
+ # initial guess
2073
+ x0 = ones([numpoints])
2074
+ # boundarys - set to non negative
2075
+ boundarys = tile((0, +inf), (len(x0), 1))
2076
+
2077
+ # optimize
2078
+ self._ac[i], yval, dicts = fmin_l_bfgs_b(
2079
+ function,
2080
+ x0,
2081
+ fprime=None,
2082
+ args=(),
2083
+ approx_grad=0,
2084
+ bounds=boundarys,
2085
+ m=10,
2086
+ factr=10000000.0,
2087
+ pgtol=1e-05,
2088
+ epsilon=1e-08,
2089
+ iprint=-1,
2090
+ maxfun=15000,
2091
+ maxiter=self.max_iter,
2092
+ disp=None,
2093
+ callback=None,
2094
+ maxls=20,
2095
+ )
1967
2096
 
1968
- ac[i] /= unit
1969
- else:
1970
- # from sklearn 1.2, normalize=True does not work the same way anymore and the pipeline
1971
- # approach with StandardScaler does scale in a different way, thus we monkeypatch the
1972
- # code and normalize ourselves to make results the same over different sklearn versions
1973
- norms = norm(A, axis=0)
1974
- # get rid of annoying sklearn warnings that appear for sklearn<1.2 despite any settings
1975
- with warnings.catch_warnings():
1976
- warnings.simplefilter('ignore', category=FutureWarning)
1977
- # normalized A
1978
- model.fit(A / norms, R[:, 0])
1979
- # recover normalization in the coef's
1980
- ac[i] = model.coef_[:] / norms / unit
1981
- fr[i] = 1
2097
+ self._ac[i] /= unit
2098
+ else:
2099
+ # from sklearn 1.2, normalize=True does not work the same way anymore and the pipeline
2100
+ # approach with StandardScaler does scale in a different way, thus we monkeypatch the
2101
+ # code and normalize ourselves to make results the same over different sklearn versions
2102
+ norms = norm(A, axis=0)
2103
+ # get rid of annoying sklearn warnings that appear for sklearn<1.2 despite any settings
2104
+ with warnings.catch_warnings():
2105
+ warnings.simplefilter('ignore', category=FutureWarning)
2106
+ # normalized A
2107
+ model.fit(A / norms, R[:, 0])
2108
+ # recover normalization in the coef's
2109
+ self._ac[i] = model.coef_[:] / norms / unit
2110
+ self._fr[i] = 1
1982
2111
 
1983
2112
 
1984
2113
  class BeamformerSODIX(BeamformerBase):
1985
- """SODIX, see Funke, Ein Mikrofonarray-Verfahren zur Untersuchung der
1986
- Schallabstrahlung von Turbofantriebwerken, 2017. and
1987
- Oertwig, Advancements in the source localization method SODIX and
1988
- application to short cowl engine data, 2019.
2114
+ """Source directivity modeling in the cross-spectral matrix (SODIX) algorithm.
1989
2115
 
1990
- Source directivity modeling in the cross-spectral matrix
2116
+ See :cite:`Funke2017` and :cite:`Oertwig2019` for details.
1991
2117
  """
1992
2118
 
1993
2119
  #: Type of fit method to be used ('fmin_l_bfgs_b').
@@ -2000,10 +2126,6 @@ class BeamformerSODIX(BeamformerBase):
2000
2126
  #: defaults to 200
2001
2127
  max_iter = Int(200, desc='maximum number of iterations')
2002
2128
 
2003
- #: Norm to consider for the regularization
2004
- #: defaults to L-1 Norm
2005
- pnorm = Float(1, desc='Norm for regularization')
2006
-
2007
2129
  #: Weight factor for regularization,
2008
2130
  #: defaults to 0.0.
2009
2131
  alpha = Range(0.0, 1.0, 0.0, desc='regularization factor')
@@ -2014,215 +2136,60 @@ class BeamformerSODIX(BeamformerBase):
2014
2136
  #: within fitting method algorithms. Defaults to 1e9.
2015
2137
  unit_mult = Float(1e9, desc='unit multiplier')
2016
2138
 
2017
- #: The beamforming result as squared sound pressure values
2018
- #: at all grid point locations (readonly).
2019
- #: Returns a (number of frequencies, number of gridpoints) array of floats.
2020
- sodix_result = Property(desc='beamforming result')
2139
+ #: Energy normalization in case of diagonal removal not implemented for inverse methods.
2140
+ r_diag_norm = Enum(
2141
+ None,
2142
+ desc='Energy normalization in case of diagonal removal not implemented for inverse methods',
2143
+ )
2021
2144
 
2022
2145
  # internal identifier
2023
2146
  digest = Property(
2024
- depends_on=['freq_data.digest', 'alpha', 'method', 'max_iter', 'unit_mult', 'r_diag', 'steer.inv_digest'],
2147
+ depends_on=[
2148
+ 'freq_data.digest',
2149
+ 'alpha',
2150
+ 'method',
2151
+ 'max_iter',
2152
+ 'unit_mult',
2153
+ 'r_diag',
2154
+ 'precision',
2155
+ 'steer.inv_digest',
2156
+ ],
2025
2157
  )
2026
2158
 
2027
2159
  @cached_property
2028
2160
  def _get_digest(self):
2029
2161
  return digest(self)
2030
2162
 
2031
- def _get_filecache(self):
2032
- """Function collects cached results from file depending on
2033
- global/local caching behaviour. Returns (None, None) if no cachefile/data
2034
- exist and global caching mode is 'readonly'.
2035
- """
2036
- H5cache.get_cache_file(self, self.freq_data.basename)
2037
- if not self.h5f:
2038
- return (None, None) # only happens in case of global caching readonly
2039
-
2040
- nodename = self.__class__.__name__ + self.digest
2041
- if config.global_caching == 'overwrite' and self.h5f.is_cached(nodename):
2042
- self.h5f.remove_data(nodename) # remove old data before writing in overwrite mode
2043
-
2044
- if not self.h5f.is_cached(nodename):
2045
- if config.global_caching == 'readonly':
2046
- return (None, None)
2047
- # print("initialize data.")
2048
- numfreq = self.freq_data.fftfreq().shape[0] # block_size/2 + 1steer_obj
2049
- group = self.h5f.create_new_group(nodename)
2050
- self.h5f.create_compressible_array(
2051
- 'result',
2052
- (numfreq, self.steer.grid.size * self.steer.mics.num_mics),
2053
- self.precision,
2054
- group,
2055
- )
2056
- self.h5f.create_compressible_array(
2057
- 'freqs',
2058
- (numfreq,),
2059
- 'int8', #'bool',
2060
- group,
2061
- )
2062
- ac = self.h5f.get_data_by_reference('result', '/' + nodename)
2063
- fr = self.h5f.get_data_by_reference('freqs', '/' + nodename)
2064
- gpos = None
2065
- return (ac, fr, gpos)
2066
-
2067
- @property_depends_on('ext_digest')
2068
- def _get_sodix_result(self):
2069
- """Implements the :attr:`result` getter routine.
2070
- The sodix beamforming result is either loaded or calculated.
2071
- """
2072
- f = self.freq_data
2073
- numfreq = f.fftfreq().shape[0] # block_size/2 + 1steer_obj
2074
- _digest = ''
2075
- while self.digest != _digest:
2076
- _digest = self.digest
2077
- self._assert_equal_channels()
2078
- if not ( # if result caching is active
2079
- config.global_caching == 'none' or (config.global_caching == 'individual' and not self.cached)
2080
- ):
2081
- (ac, fr, gpos) = self._get_filecache()
2082
- if ac and fr:
2083
- if not fr[f.ind_low : f.ind_high].all():
2084
- if config.global_caching == 'readonly':
2085
- (ac, fr) = (ac[:], fr[:])
2086
- self.calc(ac, fr)
2087
- self.h5f.flush()
2088
-
2089
- else:
2090
- ac = zeros((numfreq, self.steer.grid.size * self.steer.mics.num_mics), dtype=self.precision)
2091
- fr = zeros(numfreq, dtype='int8')
2092
- self.calc(ac, fr)
2093
- else:
2094
- ac = zeros((numfreq, self.steer.grid.size * self.steer.mics.num_mics), dtype=self.precision)
2095
- fr = zeros(numfreq, dtype='int8')
2096
- self.calc(ac, fr)
2097
- return ac
2098
-
2099
- def synthetic(self, f, num=0):
2100
- """Evaluates the beamforming result for an arbitrary frequency band.
2101
-
2102
- Parameters
2103
- ----------
2104
- f: float
2105
- Band center frequency.
2106
- num : integer
2107
- Controls the width of the frequency bands considered; defaults to
2108
- 0 (single frequency line).
2109
-
2110
- === =====================
2111
- num frequency band width
2112
- === =====================
2113
- 0 single frequency line
2114
- 1 octave band
2115
- 3 third-octave band
2116
- n 1/n-octave band
2117
- === =====================
2118
-
2119
- Returns
2120
- -------
2121
- array of floats
2122
- The synthesized frequency band values of the beamforming result at
2123
- each grid point and each microphone .
2124
- Note that the frequency resolution and therefore the bandwidth
2125
- represented by a single frequency line depends on
2126
- the :attr:`sampling frequency<acoular.sources.SamplesGenerator.sample_freq>` and conjugate
2127
- used :attr:`FFT block size<acoular.spectra.PowerSpectra.block_size>`.
2128
-
2129
- """
2130
- res = self.sodix_result # trigger calculation
2131
- freq = self.freq_data.fftfreq()
2132
- if len(freq) == 0:
2133
- return None
2134
-
2135
- indices = self.freq_data.indices
2136
-
2137
- if num == 0:
2138
- # single frequency line
2139
- ind = searchsorted(freq, f)
2140
- if ind >= len(freq):
2141
- warn(
2142
- 'Queried frequency (%g Hz) not in resolved frequency range. Returning zeros.' % f,
2143
- Warning,
2144
- stacklevel=2,
2145
- )
2146
- h = zeros_like(res[0])
2147
- else:
2148
- if freq[ind] != f:
2149
- warn(
2150
- f'Queried frequency ({f:g} Hz) not in set of '
2151
- 'discrete FFT sample frequencies. '
2152
- f'Using frequency {freq[ind]:g} Hz instead.',
2153
- Warning,
2154
- stacklevel=2,
2155
- )
2156
- if ind not in indices:
2157
- warn(
2158
- 'Beamforming result may not have been calculated '
2159
- 'for queried frequency. Check '
2160
- 'freq_data.ind_low and freq_data.ind_high!',
2161
- Warning,
2162
- stacklevel=2,
2163
- )
2164
- h = res[ind]
2165
- else:
2166
- # fractional octave band
2167
- f1 = f * 2.0 ** (-0.5 / num)
2168
- f2 = f * 2.0 ** (+0.5 / num)
2169
- ind1 = searchsorted(freq, f1)
2170
- ind2 = searchsorted(freq, f2)
2171
- if ind1 == ind2:
2172
- warn(
2173
- f'Queried frequency band ({f1:g} to {f2:g} Hz) does not '
2174
- 'include any discrete FFT sample frequencies. '
2175
- 'Returning zeros.',
2176
- Warning,
2177
- stacklevel=2,
2178
- )
2179
- h = zeros_like(res[0])
2180
- else:
2181
- h = sum(res[ind1:ind2], 0)
2182
- if not ((ind1 in indices) and (ind2 in indices)):
2183
- warn(
2184
- 'Beamforming result may not have been calculated '
2185
- 'for all queried frequencies. Check '
2186
- 'freq_data.ind_low and freq_data.ind_high!',
2187
- Warning,
2188
- stacklevel=2,
2189
- )
2190
- return h.reshape([self.steer.grid.size, self.steer.mics.num_mics])
2191
-
2192
- def calc(self, ac, fr):
2193
- """Calculates the SODIX result for the frequencies defined by :attr:`freq_data`.
2163
+ def _calc(self, ind):
2164
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
2194
2165
 
2195
2166
  This is an internal helper function that is automatically called when
2196
- accessing the beamformer's :attr:`~Beamformer.sodix_result` or calling
2197
- its :meth:`~BeamformerSODIX.synthetic` method.
2167
+ accessing the beamformer's :attr:`result` or calling
2168
+ its :meth:`synthetic` method.
2198
2169
 
2199
2170
  Parameters
2200
2171
  ----------
2201
- ac : array of floats
2202
- This array of dimension ([number of frequencies]x[number of gridpoints]x[number of microphones])
2203
- is used as call-by-reference parameter and contains the calculated
2204
- value after calling this method.
2205
- fr : array of booleans
2206
- The entries of this [number of frequencies]-sized array are either
2207
- 'True' (if the result for this frequency has already been calculated)
2208
- or 'False' (for the frequencies where the result has yet to be calculated).
2209
- After the calculation at a certain frequency the value will be set
2210
- to 'True'
2172
+ ind : array of int
2173
+ This array contains all frequency indices for which (re)calculation is
2174
+ to be performed
2211
2175
 
2212
2176
  Returns
2213
2177
  -------
2214
- This method only returns values through the *ac* and *fr* parameters
2178
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
2215
2179
 
2216
2180
  """
2217
2181
  # prepare calculation
2218
- i = self.freq_data.indices
2219
- f = self.freq_data.fftfreq()
2182
+ f = self._f
2220
2183
  numpoints = self.steer.grid.size
2221
2184
  # unit = self.unit_mult
2222
2185
  num_mics = self.steer.mics.num_mics
2223
-
2224
- for i in self.freq_data.indices:
2225
- if not fr[i]:
2186
+ # SODIX needs special treatment as the result from one frequency is used to
2187
+ # determine the initial guess for the next frequency in order to speed up
2188
+ # computation. Instead of just solving for only the frequencies in ind, we
2189
+ # start with index 1 (minimum frequency) and also check if the result is
2190
+ # already computed
2191
+ for i in range(1, ind.max() + 1):
2192
+ if not self._fr[i]:
2226
2193
  # measured csm
2227
2194
  csm = array(self.freq_data.csm[i], dtype='complex128', copy=1)
2228
2195
  # transfer function
@@ -2230,10 +2197,10 @@ class BeamformerSODIX(BeamformerBase):
2230
2197
 
2231
2198
  if self.method == 'fmin_l_bfgs_b':
2232
2199
  # function to minimize
2233
- def function(D):
2200
+ def function(directions):
2234
2201
  """Parameters
2235
2202
  ----------
2236
- D
2203
+ directions
2237
2204
  [numpoints*num_mics]
2238
2205
 
2239
2206
  Returns
@@ -2245,7 +2212,7 @@ class BeamformerSODIX(BeamformerBase):
2245
2212
 
2246
2213
  """
2247
2214
  #### the sodix function ####
2248
- Djm = D.reshape([numpoints, num_mics])
2215
+ Djm = directions.reshape([numpoints, num_mics])
2249
2216
  p = h.T * Djm
2250
2217
  csm_mod = dot(p.T, p.conj())
2251
2218
  Q = csm - csm_mod
@@ -2262,11 +2229,11 @@ class BeamformerSODIX(BeamformerBase):
2262
2229
  return func, derdrl.ravel()
2263
2230
 
2264
2231
  ##### initial guess ####
2265
- if all(ac[(i - 1)] == 0):
2232
+ if not self._fr[(i - 1)]:
2266
2233
  D0 = ones([numpoints, num_mics])
2267
2234
  else:
2268
2235
  D0 = sqrt(
2269
- ac[(i - 1)]
2236
+ self._ac[(i - 1)]
2270
2237
  * real(trace(csm) / trace(array(self.freq_data.csm[i - 1], dtype='complex128', copy=1))),
2271
2238
  )
2272
2239
 
@@ -2295,14 +2262,17 @@ class BeamformerSODIX(BeamformerBase):
2295
2262
  maxls=20,
2296
2263
  )
2297
2264
  # squared pressure
2298
- ac[i] = qi**2
2265
+ self._ac[i] = qi**2
2299
2266
  else:
2300
2267
  pass
2301
- fr[i] = 1
2268
+ self._fr[i] = 1
2302
2269
 
2303
2270
 
2304
2271
  class BeamformerGIB(BeamformerEig): # BeamformerEig #BeamformerBase
2305
- """Beamforming GIB methods with different normalizations,."""
2272
+ """Beamforming GIB methods with different normalizations.
2273
+
2274
+ See :cite:`Suzuki2011` for details.
2275
+ """
2306
2276
 
2307
2277
  #: Unit multiplier for evaluating, e.g., nPa instead of Pa.
2308
2278
  #: Values are converted back before returning.
@@ -2352,11 +2322,18 @@ class BeamformerGIB(BeamformerEig): # BeamformerEig #BeamformerBase
2352
2322
  # First eigenvalue to consider. Defaults to 0.
2353
2323
  m = Int(0, desc='First eigenvalue to consider')
2354
2324
 
2355
- # internal identifier++++++++++++++++++++++++++++++++++++++++++++++++++
2325
+ #: Energy normalization in case of diagonal removal not implemented for inverse methods.
2326
+ r_diag_norm = Enum(
2327
+ None,
2328
+ desc='Energy normalization in case of diagonal removal not implemented for inverse methods',
2329
+ )
2330
+
2331
+ # internal identifier
2356
2332
  digest = Property(
2357
2333
  depends_on=[
2358
2334
  'steer.inv_digest',
2359
2335
  'freq_data.digest',
2336
+ 'precision',
2360
2337
  'alpha',
2361
2338
  'method',
2362
2339
  'max_iter',
@@ -2381,33 +2358,25 @@ class BeamformerGIB(BeamformerEig): # BeamformerEig #BeamformerBase
2381
2358
  na = max(nm + na, 0)
2382
2359
  return min(nm - 1, na)
2383
2360
 
2384
- def calc(self, ac, fr):
2361
+ def _calc(self, ind):
2385
2362
  """Calculates the result for the frequencies defined by :attr:`freq_data`.
2386
2363
 
2387
2364
  This is an internal helper function that is automatically called when
2388
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
2389
- its :meth:`~BeamformerBase.synthetic` method.
2365
+ accessing the beamformer's :attr:`result` or calling
2366
+ its :meth:`synthetic` method.
2390
2367
 
2391
2368
  Parameters
2392
2369
  ----------
2393
- ac : array of floats
2394
- This array of dimension ([number of frequencies]x[number of gridpoints])
2395
- is used as call-by-reference parameter and contains the calculated
2396
- value after calling this method.
2397
- fr : array of booleans
2398
- The entries of this [number of frequencies]-sized array are either
2399
- 'True' (if the result for this frequency has already been calculated)
2400
- or 'False' (for the frequencies where the result has yet to be calculated).
2401
- After the calculation at a certain frequency the value will be set
2402
- to 'True'
2370
+ ind : array of int
2371
+ This array contains all frequency indices for which (re)calculation is
2372
+ to be performed
2403
2373
 
2404
2374
  Returns
2405
2375
  -------
2406
- This method only returns values through the *ac* and *fr* parameters
2376
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
2407
2377
 
2408
2378
  """
2409
- # prepare calculation
2410
- f = self.freq_data.fftfreq()
2379
+ f = self._f
2411
2380
  n = int(self.na) # number of eigenvalues
2412
2381
  m = int(self.m) # number of first eigenvalue
2413
2382
  numchannels = self.freq_data.numchannels # number of channels
@@ -2415,122 +2384,121 @@ class BeamformerGIB(BeamformerEig): # BeamformerEig #BeamformerBase
2415
2384
  hh = zeros((1, numpoints, numchannels), dtype='D')
2416
2385
 
2417
2386
  # Generate a cross spectral matrix, and perform the eigenvalue decomposition
2418
- for i in self.freq_data.indices:
2419
- if not fr[i]:
2420
- # for monopole and source strenght Q needs to define density
2421
- # calculate a transfer matrix A
2422
- hh = self.steer.transfer(f[i])
2423
- A = hh.T
2424
- # eigenvalues and vectors
2425
- csm = array(self.freq_data.csm[i], dtype='complex128', copy=1)
2426
- eva, eve = eigh(csm)
2427
- eva = eva[::-1]
2428
- eve = eve[:, ::-1]
2429
- eva[eva < max(eva) / 1e12] = 0 # set small values zo 0, lowers numerical errors in simulated data
2430
- # init sources
2431
- qi = zeros([n + m, numpoints], dtype='complex128')
2432
- # Select the number of coherent modes to be processed referring to the eigenvalue distribution.
2433
- # for s in arange(n):
2434
- for s in list(range(m, n + m)):
2435
- if eva[s] > 0:
2436
- # Generate the corresponding eigenmodes
2437
- emode = array(sqrt(eva[s]) * eve[:, s], dtype='complex128')
2438
- # choose method for computation
2439
- if self.method == 'Suzuki':
2440
- leftpoints = numpoints
2441
- locpoints = arange(numpoints)
2442
- weights = diag(ones(numpoints))
2443
- epsilon = arange(self.max_iter)
2444
- for it in arange(self.max_iter):
2445
- if numchannels <= leftpoints:
2446
- AWA = dot(dot(A[:, locpoints], weights), A[:, locpoints].conj().T)
2447
- epsilon[it] = max(absolute(eigvals(AWA))) * self.eps_perc
2448
- qi[s, locpoints] = dot(
2449
- dot(
2450
- dot(weights, A[:, locpoints].conj().T),
2451
- inv(AWA + eye(numchannels) * epsilon[it]),
2452
- ),
2453
- emode,
2454
- )
2455
- elif numchannels > leftpoints:
2456
- AA = dot(A[:, locpoints].conj().T, A[:, locpoints])
2457
- epsilon[it] = max(absolute(eigvals(AA))) * self.eps_perc
2458
- qi[s, locpoints] = dot(
2459
- dot(inv(AA + inv(weights) * epsilon[it]), A[:, locpoints].conj().T),
2460
- emode,
2461
- )
2462
- if self.beta < 1 and it > 1:
2463
- # Reorder from the greatest to smallest magnitude to define a reduced-point source distribution , and reform a reduced transfer matrix
2464
- leftpoints = int(round(numpoints * self.beta ** (it + 1)))
2465
- idx = argsort(abs(qi[s, locpoints]))[::-1]
2466
- # print(it, leftpoints, locpoints, idx )
2467
- locpoints = delete(locpoints, [idx[leftpoints::]])
2468
- qix = zeros([n + m, leftpoints], dtype='complex128')
2469
- qix[s, :] = qi[s, locpoints]
2470
- # calc weights for next iteration
2471
- weights = diag(absolute(qix[s, :]) ** (2 - self.pnorm))
2472
- else:
2473
- weights = diag(absolute(qi[s, :]) ** (2 - self.pnorm))
2474
-
2475
- elif self.method == 'InverseIRLS':
2476
- weights = eye(numpoints)
2477
- locpoints = arange(numpoints)
2478
- for _it in arange(self.max_iter):
2479
- if numchannels <= numpoints:
2480
- wtwi = inv(dot(weights.T, weights))
2481
- aH = A.conj().T
2482
- qi[s, :] = dot(dot(wtwi, aH), dot(inv(dot(A, dot(wtwi, aH))), emode))
2483
- weights = diag(absolute(qi[s, :]) ** ((2 - self.pnorm) / 2))
2484
- weights = weights / sum(absolute(weights))
2485
- elif numchannels > numpoints:
2486
- wtw = dot(weights.T, weights)
2487
- qi[s, :] = dot(dot(inv(dot(dot(A.conj.T, wtw), A)), dot(A.conj().T, wtw)), emode)
2488
- weights = diag(absolute(qi[s, :]) ** ((2 - self.pnorm) / 2))
2489
- weights = weights / sum(absolute(weights))
2490
- else:
2491
- locpoints = arange(numpoints)
2492
- unit = self.unit_mult
2493
- AB = vstack([hstack([A.real, -A.imag]), hstack([A.imag, A.real])])
2494
- R = hstack([emode.real.T, emode.imag.T]) * unit
2495
- if self.method == 'LassoLars':
2496
- model = LassoLars(alpha=self.alpha * unit, max_iter=self.max_iter)
2497
- elif self.method == 'LassoLarsBIC':
2498
- model = LassoLarsIC(criterion='bic', max_iter=self.max_iter)
2499
- elif self.method == 'OMPCV':
2500
- model = OrthogonalMatchingPursuitCV()
2501
- elif self.method == 'LassoLarsCV':
2502
- model = LassoLarsCV()
2503
- elif self.method == 'NNLS':
2504
- model = LinearRegression(positive=True)
2505
- model.normalize = False
2506
- # from sklearn 1.2, normalize=True does not work
2507
- # the same way anymore and the pipeline approach
2508
- # with StandardScaler does scale in a different
2509
- # way, thus we monkeypatch the code and normalize
2510
- # ourselves to make results the same over different
2511
- # sklearn versions
2512
- norms = norm(AB, axis=0)
2513
- # get rid of annoying sklearn warnings that appear
2514
- # for sklearn<1.2 despite any settings
2515
- with warnings.catch_warnings():
2516
- warnings.simplefilter('ignore', category=FutureWarning)
2517
- # normalized A
2518
- model.fit(AB / norms, R)
2519
- # recover normalization in the coef's
2520
- qi_real, qi_imag = hsplit(model.coef_[:] / norms / unit, 2)
2521
- # print(s,qi.size)
2522
- qi[s, locpoints] = qi_real + qi_imag * 1j
2387
+ for i in ind:
2388
+ # for monopole and source strenght Q needs to define density
2389
+ # calculate a transfer matrix A
2390
+ hh = self.steer.transfer(f[i])
2391
+ A = hh.T
2392
+ # eigenvalues and vectors
2393
+ csm = array(self.freq_data.csm[i], dtype='complex128', copy=1)
2394
+ eva, eve = eigh(csm)
2395
+ eva = eva[::-1]
2396
+ eve = eve[:, ::-1]
2397
+ eva[eva < max(eva) / 1e12] = 0 # set small values zo 0, lowers numerical errors in simulated data
2398
+ # init sources
2399
+ qi = zeros([n + m, numpoints], dtype='complex128')
2400
+ # Select the number of coherent modes to be processed referring to the eigenvalue distribution.
2401
+ # for s in arange(n):
2402
+ for s in list(range(m, n + m)):
2403
+ if eva[s] > 0:
2404
+ # Generate the corresponding eigenmodes
2405
+ emode = array(sqrt(eva[s]) * eve[:, s], dtype='complex128')
2406
+ # choose method for computation
2407
+ if self.method == 'Suzuki':
2408
+ leftpoints = numpoints
2409
+ locpoints = arange(numpoints)
2410
+ weights = diag(ones(numpoints))
2411
+ epsilon = arange(self.max_iter)
2412
+ for it in arange(self.max_iter):
2413
+ if numchannels <= leftpoints:
2414
+ AWA = dot(dot(A[:, locpoints], weights), A[:, locpoints].conj().T)
2415
+ epsilon[it] = max(absolute(eigvals(AWA))) * self.eps_perc
2416
+ qi[s, locpoints] = dot(
2417
+ dot(
2418
+ dot(weights, A[:, locpoints].conj().T),
2419
+ inv(AWA + eye(numchannels) * epsilon[it]),
2420
+ ),
2421
+ emode,
2422
+ )
2423
+ elif numchannels > leftpoints:
2424
+ AA = dot(A[:, locpoints].conj().T, A[:, locpoints])
2425
+ epsilon[it] = max(absolute(eigvals(AA))) * self.eps_perc
2426
+ qi[s, locpoints] = dot(
2427
+ dot(inv(AA + inv(weights) * epsilon[it]), A[:, locpoints].conj().T),
2428
+ emode,
2429
+ )
2430
+ if self.beta < 1 and it > 1:
2431
+ # Reorder from the greatest to smallest magnitude to define a reduced-point source distribution , and reform a reduced transfer matrix
2432
+ leftpoints = int(round(numpoints * self.beta ** (it + 1)))
2433
+ idx = argsort(abs(qi[s, locpoints]))[::-1]
2434
+ # print(it, leftpoints, locpoints, idx )
2435
+ locpoints = delete(locpoints, [idx[leftpoints::]])
2436
+ qix = zeros([n + m, leftpoints], dtype='complex128')
2437
+ qix[s, :] = qi[s, locpoints]
2438
+ # calc weights for next iteration
2439
+ weights = diag(absolute(qix[s, :]) ** (2 - self.pnorm))
2440
+ else:
2441
+ weights = diag(absolute(qi[s, :]) ** (2 - self.pnorm))
2442
+
2443
+ elif self.method == 'InverseIRLS':
2444
+ weights = eye(numpoints)
2445
+ locpoints = arange(numpoints)
2446
+ for _it in arange(self.max_iter):
2447
+ if numchannels <= numpoints:
2448
+ wtwi = inv(dot(weights.T, weights))
2449
+ aH = A.conj().T
2450
+ qi[s, :] = dot(dot(wtwi, aH), dot(inv(dot(A, dot(wtwi, aH))), emode))
2451
+ weights = diag(absolute(qi[s, :]) ** ((2 - self.pnorm) / 2))
2452
+ weights = weights / sum(absolute(weights))
2453
+ elif numchannels > numpoints:
2454
+ wtw = dot(weights.T, weights)
2455
+ qi[s, :] = dot(dot(inv(dot(dot(A.conj.T, wtw), A)), dot(A.conj().T, wtw)), emode)
2456
+ weights = diag(absolute(qi[s, :]) ** ((2 - self.pnorm) / 2))
2457
+ weights = weights / sum(absolute(weights))
2523
2458
  else:
2524
- warn(
2525
- f'Eigenvalue {s:g} <= 0 for frequency index {i:g}. Will not be calculated!',
2526
- Warning,
2527
- stacklevel=2,
2528
- )
2529
- # Generate source maps of all selected eigenmodes, and superpose source intensity for each source type.
2530
- temp = zeros(numpoints)
2531
- temp[locpoints] = sum(absolute(qi[:, locpoints]) ** 2, axis=0)
2532
- ac[i] = temp
2533
- fr[i] = 1
2459
+ locpoints = arange(numpoints)
2460
+ unit = self.unit_mult
2461
+ AB = vstack([hstack([A.real, -A.imag]), hstack([A.imag, A.real])])
2462
+ R = hstack([emode.real.T, emode.imag.T]) * unit
2463
+ if self.method == 'LassoLars':
2464
+ model = LassoLars(alpha=self.alpha * unit, max_iter=self.max_iter)
2465
+ elif self.method == 'LassoLarsBIC':
2466
+ model = LassoLarsIC(criterion='bic', max_iter=self.max_iter)
2467
+ elif self.method == 'OMPCV':
2468
+ model = OrthogonalMatchingPursuitCV()
2469
+ elif self.method == 'LassoLarsCV':
2470
+ model = LassoLarsCV()
2471
+ elif self.method == 'NNLS':
2472
+ model = LinearRegression(positive=True)
2473
+ model.normalize = False
2474
+ # from sklearn 1.2, normalize=True does not work
2475
+ # the same way anymore and the pipeline approach
2476
+ # with StandardScaler does scale in a different
2477
+ # way, thus we monkeypatch the code and normalize
2478
+ # ourselves to make results the same over different
2479
+ # sklearn versions
2480
+ norms = norm(AB, axis=0)
2481
+ # get rid of annoying sklearn warnings that appear
2482
+ # for sklearn<1.2 despite any settings
2483
+ with warnings.catch_warnings():
2484
+ warnings.simplefilter('ignore', category=FutureWarning)
2485
+ # normalized A
2486
+ model.fit(AB / norms, R)
2487
+ # recover normalization in the coef's
2488
+ qi_real, qi_imag = hsplit(model.coef_[:] / norms / unit, 2)
2489
+ # print(s,qi.size)
2490
+ qi[s, locpoints] = qi_real + qi_imag * 1j
2491
+ else:
2492
+ warn(
2493
+ f'Eigenvalue {s:g} <= 0 for frequency index {i:g}. Will not be calculated!',
2494
+ Warning,
2495
+ stacklevel=2,
2496
+ )
2497
+ # Generate source maps of all selected eigenmodes, and superpose source intensity for each source type.
2498
+ temp = zeros(numpoints)
2499
+ temp[locpoints] = sum(absolute(qi[:, locpoints]) ** 2, axis=0)
2500
+ self._ac[i] = temp
2501
+ self._fr[i] = 1
2534
2502
 
2535
2503
 
2536
2504
  class BeamformerAdaptiveGrid(BeamformerBase, Grid):
@@ -2577,11 +2545,14 @@ class BeamformerAdaptiveGrid(BeamformerBase, Grid):
2577
2545
 
2578
2546
 
2579
2547
  class BeamformerGridlessOrth(BeamformerAdaptiveGrid):
2580
- """Orthogonal beamforming without predefined grid."""
2548
+ """Orthogonal beamforming without predefined grid.
2549
+
2550
+ See :cite:`Sarradj2022` for details.
2551
+ """
2581
2552
 
2582
2553
  #: List of components to consider, use this to directly set the eigenvalues
2583
2554
  #: used in the beamformer. Alternatively, set :attr:`n`.
2584
- eva_list = CArray(dtype=int, desc='components')
2555
+ eva_list = CArray(dtype=int, value=array([-1]), desc='components')
2585
2556
 
2586
2557
  #: Number of components to consider, defaults to 1. If set,
2587
2558
  #: :attr:`eva_list` will contain
@@ -2601,9 +2572,17 @@ class BeamformerGridlessOrth(BeamformerAdaptiveGrid):
2601
2572
  #: and 1 iteration
2602
2573
  shgo = Dict
2603
2574
 
2575
+ #: No normalization implemented. Defaults to 1.0.
2576
+ r_diag_norm = Enum(
2577
+ 1.0,
2578
+ desc='If diagonal of the csm is removed, some signal energy is lost.'
2579
+ 'This is handled via this normalization factor.'
2580
+ 'For this class, normalization is not implemented. Defaults to 1.0.',
2581
+ )
2582
+
2604
2583
  # internal identifier
2605
2584
  digest = Property(
2606
- depends_on=['freq_data.digest', '_steer_obj.digest', 'r_diag', 'eva_list', 'bounds', 'shgo'],
2585
+ depends_on=['freq_data.digest', '_steer_obj.digest', 'precision', 'r_diag', 'eva_list', 'bounds', 'shgo'],
2607
2586
  )
2608
2587
 
2609
2588
  @cached_property
@@ -2615,41 +2594,30 @@ class BeamformerGridlessOrth(BeamformerAdaptiveGrid):
2615
2594
  """Sets the list of eigenvalues to consider."""
2616
2595
  self.eva_list = arange(-1, -1 - self.n, -1)
2617
2596
 
2618
- @on_trait_change('eva_list')
2619
- def set_n(self):
2620
- """Sets the list of eigenvalues to consider."""
2621
- self.n = self.eva_list.shape[0]
2622
-
2623
2597
  @property_depends_on('n')
2624
2598
  def _get_size(self):
2625
2599
  return self.n * self.freq_data.fftfreq().shape[0]
2626
2600
 
2627
- def calc(self, ac, fr):
2601
+ def _calc(self, ind):
2628
2602
  """Calculates the result for the frequencies defined by :attr:`freq_data`.
2629
2603
 
2630
2604
  This is an internal helper function that is automatically called when
2631
- accessing the beamformer's :attr:`~BeamformerBase.result` or calling
2632
- its :meth:`~BeamformerBase.synthetic` method.
2605
+ accessing the beamformer's :attr:`result` or calling
2606
+ its :meth:`synthetic` method.
2633
2607
 
2634
2608
  Parameters
2635
2609
  ----------
2636
- ac : array of floats
2637
- This array of dimension ([number of frequencies]x[number of gridpoints])
2638
- is used as call-by-reference parameter and contains the calculated
2639
- value after calling this method.
2640
- fr : array of booleans
2641
- The entries of this [number of frequencies]-sized array are either
2642
- 'True' (if the result for this frequency has already been calculated)
2643
- or 'False' (for the frequencies where the result has yet to be calculated).
2644
- After the calculation at a certain frequency the value will be set
2645
- to 'True'
2610
+ ind : array of int
2611
+ This array contains all frequency indices for which (re)calculation is
2612
+ to be performed
2646
2613
 
2647
2614
  Returns
2648
2615
  -------
2649
- This method only returns values through the *ac* and *fr* parameters
2616
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
2650
2617
 
2651
2618
  """
2652
- f = self.freq_data.fftfreq()
2619
+ f = self._f
2620
+ normfactor = self.sig_loss_norm()
2653
2621
  numchannels = self.freq_data.numchannels
2654
2622
  # eigenvalue number list in standard form from largest to smallest
2655
2623
  eva_list = unique(self.eva_list % self.steer.mics.num_mics)[::-1]
@@ -2663,7 +2631,6 @@ class BeamformerGridlessOrth(BeamformerAdaptiveGrid):
2663
2631
  'n': 256,
2664
2632
  'iters': 1,
2665
2633
  'sampling_method': 'sobol',
2666
- 'options': {'local_iter': 1},
2667
2634
  'minimizer_kwargs': {'method': 'Nelder-Mead'},
2668
2635
  }
2669
2636
  shgo_opts.update(self.shgo)
@@ -2675,36 +2642,39 @@ class BeamformerGridlessOrth(BeamformerAdaptiveGrid):
2675
2642
  self.steer.env.roi = array(roi).T
2676
2643
  bmin = array(tuple(map(min, self.bounds)))
2677
2644
  bmax = array(tuple(map(max, self.bounds)))
2678
- for i in self.freq_data.indices:
2679
- if not fr[i]:
2680
- eva = array(self.freq_data.eva[i], dtype='float64')
2681
- eve = array(self.freq_data.eve[i], dtype='complex128')
2682
- k = 2 * pi * f[i] / env.c
2683
- for j, n in enumerate(eva_list):
2684
- # print(f[i],n)
2685
-
2686
- def func(xy):
2687
- # function to minimize globally
2688
- xy = clip(xy, bmin, bmax)
2689
- r0 = env._r(xy[:, newaxis])
2690
- rm = env._r(xy[:, newaxis], mpos)
2691
- return -beamformerFreq(steer_type, self.r_diag, 1.0, (r0, rm, k), (ones(1), eve[:, n : n + 1]))[
2692
- 0
2693
- ][0] # noqa: B023
2694
-
2695
- # simplical global homotopy optimizer
2696
- oR = shgo(func, self.bounds, **shgo_opts)
2697
- # index in grid
2698
- ind = i * self.n + j
2699
- # store result for position
2700
- self._gpos[:, ind] = oR['x']
2701
- # store result for level
2702
- ac[i, ind] = eva[n] / numchannels
2703
- # print(oR['x'],eva[n]/numchannels,oR)
2704
- fr[i] = 1
2705
-
2706
-
2707
- def L_p(x):
2645
+ for i in ind:
2646
+ eva = array(self.freq_data.eva[i], dtype='float64')
2647
+ eve = array(self.freq_data.eve[i], dtype='complex128')
2648
+ k = 2 * pi * f[i] / env.c
2649
+ for j, n in enumerate(eva_list):
2650
+ # print(f[i],n)
2651
+
2652
+ def func(xy):
2653
+ # function to minimize globally
2654
+ xy = clip(xy, bmin, bmax)
2655
+ r0 = env._r(xy[:, newaxis])
2656
+ rm = env._r(xy[:, newaxis], mpos)
2657
+ return -beamformerFreq(
2658
+ steer_type,
2659
+ self.r_diag,
2660
+ normfactor,
2661
+ (r0, rm, k),
2662
+ (ones(1), eve[:, n : n + 1]),
2663
+ )[0][0] # noqa: B023
2664
+
2665
+ # simplical global homotopy optimizer
2666
+ oR = shgo(func, self.bounds, **shgo_opts)
2667
+ # index in grid
2668
+ i1 = i * self.n + j
2669
+ # store result for position
2670
+ self._gpos[:, i1] = oR['x']
2671
+ # store result for level
2672
+ self._ac[i, i1] = eva[n] / numchannels
2673
+ # print(oR['x'],eva[n]/numchannels,oR)
2674
+ self._fr[i] = 1
2675
+
2676
+
2677
+ def L_p(x): # noqa: N802
2708
2678
  r"""Calculates the sound pressure level from the squared sound pressure.
2709
2679
 
2710
2680
  :math:`L_p = 10 \lg ( x / 4\cdot 10^{-10})`