acoular 24.5__py3-none-any.whl → 24.7__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,7 +55,9 @@ 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
63
  linalg,
@@ -89,7 +91,6 @@ from traits.api import (
89
91
  Any,
90
92
  Bool,
91
93
  CArray,
92
- Delegate,
93
94
  Dict,
94
95
  Enum,
95
96
  Float,
@@ -121,6 +122,8 @@ sklearn_ndict = {}
121
122
  if parse(sklearn.__version__) < parse('1.4'):
122
123
  sklearn_ndict['normalize'] = False
123
124
 
125
+ BEAMFORMER_BASE_DIGEST_DEPENDENCIES = ['freq_data.digest', 'r_diag', 'r_diag_norm', 'precision', '_steer_obj.digest']
126
+
124
127
 
125
128
  class SteeringVector(HasPrivateTraits):
126
129
  """Basic class for implementing steering vectors with monopole source transfer models."""
@@ -139,14 +142,6 @@ class SteeringVector(HasPrivateTraits):
139
142
  #: Defaults to standard :class:`~acoular.environments.Environment` object.
140
143
  env = Instance(Environment(), Environment)
141
144
 
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
145
  # Sound travel distances from microphone array center to grid
151
146
  # points or reference position (readonly). Feature may change.
152
147
  r0 = Property(desc='array center to grid distances')
@@ -169,8 +164,8 @@ class SteeringVector(HasPrivateTraits):
169
164
  if isscalar(ref):
170
165
  try:
171
166
  self._ref = absolute(float(ref))
172
- except:
173
- raise TraitError(args=self, name='ref', info='Float or CArray(3,)', value=ref)
167
+ except ValueError as ve:
168
+ raise TraitError(args=self, name='ref', info='Float or CArray(3,)', value=ref) from ve
174
169
  elif len(ref) == 3:
175
170
  self._ref = array(ref, dtype=float)
176
171
  else:
@@ -263,6 +258,34 @@ class SteeringVector(HasPrivateTraits):
263
258
  return func(self.transfer(f, ind))
264
259
 
265
260
 
261
+ class LazyBfResult:
262
+ """Manages lazy per-frequency evaluation."""
263
+
264
+ # Internal helper class which works together with BeamformerBase to provide
265
+ # calculation on demand; provides an 'intelligent' [] operator. This is
266
+ # implemented as an extra class instead of as a method of BeamformerBase to
267
+ # properly control the BeamformerBase.result attribute. Might be migrated to
268
+ # be a method of BeamformerBase in the future.
269
+
270
+ def __init__(self, bf):
271
+ self.bf = bf
272
+
273
+ def __getitem__(self, key):
274
+ # 'intelligent' [] operator checks if results are available and triggers calculation
275
+ sl = index_exp[key][0]
276
+ if isinstance(sl, (int, integer)):
277
+ sl = slice(sl, sl + 1)
278
+ # indices which are missing
279
+ missingind = arange(*sl.indices(self.bf._numfreq))[self.bf._fr[sl] == 0]
280
+ # calc if needed
281
+ if missingind.size:
282
+ self.bf._calc(missingind)
283
+ if self.bf.h5f:
284
+ self.bf.h5f.flush()
285
+
286
+ return self.bf._ac.__getitem__(key)
287
+
288
+
266
289
  class BeamformerBase(HasPrivateTraits):
267
290
  """Beamforming using the basic delay-and-sum algorithm in the frequency domain."""
268
291
 
@@ -387,25 +410,17 @@ class BeamformerBase(HasPrivateTraits):
387
410
 
388
411
  #: The beamforming result as squared sound pressure values
389
412
  #: at all grid point locations (readonly).
390
- #: Returns a (number of frequencies, number of gridpoints) array of floats.
413
+ #: Returns a (number of frequencies, number of gridpoints) array-like
414
+ #: of floats. Values can only be accessed via the index operator [].
391
415
  result = Property(desc='beamforming result')
392
416
 
393
417
  # 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
- )
418
+ digest = Property(depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES)
400
419
 
401
420
  @cached_property
402
421
  def _get_digest(self):
403
422
  return digest(self)
404
423
 
405
- @cached_property
406
- def _get_ext_digest(self):
407
- return digest(self, 'ext_digest')
408
-
409
424
  def _get_filecache(self):
410
425
  """Function collects cached results from file depending on
411
426
  global/local caching behaviour. Returns (None, None) if no cachefile/data
@@ -438,6 +453,13 @@ class BeamformerBase(HasPrivateTraits):
438
453
  if isinstance(self, BeamformerAdaptiveGrid):
439
454
  self.h5f.create_compressible_array('gpos', (3, self.size), 'float64', group)
440
455
  self.h5f.create_compressible_array('result', (numfreq, self.size), self.precision, group)
456
+ elif isinstance(self, BeamformerSODIX):
457
+ self.h5f.create_compressible_array(
458
+ 'result',
459
+ (numfreq, self.steer.grid.size * self.steer.mics.num_mics),
460
+ self.precision,
461
+ group,
462
+ )
441
463
  else:
442
464
  self.h5f.create_compressible_array('result', (numfreq, self.steer.grid.size), self.precision, group)
443
465
 
@@ -454,58 +476,51 @@ class BeamformerBase(HasPrivateTraits):
454
476
  if numchannels != self.steer.mics.num_mics or numchannels == 0:
455
477
  raise ValueError('%i channels do not fit %i mics' % (numchannels, self.steer.mics.num_mics))
456
478
 
457
- @property_depends_on('ext_digest')
479
+ @property_depends_on('digest')
458
480
  def _get_result(self):
459
481
  """Implements the :attr:`result` getter routine.
460
482
  The beamforming result is either loaded or calculated.
461
483
  """
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.")
484
+ # store locally for performance
485
+ self._f = self.freq_data.fftfreq()
486
+ self._numfreq = self._f.shape[0]
487
+ self._assert_equal_channels()
488
+ ac, fr = (None, None)
489
+ if not ( # if result caching is active
490
+ config.global_caching == 'none' or (config.global_caching == 'individual' and not self.cached)
491
+ ):
492
+ (ac, fr, gpos) = self._get_filecache() # can also be (None, None, None)
493
+ if gpos: # we have an adaptive grid
494
+ self._gpos = gpos
495
+ if ac and fr: # cached data is available
496
+ if config.global_caching == 'readonly':
497
+ (ac, fr) = (ac[:], fr[:]) # so never write back to disk
498
+ else:
499
+ # no caching or not activated, init numpy arrays
500
+ if isinstance(self, BeamformerAdaptiveGrid):
501
+ self._gpos = zeros((3, self.size), dtype=self.precision)
502
+ ac = zeros((self._numfreq, self.size), dtype=self.precision)
503
+ elif isinstance(self, BeamformerSODIX):
504
+ ac = zeros((self._numfreq, self.steer.grid.size * self.steer.mics.num_mics), dtype=self.precision)
486
505
  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
506
+ ac = zeros((self._numfreq, self.steer.grid.size), dtype=self.precision)
507
+ fr = zeros(self._numfreq, dtype='int8')
508
+ self._ac = ac
509
+ self._fr = fr
510
+ return LazyBfResult(self)
496
511
 
497
512
  def sig_loss_norm(self):
498
513
  """If the diagonal of the CSM is removed one has to handle the loss
499
514
  of signal energy --> Done via a normalization factor.
500
515
  """
501
516
  if not self.r_diag: # Full CSM --> no normalization needed
502
- normFactor = 1.0
517
+ normfactor = 1.0
503
518
  elif self.r_diag_norm == 0.0: # Removed diag: standard normalization factor
504
519
  nMics = float(self.freq_data.numchannels)
505
- normFactor = nMics / (nMics - 1)
520
+ normfactor = nMics / (nMics - 1)
506
521
  elif self.r_diag_norm != 0.0: # Removed diag: user defined normalization factor
507
- normFactor = self.r_diag_norm
508
- return normFactor
522
+ normfactor = self.r_diag_norm
523
+ return normfactor
509
524
 
510
525
  def _beamformer_params(self):
511
526
  """Manages the parameters for calling of the core beamformer functionality.
@@ -528,9 +543,8 @@ class BeamformerBase(HasPrivateTraits):
528
543
  param_steer_func = self.steer.steer_vector
529
544
  return param_type, param_steer_func
530
545
 
531
- def calc(self, ac, fr):
532
- """Calculates the delay-and-sum beamforming result for the frequencies
533
- defined by :attr:`freq_data`.
546
+ def _calc(self, ind):
547
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
534
548
 
535
549
  This is an internal helper function that is automatically called when
536
550
  accessing the beamformer's :attr:`result` or calling
@@ -538,39 +552,33 @@ class BeamformerBase(HasPrivateTraits):
538
552
 
539
553
  Parameters
540
554
  ----------
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'
555
+ ind : array of int
556
+ This array contains all frequency indices for which (re)calculation is
557
+ to be performed
551
558
 
552
559
  Returns
553
560
  -------
554
- This method only returns values through the *ac* and *fr* parameters
561
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
555
562
 
556
563
  """
557
- f = self.freq_data.fftfreq() # [inds]
564
+ f = self._f
565
+ normfactor = self.sig_loss_norm()
558
566
  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
567
+ for i in ind:
568
+ # print(f'compute{i}')
569
+ csm = array(self.freq_data.csm[i], dtype='complex128')
570
+ beamformerOutput = beamformerFreq(
571
+ param_steer_type,
572
+ self.r_diag,
573
+ normfactor,
574
+ steer_vector(f[i]),
575
+ csm,
576
+ )[0]
577
+ if self.r_diag: # set (unphysical) negative output values to 0
578
+ indNegSign = sign(beamformerOutput) < 0
579
+ beamformerOutput[indNegSign] = 0.0
580
+ self._ac[i] = beamformerOutput
581
+ self._fr[i] = 1
574
582
 
575
583
  def synthetic(self, f, num=0):
576
584
  """Evaluates the beamforming result for an arbitrary frequency band.
@@ -608,8 +616,6 @@ class BeamformerBase(HasPrivateTraits):
608
616
  if len(freq) == 0:
609
617
  return None
610
618
 
611
- indices = self.freq_data.indices
612
-
613
619
  if num == 0:
614
620
  # single frequency line
615
621
  ind = searchsorted(freq, f)
@@ -629,14 +635,6 @@ class BeamformerBase(HasPrivateTraits):
629
635
  Warning,
630
636
  stacklevel=2,
631
637
  )
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
638
  h = res[ind]
641
639
  else:
642
640
  # fractional octave band
@@ -659,46 +657,56 @@ class BeamformerBase(HasPrivateTraits):
659
657
  h = zeros_like(res[0])
660
658
  else:
661
659
  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
660
  if isinstance(self, BeamformerAdaptiveGrid):
671
661
  return h
662
+ if isinstance(self, BeamformerSODIX):
663
+ return h.reshape((self.steer.grid.size, self.steer.mics.num_mics))
672
664
  return h.reshape(self.steer.grid.shape)
673
665
 
674
- def integrate(self, sector):
666
+ def integrate(self, sector, frange=None, num=0):
675
667
  """Integrates result map over a given sector.
676
668
 
677
669
  Parameters
678
670
  ----------
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])*.
671
+ sector: array of floats or :class:`~acoular.grids.Sector`
672
+ either an array, tuple or list with arguments for the 'indices'
673
+ method of a :class:`~acoular.grids.Grid`-derived class
674
+ (e.g. :meth:`RectGrid.indices<acoular.grids.RectGrid.indices>`
675
+ or :meth:`RectGrid3D.indices<acoular.grids.RectGrid3D.indices>`).
676
+ Possible sectors would be *array([xmin, ymin, xmax, ymax])*
677
+ or *array([x, y, radius])* or an instance of a
678
+ :class:`~acoular.grids.Sector`-derived class
679
+
680
+ frange: tuple or None
681
+ a tuple of (fmin,fmax) frequencies to include in the result if *num*==0,
682
+ or band center frequency/frequencies for which to return the results
683
+ if *num*>0; if None, then the frequency range is determined from
684
+ the settings of the :attr:`PowerSpectra.ind_low` and
685
+ :attr:`PowerSpectra.ind_high` of :attr:`freq_data`
686
+
687
+ num : integer
688
+ Controls the width of the frequency bands considered; defaults to
689
+ 0 (single frequency line). Only considered if *frange* is not None.
690
+
691
+ === =====================
692
+ num frequency band width
693
+ === =====================
694
+ 0 single frequency line
695
+ 1 octave band
696
+ 3 third-octave band
697
+ n 1/n-octave band
698
+ === =====================
699
+
686
700
 
687
701
  Returns
688
702
  -------
689
- array of floats
690
- The spectrum (all calculated frequency bands) for the integrated sector.
691
-
703
+ res or (f, res): array of floats or tuple(array of floats, array of floats)
704
+ If *frange*==None or *num*>0, the spectrum (all calculated frequency bands)
705
+ for the integrated sector is returned as *res*. The dimension of this array is the
706
+ number of frequencies given by :attr:`freq_data` and entries not computed are zero.
707
+ If *frange*!=None and *num*==0, then (f, res) is returned where *f* are the (band)
708
+ frequencies and the dimension of both arrays is determined from *frange*
692
709
  """
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
710
  if isinstance(sector, Sector):
703
711
  ind = self.steer.grid.subdomain(sector)
704
712
  elif hasattr(self.steer.grid, 'indices'):
@@ -713,10 +721,40 @@ class BeamformerBase(HasPrivateTraits):
713
721
  msg,
714
722
  )
715
723
  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()
724
+ if num == 0 or frange is None:
725
+ if frange is None:
726
+ ind_low = self.freq_data.ind_low
727
+ ind_high = self.freq_data.ind_high
728
+ if ind_low is None:
729
+ ind_low = 0
730
+ if ind_low < 0:
731
+ ind_low += self._numfreq
732
+ if ind_high is None:
733
+ ind_high = self._numfreq
734
+ if ind_high < 0:
735
+ ind_high += self._numfreq
736
+ irange = (ind_low, ind_high)
737
+ num = 0
738
+ elif len(frange) == 2:
739
+ irange = (searchsorted(self._f, frange[0]), searchsorted(self._f, frange[1]))
740
+ else:
741
+ msg = 'Only a tuple of length 2 is allowed for frange if num==0'
742
+ raise TypeError(
743
+ msg,
744
+ )
745
+ h = zeros(self._numfreq, dtype=float)
746
+ sl = slice(*irange)
747
+ r = self.result[sl]
748
+ for i in range(*irange):
749
+ # we do this per frequency because r might not have fancy indexing
750
+ h[i] = r[i - sl.start].reshape(gshape)[ind].sum()
751
+ if frange is None:
752
+ return h
753
+ return self._f[sl], h[sl]
754
+
755
+ h = zeros(len(frange), dtype=float)
756
+ for i, f in enumerate(frange):
757
+ h[i] = self.synthetic(f, num).reshape(gshape)[ind].sum()
720
758
  return h
721
759
 
722
760
 
@@ -726,138 +764,130 @@ class BeamformerFunctional(BeamformerBase):
726
764
  #: Functional exponent, defaults to 1 (= Classic Beamforming).
727
765
  gamma = Float(1, desc='functional exponent')
728
766
 
729
- # internal identifier
730
- digest = Property(depends_on=['freq_data.digest', '_steer_obj.digest', 'r_diag', 'gamma'])
731
-
732
767
  #: Functional Beamforming is only well defined for full CSM
733
768
  r_diag = Enum(False, desc='False, as Functional Beamformer is only well defined for the full CSM')
734
769
 
770
+ #: Normalization factor in case of CSM diagonal removal. Defaults to 1.0 since Functional Beamforming is only well defined for full CSM.
771
+ r_diag_norm = Enum(
772
+ 1.0,
773
+ desc='No normalization needed. Functional Beamforming is only well defined for full CSM.',
774
+ )
775
+
776
+ # internal identifier
777
+ digest = Property(depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['gamma'])
778
+
735
779
  @cached_property
736
780
  def _get_digest(self):
737
781
  return digest(self)
738
782
 
739
- def calc(self, ac, fr):
740
- """Calculates the Functional Beamformer result for the frequencies defined by :attr:`freq_data`.
783
+ def _calc(self, ind):
784
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
741
785
 
742
786
  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.
787
+ accessing the beamformer's :attr:`result` or calling
788
+ its :meth:`synthetic` method.
745
789
 
746
790
  Parameters
747
791
  ----------
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'
792
+ ind : array of int
793
+ This array contains all frequency indices for which (re)calculation is
794
+ to be performed
758
795
 
759
796
  Returns
760
797
  -------
761
- This method only returns values through the *ac* and *fr* parameters
798
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
762
799
 
763
800
  """
764
- f = self.freq_data.fftfreq()
765
- normFactor = self.sig_loss_norm()
801
+ f = self._f
802
+ normfactor = self.sig_loss_norm()
766
803
  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
804
+ for i in ind:
805
+ if self.r_diag:
806
+ # This case is not used at the moment (see Trait r_diag)
807
+ # It would need some testing as structural changes were not tested...
808
+ # ==============================================================================
809
+ # One cannot use spectral decomposition when diagonal of csm is removed,
810
+ # as the resulting modified eigenvectors are not orthogonal to each other anymore.
811
+ # Therefor potentiating cannot be applied only to the eigenvalues.
812
+ # --> To avoid this the root of the csm (removed diag) is calculated directly.
813
+ # WATCH OUT: This doesn't really produce good results.
814
+ # ==============================================================================
815
+ csm = self.freq_data.csm[i]
816
+ fill_diagonal(csm, 0)
817
+ csmRoot = fractional_matrix_power(csm, 1.0 / self.gamma)
818
+ beamformerOutput, steerNorm = beamformerFreq(
819
+ param_steer_type,
820
+ self.r_diag,
821
+ normfactor,
822
+ steer_vector(f[i]),
823
+ csmRoot,
824
+ )
825
+ beamformerOutput /= steerNorm # take normalized steering vec
790
826
 
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
827
+ # set (unphysical) negative output values to 0
828
+ indNegSign = sign(beamformerOutput) < 0
829
+ beamformerOutput[indNegSign] = 0.0
830
+ else:
831
+ eva = array(self.freq_data.eva[i], dtype='float64') ** (1.0 / self.gamma)
832
+ eve = array(self.freq_data.eve[i], dtype='complex128')
833
+ beamformerOutput, steerNorm = beamformerFreq(
834
+ param_steer_type,
835
+ self.r_diag,
836
+ 1.0,
837
+ steer_vector(f[i]),
838
+ (eva, eve),
839
+ )
840
+ beamformerOutput /= steerNorm # take normalized steering vec
841
+ self._ac[i] = (
842
+ (beamformerOutput**self.gamma) * steerNorm * normfactor
843
+ ) # the normalization must be done outside the beamformer
844
+ self._fr[i] = 1
809
845
 
810
846
 
811
847
  class BeamformerCapon(BeamformerBase):
812
- """Beamforming using the Capon (Mininimum Variance) algorithm,
813
- see :ref:`Capon, 1969<Capon1969>`.
814
- """
848
+ """Beamforming using the Capon (Mininimum Variance) algorithm, see :ref:`Capon, 1969<Capon1969>`."""
815
849
 
816
850
  # Boolean flag, if 'True', the main diagonal is removed before beamforming;
817
851
  # for Capon beamforming r_diag is set to 'False'.
818
852
  r_diag = Enum(False, desc='removal of diagonal')
819
853
 
820
- def calc(self, ac, fr):
821
- """Calculates the Capon result for the frequencies defined by :attr:`freq_data`.
854
+ #: Normalization factor in case of CSM diagonal removal. Defaults to 1.0 since Beamformer Capon is only well defined for full CSM.
855
+ r_diag_norm = Enum(
856
+ 1.0,
857
+ desc='No normalization. BeamformerCapon is only well defined for full CSM.',
858
+ )
859
+
860
+ def _calc(self, ind):
861
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
822
862
 
823
863
  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.
864
+ accessing the beamformer's :attr:`result` or calling
865
+ its :meth:`synthetic` method.
826
866
 
827
867
  Parameters
828
868
  ----------
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'
869
+ ind : array of int
870
+ This array contains all frequency indices for which (re)calculation is
871
+ to be performed
839
872
 
840
873
  Returns
841
874
  -------
842
- This method only returns values through the *ac* and *fr* parameters
875
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
843
876
 
844
877
  """
845
- f = self.freq_data.fftfreq()
878
+ f = self._f
846
879
  nMics = self.freq_data.numchannels
847
- normFactor = self.sig_loss_norm() * nMics**2
880
+ normfactor = self.sig_loss_norm() * nMics**2
848
881
  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
882
+ for i in ind:
883
+ csm = array(linalg.inv(array(self.freq_data.csm[i], dtype='complex128')), order='C')
884
+ beamformerOutput = beamformerFreq(param_steer_type, self.r_diag, normfactor, steer_vector(f[i]), csm)[0]
885
+ self._ac[i] = 1.0 / beamformerOutput
886
+ self._fr[i] = 1
855
887
 
856
888
 
857
889
  class BeamformerEig(BeamformerBase):
858
- """Beamforming using eigenvalue and eigenvector techniques,
859
- see :ref:`Sarradj et al., 2005<Sarradj2005>`.
860
- """
890
+ """Beamforming using eigenvalue and eigenvector techniques, see :ref:`Sarradj et al., 2005<Sarradj2005>`."""
861
891
 
862
892
  #: Number of component to calculate:
863
893
  #: 0 (smallest) ... :attr:`~acoular.tprocess.SamplesGenerator.numchannels`-1;
@@ -868,7 +898,7 @@ class BeamformerEig(BeamformerBase):
868
898
  na = Property(desc='No. of eigenvalue')
869
899
 
870
900
  # internal identifier
871
- digest = Property(depends_on=['freq_data.digest', '_steer_obj.digest', 'r_diag', 'n'])
901
+ digest = Property(depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['n'])
872
902
 
873
903
  @cached_property
874
904
  def _get_digest(self):
@@ -882,51 +912,43 @@ class BeamformerEig(BeamformerBase):
882
912
  na = max(nm + na, 0)
883
913
  return min(nm - 1, na)
884
914
 
885
- def calc(self, ac, fr):
915
+ def _calc(self, ind):
886
916
  """Calculates the result for the frequencies defined by :attr:`freq_data`.
887
917
 
888
918
  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.
919
+ accessing the beamformer's :attr:`result` or calling
920
+ its :meth:`synthetic` method.
891
921
 
892
922
  Parameters
893
923
  ----------
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'
924
+ ind : array of int
925
+ This array contains all frequency indices for which (re)calculation is
926
+ to be performed
904
927
 
905
928
  Returns
906
929
  -------
907
- This method only returns values through the *ac* and *fr* parameters
930
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
908
931
 
909
932
  """
910
- f = self.freq_data.fftfreq()
933
+ f = self._f
911
934
  na = int(self.na) # eigenvalue taken into account
912
- normFactor = self.sig_loss_norm()
935
+ normfactor = self.sig_loss_norm()
913
936
  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
937
+ for i in ind:
938
+ eva = array(self.freq_data.eva[i], dtype='float64')
939
+ eve = array(self.freq_data.eve[i], dtype='complex128')
940
+ beamformerOutput = beamformerFreq(
941
+ param_steer_type,
942
+ self.r_diag,
943
+ normfactor,
944
+ steer_vector(f[i]),
945
+ (eva[na : na + 1], eve[:, na : na + 1]),
946
+ )[0]
947
+ if self.r_diag: # set (unphysical) negative output values to 0
948
+ indNegSign = sign(beamformerOutput) < 0
949
+ beamformerOutput[indNegSign] = 0
950
+ self._ac[i] = beamformerOutput
951
+ self._fr[i] = 1
930
952
 
931
953
 
932
954
  class BeamformerMusic(BeamformerEig):
@@ -936,53 +958,51 @@ class BeamformerMusic(BeamformerEig):
936
958
  # for MUSIC beamforming r_diag is set to 'False'.
937
959
  r_diag = Enum(False, desc='removal of diagonal')
938
960
 
961
+ #: Normalization factor in case of CSM diagonal removal. Defaults to 1.0 since BeamformerMusic is only well defined for full CSM.
962
+ r_diag_norm = Enum(
963
+ 1.0,
964
+ desc='No normalization. BeamformerMusic is only well defined for full CSM.',
965
+ )
966
+
939
967
  # assumed number of sources, should be set to a value not too small
940
968
  # defaults to 1
941
969
  n = Int(1, desc='assumed number of sources')
942
970
 
943
- def calc(self, ac, fr):
944
- """Calculates the MUSIC result for the frequencies defined by :attr:`freq_data`.
971
+ def _calc(self, ind):
972
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
945
973
 
946
974
  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.
975
+ accessing the beamformer's :attr:`result` or calling
976
+ its :meth:`synthetic` method.
949
977
 
950
978
  Parameters
951
979
  ----------
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'
980
+ ind : array of int
981
+ This array contains all frequency indices for which (re)calculation is
982
+ to be performed
962
983
 
963
984
  Returns
964
985
  -------
965
- This method only returns values through the *ac* and *fr* parameters
986
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
966
987
 
967
988
  """
968
- f = self.freq_data.fftfreq()
989
+ f = self._f
969
990
  nMics = self.freq_data.numchannels
970
991
  n = int(self.steer.mics.num_mics - self.na)
971
- normFactor = self.sig_loss_norm() * nMics**2
992
+ normfactor = self.sig_loss_norm() * nMics**2
972
993
  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
994
+ for i in ind:
995
+ eva = array(self.freq_data.eva[i], dtype='float64')
996
+ eve = array(self.freq_data.eve[i], dtype='complex128')
997
+ beamformerOutput = beamformerFreq(
998
+ param_steer_type,
999
+ self.r_diag,
1000
+ normfactor,
1001
+ steer_vector(f[i]),
1002
+ (eva[:n], eve[:, :n]),
1003
+ )[0]
1004
+ self._ac[i] = 4e-10 * beamformerOutput.min() / beamformerOutput
1005
+ self._fr[i] = 1
986
1006
 
987
1007
 
988
1008
  class PointSpreadFunction(HasPrivateTraits):
@@ -1206,19 +1226,19 @@ class PointSpreadFunction(HasPrivateTraits):
1206
1226
 
1207
1227
  if self.calcmode == 'single': # calculate selected psfs one-by-one
1208
1228
  for ind in g_ind_calc:
1209
- ac[:, ind] = self._psfCall([ind])[:, 0]
1229
+ ac[:, ind] = self._psf_call([ind])[:, 0]
1210
1230
  gp[ind] = 1
1211
1231
  elif self.calcmode == 'full': # calculate all psfs in one go
1212
1232
  gp[:] = 1
1213
- ac[:] = self._psfCall(arange(self.steer.grid.size))
1233
+ ac[:] = self._psf_call(arange(self.steer.grid.size))
1214
1234
  else: # 'block' # calculate selected psfs in one go
1215
- hh = self._psfCall(g_ind_calc)
1235
+ hh = self._psf_call(g_ind_calc)
1216
1236
  for indh, ind in enumerate(g_ind_calc):
1217
1237
  gp[ind] = 1
1218
1238
  ac[:, ind] = hh[:, indh]
1219
1239
  indh += 1
1220
1240
 
1221
- def _psfCall(self, ind):
1241
+ def _psf_call(self, ind):
1222
1242
  """Manages the calling of the core psf functionality.
1223
1243
 
1224
1244
  Parameters
@@ -1249,28 +1269,13 @@ class PointSpreadFunction(HasPrivateTraits):
1249
1269
 
1250
1270
 
1251
1271
  class BeamformerDamas(BeamformerBase):
1252
- """DAMAS deconvolution, see :ref:`Brooks and Humphreys, 2006<BrooksHumphreys2006>`.
1253
- Needs a-priori delay-and-sum beamforming (:class:`BeamformerBase`).
1254
- """
1272
+ """DAMAS deconvolution, see :ref:`Brooks and Humphreys, 2006<BrooksHumphreys2006>`."""
1255
1273
 
1256
- #: :class:`BeamformerBase` object that provides data for deconvolution.
1274
+ #: (only for backward compatibility) :class:`BeamformerBase` object
1275
+ #: if set, provides :attr:`freq_data`, :attr:`steer`, :attr:`r_diag`
1276
+ #: if not set, these have to be set explicitly
1257
1277
  beamformer = Trait(BeamformerBase)
1258
1278
 
1259
- #: :class:`~acoular.spectra.PowerSpectra` object that provides the cross spectral matrix;
1260
- #: is set automatically.
1261
- freq_data = Delegate('beamformer')
1262
-
1263
- #: Boolean flag, if 'True' (default), the main diagonal is removed before beamforming;
1264
- #: is set automatically.
1265
- r_diag = Delegate('beamformer')
1266
-
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')
1270
-
1271
- #: Floating point precision of result, is set automatically.
1272
- precision = Delegate('beamformer')
1273
-
1274
1279
  #: The floating-number-precision of the PSFs. Default is 64 bit.
1275
1280
  psf_precision = Trait('float64', 'float32', desc='precision of PSF')
1276
1281
 
@@ -1286,59 +1291,59 @@ class BeamformerDamas(BeamformerBase):
1286
1291
 
1287
1292
  # internal identifier
1288
1293
  digest = Property(
1289
- depends_on=['beamformer.digest', 'n_iter', 'damp', 'psf_precision'],
1290
- )
1291
-
1292
- # internal identifier
1293
- ext_digest = Property(
1294
- depends_on=['digest', 'beamformer.ext_digest'],
1294
+ depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['n_iter', 'damp', 'psf_precision'],
1295
1295
  )
1296
1296
 
1297
1297
  @cached_property
1298
1298
  def _get_digest(self):
1299
1299
  return digest(self)
1300
1300
 
1301
- @cached_property
1302
- def _get_ext_digest(self):
1303
- return digest(self, 'ext_digest')
1301
+ @on_trait_change('beamformer.digest')
1302
+ def delegate_beamformer_traits(self):
1303
+ self.freq_data = self.beamformer.freq_data
1304
+ self.r_diag = self.beamformer.r_diag
1305
+ self.steer = self.beamformer.steer
1304
1306
 
1305
- def calc(self, ac, fr):
1306
- """Calculates the DAMAS result for the frequencies defined by :attr:`freq_data`.
1307
+ def _calc(self, ind):
1308
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1307
1309
 
1308
1310
  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.
1311
+ accessing the beamformer's :attr:`result` or calling
1312
+ its :meth:`synthetic` method.
1312
1313
 
1313
1314
  Parameters
1314
1315
  ----------
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'
1316
+ ind : array of int
1317
+ This array contains all frequency indices for which (re)calculation is
1318
+ to be performed
1325
1319
 
1326
1320
  Returns
1327
1321
  -------
1328
- This method only returns values through the *ac* and *fr* parameters
1322
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1329
1323
 
1330
1324
  """
1331
- f = self.freq_data.fftfreq()
1325
+ f = self._f
1326
+ normfactor = self.sig_loss_norm()
1332
1327
  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
1328
+ param_steer_type, steer_vector = self._beamformer_params()
1329
+ for i in ind:
1330
+ csm = array(self.freq_data.csm[i], dtype='complex128')
1331
+ y = beamformerFreq(
1332
+ param_steer_type,
1333
+ self.r_diag,
1334
+ normfactor,
1335
+ steer_vector(f[i]),
1336
+ csm,
1337
+ )[0]
1338
+ if self.r_diag: # set (unphysical) negative output values to 0
1339
+ indNegSign = sign(y) < 0
1340
+ y[indNegSign] = 0.0
1341
+ x = y.copy()
1342
+ p.freq = f[i]
1343
+ psf = p.psf[:]
1344
+ damasSolverGaussSeidel(psf, y, self.n_iter, self.damp, x)
1345
+ self._ac[i] = x
1346
+ self._fr[i] = 1
1342
1347
 
1343
1348
 
1344
1349
  class BeamformerDamasPlus(BeamformerDamas):
@@ -1374,94 +1379,89 @@ class BeamformerDamasPlus(BeamformerDamas):
1374
1379
 
1375
1380
  # internal identifier
1376
1381
  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'],
1382
+ depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['alpha', 'method', 'max_iter', 'unit_mult'],
1383
1383
  )
1384
1384
 
1385
1385
  @cached_property
1386
1386
  def _get_digest(self):
1387
1387
  return digest(self)
1388
1388
 
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`.
1389
+ def _calc(self, ind):
1390
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1395
1391
 
1396
1392
  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.
1393
+ accessing the beamformer's :attr:`result` or calling
1394
+ its :meth:`synthetic` method.
1399
1395
 
1400
1396
  Parameters
1401
1397
  ----------
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'
1398
+ ind : array of int
1399
+ This array contains all frequency indices for which (re)calculation is
1400
+ to be performed
1412
1401
 
1413
1402
  Returns
1414
1403
  -------
1415
- This method only returns values through the *ac* and *fr* parameters
1404
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1416
1405
 
1417
1406
  """
1418
- f = self.freq_data.fftfreq()
1407
+ f = self._f
1419
1408
  p = PointSpreadFunction(steer=self.steer, calcmode=self.calcmode, precision=self.psf_precision)
1420
1409
  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
1410
+ normfactor = self.sig_loss_norm()
1411
+ param_steer_type, steer_vector = self._beamformer_params()
1412
+ for i in ind:
1413
+ csm = array(self.freq_data.csm[i], dtype='complex128')
1414
+ y = beamformerFreq(
1415
+ param_steer_type,
1416
+ self.r_diag,
1417
+ normfactor,
1418
+ steer_vector(f[i]),
1419
+ csm,
1420
+ )[0]
1421
+ if self.r_diag: # set (unphysical) negative output values to 0
1422
+ indNegSign = sign(y) < 0
1423
+ y[indNegSign] = 0.0
1424
+ y *= unit
1425
+ p.freq = f[i]
1426
+ psf = p.psf[:]
1427
+
1428
+ if self.method == 'NNLS':
1429
+ self._ac[i] = nnls(psf, y)[0] / unit
1430
+ elif self.method == 'LP': # linear programming (Dougherty)
1431
+ if self.r_diag:
1432
+ warn(
1433
+ 'Linear programming solver may fail when CSM main '
1434
+ 'diagonal is removed for delay-and-sum beamforming.',
1435
+ Warning,
1436
+ stacklevel=5,
1437
+ )
1438
+ cT = -1 * psf.sum(1) # turn the minimization into a maximization
1439
+ self._ac[i] = linprog(c=cT, A_ub=psf, b_ub=y).x / unit # defaults to simplex method and non-negative x
1440
+ else:
1441
+ if self.method == 'LassoLars':
1442
+ model = LassoLars(
1443
+ alpha=self.alpha * unit,
1444
+ max_iter=self.max_iter,
1445
+ )
1446
+ elif self.method == 'OMPCV':
1447
+ model = OrthogonalMatchingPursuitCV()
1439
1448
  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
1449
+ msg = f'Method {self.method} not implemented.'
1450
+ raise NotImplementedError(msg)
1451
+ model.normalize = False
1452
+ # from sklearn 1.2, normalize=True does not work the same way anymore and the pipeline approach
1453
+ # with StandardScaler does scale in a different way, thus we monkeypatch the code and normalize
1454
+ # ourselves to make results the same over different sklearn versions
1455
+ norms = norm(psf, axis=0)
1456
+ # get rid of annoying sklearn warnings that appear
1457
+ # for sklearn<1.2 despite any settings
1458
+ with warnings.catch_warnings():
1459
+ warnings.simplefilter('ignore', category=FutureWarning)
1460
+ # normalized psf
1461
+ model.fit(psf / norms, y)
1462
+ # recover normalization in the coef's
1463
+ self._ac[i] = model.coef_[:] / norms / unit
1464
+ self._fr[i] = 1
1465
1465
 
1466
1466
 
1467
1467
  class BeamformerOrth(BeamformerBase):
@@ -1476,7 +1476,7 @@ class BeamformerOrth(BeamformerBase):
1476
1476
 
1477
1477
  #: List of components to consider, use this to directly set the eigenvalues
1478
1478
  #: used in the beamformer. Alternatively, set :attr:`n`.
1479
- eva_list = CArray(dtype=int, desc='components')
1479
+ eva_list = CArray(dtype=int, value=array([-1]), desc='components')
1480
1480
 
1481
1481
  #: Number of components to consider, defaults to 1. If set,
1482
1482
  #: :attr:`eva_list` will contain
@@ -1486,17 +1486,13 @@ class BeamformerOrth(BeamformerBase):
1486
1486
 
1487
1487
  # internal identifier
1488
1488
  digest = Property(
1489
- depends_on=['freq_data.digest', '_steer_obj.digest', 'r_diag', 'eva_list'],
1489
+ depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['eva_list'],
1490
1490
  )
1491
1491
 
1492
1492
  @cached_property
1493
1493
  def _get_digest(self):
1494
1494
  return digest(self)
1495
1495
 
1496
- @cached_property
1497
- def _get_ext_digest(self):
1498
- return digest(self, 'ext_digest')
1499
-
1500
1496
  @on_trait_change('beamformer.digest')
1501
1497
  def delegate_beamformer_traits(self):
1502
1498
  self.freq_data = self.beamformer.freq_data
@@ -1508,51 +1504,41 @@ class BeamformerOrth(BeamformerBase):
1508
1504
  """Sets the list of eigenvalues to consider."""
1509
1505
  self.eva_list = arange(-1, -1 - self.n, -1)
1510
1506
 
1511
- def calc(self, ac, fr):
1512
- """Calculates the Orthogonal Beamforming result for the frequencies
1513
- defined by :attr:`freq_data`.
1507
+ def _calc(self, ind):
1508
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1514
1509
 
1515
1510
  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.
1511
+ accessing the beamformer's :attr:`result` or calling
1512
+ its :meth:`synthetic` method.
1518
1513
 
1519
1514
  Parameters
1520
1515
  ----------
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'
1516
+ ind : array of int
1517
+ This array contains all frequency indices for which (re)calculation is
1518
+ to be performed
1531
1519
 
1532
1520
  Returns
1533
1521
  -------
1534
- This method only returns values through the *ac* and *fr* parameters
1522
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1535
1523
 
1536
1524
  """
1537
- # prepare calculation
1538
- f = self.freq_data.fftfreq()
1525
+ f = self._f
1539
1526
  numchannels = self.freq_data.numchannels
1540
- normFactor = self.sig_loss_norm()
1527
+ normfactor = self.sig_loss_norm()
1541
1528
  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
1529
+ for i in ind:
1530
+ eva = array(self.freq_data.eva[i], dtype='float64')
1531
+ eve = array(self.freq_data.eve[i], dtype='complex128')
1532
+ for n in self.eva_list:
1533
+ beamformerOutput = beamformerFreq(
1534
+ param_steer_type,
1535
+ self.r_diag,
1536
+ normfactor,
1537
+ steer_vector(f[i]),
1538
+ (ones(1), eve[:, n].reshape((-1, 1))),
1539
+ )[0]
1540
+ self._ac[i, beamformerOutput.argmax()] += eva[n] / numchannels
1541
+ self._fr[i] = 1
1556
1542
 
1557
1543
 
1558
1544
  class BeamformerCleansc(BeamformerBase):
@@ -1574,106 +1560,83 @@ class BeamformerCleansc(BeamformerBase):
1574
1560
  stopn = Int(3, desc='stop criterion index')
1575
1561
 
1576
1562
  # internal identifier
1577
- digest = Property(depends_on=['freq_data.digest', '_steer_obj.digest', 'r_diag', 'n', 'damp', 'stopn'])
1563
+ digest = Property(depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['n', 'damp', 'stopn'])
1578
1564
 
1579
1565
  @cached_property
1580
1566
  def _get_digest(self):
1581
1567
  return digest(self)
1582
1568
 
1583
- def calc(self, ac, fr):
1584
- """Calculates the CLEAN-SC result for the frequencies defined by :attr:`freq_data`.
1569
+ def _calc(self, ind):
1570
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1585
1571
 
1586
1572
  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.
1573
+ accessing the beamformer's :attr:`result` or calling
1574
+ its :meth:`synthetic` method.
1589
1575
 
1590
1576
  Parameters
1591
1577
  ----------
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'
1578
+ ind : array of int
1579
+ This array contains all frequency indices for which (re)calculation is
1580
+ to be performed
1602
1581
 
1603
1582
  Returns
1604
1583
  -------
1605
- This method only returns values through the *ac* and *fr* parameters
1584
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1606
1585
 
1607
1586
  """
1608
- # prepare calculation
1609
- normFactor = self.sig_loss_norm()
1587
+ f = self._f
1588
+ normfactor = self.sig_loss_norm()
1610
1589
  numchannels = self.freq_data.numchannels
1611
- f = self.freq_data.fftfreq()
1612
1590
  result = zeros((self.steer.grid.size), 'f')
1613
- normFac = self.sig_loss_norm()
1614
1591
  J = numchannels * 2 if not self.n else self.n
1615
1592
  powers = zeros(J, 'd')
1616
1593
 
1617
1594
  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
1595
+ for i in ind:
1596
+ csm = array(self.freq_data.csm[i], dtype='complex128', copy=1)
1597
+ # h = self.steer._beamformerCall(f[i], self.r_diag, normfactor, (csm,))[0]
1598
+ h = beamformerFreq(param_steer_type, self.r_diag, normfactor, steer_vector(f[i]), csm)[0]
1599
+ # CLEANSC Iteration
1600
+ result *= 0.0
1601
+ for j in range(J):
1602
+ xi_max = h.argmax() # index of maximum
1603
+ powers[j] = hmax = h[xi_max] # maximum
1604
+ result[xi_max] += self.damp * hmax
1605
+ if j > self.stopn and hmax > powers[j - self.stopn]:
1606
+ break
1607
+ wmax = self.steer.steer_vector(f[i], xi_max) * sqrt(normfactor)
1608
+ wmax = wmax[0].conj() # as old code worked with conjugated csm..should be updated
1609
+ hh = wmax.copy()
1610
+ D1 = dot(csm.T - diag(diag(csm)), wmax) / hmax
1611
+ ww = wmax.conj() * wmax
1612
+ for _m in range(20):
1613
+ H = hh.conj() * hh
1614
+ hh = (D1 + H * wmax) / sqrt(1 + dot(ww, H))
1615
+ hh = hh[:, newaxis]
1616
+ csm1 = hmax * (hh * hh.conj().T)
1617
+
1618
+ # h1 = self.steer._beamformerCall(f[i], self.r_diag, normfactor, (array((hmax, ))[newaxis, :], hh[newaxis, :].conjugate()))[0]
1619
+ h1 = beamformerFreq(
1620
+ param_steer_type,
1621
+ self.r_diag,
1622
+ normfactor,
1623
+ steer_vector(f[i]),
1624
+ (array((hmax,)), hh.conj()),
1625
+ )[0]
1626
+ h -= self.damp * h1
1627
+ csm -= self.damp * csm1.T # transpose(0,2,1)
1628
+ self._ac[i] = result
1629
+ self._fr[i] = 1
1654
1630
 
1655
1631
 
1656
1632
  class BeamformerClean(BeamformerBase):
1657
- """CLEAN deconvolution, see :ref:`Hoegbom, 1974<Hoegbom1974>`.
1658
- Needs a-priori delay-and-sum beamforming (:class:`BeamformerBase`).
1659
- """
1633
+ """CLEAN deconvolution, see :ref:`Hoegbom, 1974<Hoegbom1974>`."""
1660
1634
 
1661
- # BeamformerBase object that provides data for deconvolution
1635
+ #: (only for backward compatibility) :class:`BeamformerBase` object
1636
+ #: if set, provides :attr:`freq_data`, :attr:`steer`, :attr:`r_diag`
1637
+ #: if not set, these have to be set explicitly
1662
1638
  beamformer = Trait(BeamformerBase)
1663
1639
 
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')
1669
-
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')
1673
-
1674
- #: Floating point precision of result, is set automatically.
1675
- precision = Delegate('beamformer')
1676
-
1677
1640
  #: The floating-number-precision of the PSFs. Default is 64 bit.
1678
1641
  psf_precision = Trait('float64', 'float32', desc='precision of PSF.')
1679
1642
 
@@ -1689,49 +1652,40 @@ class BeamformerClean(BeamformerBase):
1689
1652
 
1690
1653
  # internal identifier
1691
1654
  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'],
1655
+ depends_on=BEAMFORMER_BASE_DIGEST_DEPENDENCIES + ['n_iter', 'damp', 'psf_precision'],
1698
1656
  )
1699
1657
 
1700
1658
  @cached_property
1701
1659
  def _get_digest(self):
1702
1660
  return digest(self)
1703
1661
 
1704
- @cached_property
1705
- def _get_ext_digest(self):
1706
- return digest(self, 'ext_digest')
1662
+ @on_trait_change('beamformer.digest')
1663
+ def delegate_beamformer_traits(self):
1664
+ self.freq_data = self.beamformer.freq_data
1665
+ self.r_diag = self.beamformer.r_diag
1666
+ self.steer = self.beamformer.steer
1707
1667
 
1708
- def calc(self, ac, fr):
1709
- """Calculates the CLEAN result for the frequencies defined by :attr:`freq_data`.
1668
+ def _calc(self, ind):
1669
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1710
1670
 
1711
1671
  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.
1672
+ accessing the beamformer's :attr:`result` or calling
1673
+ its :meth:`synthetic` method.
1714
1674
 
1715
1675
  Parameters
1716
1676
  ----------
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'
1677
+ ind : array of int
1678
+ This array contains all frequency indices for which (re)calculation is
1679
+ to be performed
1727
1680
 
1728
1681
  Returns
1729
1682
  -------
1730
- This method only returns values through the *ac* and *fr* parameters
1683
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1731
1684
 
1732
1685
  """
1733
- f = self.freq_data.fftfreq()
1686
+ f = self._f
1734
1687
  gs = self.steer.grid.size
1688
+ normfactor = self.sig_loss_norm()
1735
1689
 
1736
1690
  if self.calcmode == 'full':
1737
1691
  warn(
@@ -1740,28 +1694,37 @@ class BeamformerClean(BeamformerBase):
1740
1694
  stacklevel=2,
1741
1695
  )
1742
1696
  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
1697
+ param_steer_type, steer_vector = self._beamformer_params()
1698
+ for i in ind:
1699
+ p.freq = f[i]
1700
+ csm = array(self.freq_data.csm[i], dtype='complex128')
1701
+ dirty = beamformerFreq(
1702
+ param_steer_type,
1703
+ self.r_diag,
1704
+ normfactor,
1705
+ steer_vector(f[i]),
1706
+ csm,
1707
+ )[0]
1708
+ if self.r_diag: # set (unphysical) negative output values to 0
1709
+ indNegSign = sign(dirty) < 0
1710
+ dirty[indNegSign] = 0.0
1711
+
1712
+ clean = zeros(gs, dtype=dirty.dtype)
1713
+ i_iter = 0
1714
+ flag = True
1715
+ while flag:
1716
+ dirty_sum = abs(dirty).sum(0)
1717
+ next_max = dirty.argmax(0)
1718
+ p.grid_indices = array([next_max])
1719
+ psf = p.psf.reshape(gs)
1720
+ new_amp = self.damp * dirty[next_max] # / psf[next_max]
1721
+ clean[next_max] += new_amp
1722
+ dirty -= psf * new_amp
1723
+ i_iter += 1
1724
+ flag = dirty_sum > abs(dirty).sum(0) and i_iter < self.n_iter and max(dirty) > 0
1725
+
1726
+ self._ac[i] = clean
1727
+ self._fr[i] = 1
1765
1728
 
1766
1729
 
1767
1730
  class BeamformerCMF(BeamformerBase):
@@ -1804,9 +1767,24 @@ class BeamformerCMF(BeamformerBase):
1804
1767
  #: If True, shows the status of the PyLops solver. Only relevant in case of FISTA or Split_Bregman
1805
1768
  show = Bool(False, desc='show output of PyLops solvers')
1806
1769
 
1770
+ #: Energy normalization in case of diagonal removal not implemented for inverse methods.
1771
+ r_diag_norm = Enum(
1772
+ None,
1773
+ desc='Energy normalization in case of diagonal removal not implemented for inverse methods',
1774
+ )
1775
+
1807
1776
  # internal identifier
1808
1777
  digest = Property(
1809
- depends_on=['freq_data.digest', 'alpha', 'method', 'max_iter', 'unit_mult', 'r_diag', 'steer.inv_digest'],
1778
+ depends_on=[
1779
+ 'freq_data.digest',
1780
+ 'alpha',
1781
+ 'method',
1782
+ 'max_iter',
1783
+ 'unit_mult',
1784
+ 'r_diag',
1785
+ 'precision',
1786
+ 'steer.inv_digest',
1787
+ ],
1810
1788
  )
1811
1789
 
1812
1790
  @cached_property
@@ -1822,163 +1800,154 @@ class BeamformerCMF(BeamformerBase):
1822
1800
  )
1823
1801
  raise ImportError(msg)
1824
1802
 
1825
- def calc(self, ac, fr):
1826
- """Calculates the CMF result for the frequencies defined by :attr:`freq_data`.
1803
+ def _calc(self, ind):
1804
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
1827
1805
 
1828
1806
  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.
1807
+ accessing the beamformer's :attr:`result` or calling
1808
+ its :meth:`synthetic` method.
1831
1809
 
1832
1810
  Parameters
1833
1811
  ----------
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'
1812
+ ind : array of int
1813
+ This array contains all frequency indices for which (re)calculation is
1814
+ to be performed
1844
1815
 
1845
1816
  Returns
1846
1817
  -------
1847
- This method only returns values through the *ac* and *fr* parameters
1818
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
1848
1819
 
1849
1820
  """
1821
+ f = self._f
1850
1822
 
1851
1823
  # function to repack complex matrices to deal with them in real number space
1852
- def realify(M):
1853
- return vstack([M.real, M.imag])
1824
+ def realify(matrix):
1825
+ return vstack([matrix.real, matrix.imag])
1854
1826
 
1855
1827
  # prepare calculation
1856
- i = self.freq_data.indices
1857
- f = self.freq_data.fftfreq()
1858
1828
  nc = self.freq_data.numchannels
1859
1829
  numpoints = self.steer.grid.size
1860
1830
  unit = self.unit_mult
1861
1831
 
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)
1832
+ for i in ind:
1833
+ csm = array(self.freq_data.csm[i], dtype='complex128', copy=1)
1865
1834
 
1866
- h = self.steer.transfer(f[i]).T
1835
+ h = self.steer.transfer(f[i]).T
1867
1836
 
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)
1837
+ # reduced Kronecker product (only where solution matrix != 0)
1838
+ Bc = (h[:, :, newaxis] * h.conjugate().T[newaxis, :, :]).transpose(2, 0, 1)
1839
+ Ac = Bc.reshape(nc * nc, numpoints)
1871
1840
 
1872
- # get indices for upper triangular matrices (use tril b/c transposed)
1873
- ind = reshape(tril(ones((nc, nc))), (nc * nc,)) > 0
1841
+ # get indices for upper triangular matrices (use tril b/c transposed)
1842
+ ind = reshape(tril(ones((nc, nc))), (nc * nc,)) > 0
1874
1843
 
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
- )
1844
+ ind_im0 = (reshape(eye(nc), (nc * nc,)) == 0)[ind]
1845
+ if self.r_diag:
1846
+ # omit main diagonal for noise reduction
1847
+ ind_reim = hstack([ind_im0, ind_im0])
1848
+ else:
1849
+ # take all real parts -- also main diagonal
1850
+ ind_reim = hstack([ones(size(ind_im0)) > 0, ind_im0])
1851
+ ind_reim[0] = True # why this ?
1852
+
1853
+ A = realify(Ac[ind, :])[ind_reim, :]
1854
+ # use csm.T for column stacking reshape!
1855
+ R = realify(reshape(csm.T, (nc * nc, 1))[ind, :])[ind_reim, :] * unit
1856
+ # choose method
1857
+ if self.method == 'LassoLars':
1858
+ model = LassoLars(alpha=self.alpha * unit, max_iter=self.max_iter, **sklearn_ndict)
1859
+ elif self.method == 'LassoLarsBIC':
1860
+ model = LassoLarsIC(criterion='bic', max_iter=self.max_iter, **sklearn_ndict)
1861
+ elif self.method == 'OMPCV':
1862
+ model = OrthogonalMatchingPursuitCV(**sklearn_ndict)
1863
+ elif self.method == 'NNLS':
1864
+ model = LinearRegression(positive=True)
1865
+
1866
+ if self.method == 'Split_Bregman' and config.have_pylops:
1867
+ from pylops import Identity, MatrixMult, SplitBregman
1868
+
1869
+ Oop = MatrixMult(A) # tranfer operator
1870
+ Iop = self.alpha * Identity(numpoints) # regularisation
1871
+ self._ac[i], iterations = SplitBregman(
1872
+ Oop,
1873
+ [Iop],
1874
+ R[:, 0],
1875
+ niter_outer=self.max_iter,
1876
+ niter_inner=5,
1877
+ RegsL2=None,
1878
+ dataregsL2=None,
1879
+ mu=1.0,
1880
+ epsRL1s=[1],
1881
+ tol=1e-10,
1882
+ tau=1.0,
1883
+ show=self.show,
1884
+ )
1885
+ self._ac[i] /= unit
1886
+
1887
+ elif self.method == 'FISTA' and config.have_pylops:
1888
+ from pylops import FISTA, MatrixMult
1889
+
1890
+ Oop = MatrixMult(A) # tranfer operator
1891
+ self._ac[i], iterations = FISTA(
1892
+ Op=Oop,
1893
+ data=R[:, 0],
1894
+ niter=self.max_iter,
1895
+ eps=self.alpha,
1896
+ alpha=None,
1897
+ eigsiter=None,
1898
+ eigstol=0,
1899
+ tol=1e-10,
1900
+ show=self.show,
1901
+ )
1902
+ self._ac[i] /= unit
1903
+ elif self.method == 'fmin_l_bfgs_b':
1904
+ # function to minimize
1905
+ def function(x):
1906
+ # function
1907
+ func = x.T @ A.T @ A @ x - 2 * R.T @ A @ x + R.T @ R
1908
+ # derivitaive
1909
+ der = 2 * A.T @ A @ x.T[:, newaxis] - 2 * A.T @ R
1910
+ return func[0].T, der[:, 0]
1911
+
1912
+ # initial guess
1913
+ x0 = ones([numpoints])
1914
+ # boundarys - set to non negative
1915
+ boundarys = tile((0, +inf), (len(x0), 1))
1916
+
1917
+ # optimize
1918
+ self._ac[i], yval, dicts = fmin_l_bfgs_b(
1919
+ function,
1920
+ x0,
1921
+ fprime=None,
1922
+ args=(),
1923
+ approx_grad=0,
1924
+ bounds=boundarys,
1925
+ m=10,
1926
+ factr=10000000.0,
1927
+ pgtol=1e-05,
1928
+ epsilon=1e-08,
1929
+ iprint=-1,
1930
+ maxfun=15000,
1931
+ maxiter=self.max_iter,
1932
+ disp=None,
1933
+ callback=None,
1934
+ maxls=20,
1935
+ )
1967
1936
 
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
1937
+ self._ac[i] /= unit
1938
+ else:
1939
+ # from sklearn 1.2, normalize=True does not work the same way anymore and the pipeline
1940
+ # approach with StandardScaler does scale in a different way, thus we monkeypatch the
1941
+ # code and normalize ourselves to make results the same over different sklearn versions
1942
+ norms = norm(A, axis=0)
1943
+ # get rid of annoying sklearn warnings that appear for sklearn<1.2 despite any settings
1944
+ with warnings.catch_warnings():
1945
+ warnings.simplefilter('ignore', category=FutureWarning)
1946
+ # normalized A
1947
+ model.fit(A / norms, R[:, 0])
1948
+ # recover normalization in the coef's
1949
+ self._ac[i] = model.coef_[:] / norms / unit
1950
+ self._fr[i] = 1
1982
1951
 
1983
1952
 
1984
1953
  class BeamformerSODIX(BeamformerBase):
@@ -2000,10 +1969,6 @@ class BeamformerSODIX(BeamformerBase):
2000
1969
  #: defaults to 200
2001
1970
  max_iter = Int(200, desc='maximum number of iterations')
2002
1971
 
2003
- #: Norm to consider for the regularization
2004
- #: defaults to L-1 Norm
2005
- pnorm = Float(1, desc='Norm for regularization')
2006
-
2007
1972
  #: Weight factor for regularization,
2008
1973
  #: defaults to 0.0.
2009
1974
  alpha = Range(0.0, 1.0, 0.0, desc='regularization factor')
@@ -2014,215 +1979,60 @@ class BeamformerSODIX(BeamformerBase):
2014
1979
  #: within fitting method algorithms. Defaults to 1e9.
2015
1980
  unit_mult = Float(1e9, desc='unit multiplier')
2016
1981
 
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')
1982
+ #: Energy normalization in case of diagonal removal not implemented for inverse methods.
1983
+ r_diag_norm = Enum(
1984
+ None,
1985
+ desc='Energy normalization in case of diagonal removal not implemented for inverse methods',
1986
+ )
2021
1987
 
2022
1988
  # internal identifier
2023
1989
  digest = Property(
2024
- depends_on=['freq_data.digest', 'alpha', 'method', 'max_iter', 'unit_mult', 'r_diag', 'steer.inv_digest'],
1990
+ depends_on=[
1991
+ 'freq_data.digest',
1992
+ 'alpha',
1993
+ 'method',
1994
+ 'max_iter',
1995
+ 'unit_mult',
1996
+ 'r_diag',
1997
+ 'precision',
1998
+ 'steer.inv_digest',
1999
+ ],
2025
2000
  )
2026
2001
 
2027
2002
  @cached_property
2028
2003
  def _get_digest(self):
2029
2004
  return digest(self)
2030
2005
 
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`.
2006
+ def _calc(self, ind):
2007
+ """Calculates the result for the frequencies defined by :attr:`freq_data`.
2194
2008
 
2195
2009
  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.
2010
+ accessing the beamformer's :attr:`result` or calling
2011
+ its :meth:`synthetic` method.
2198
2012
 
2199
2013
  Parameters
2200
2014
  ----------
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'
2015
+ ind : array of int
2016
+ This array contains all frequency indices for which (re)calculation is
2017
+ to be performed
2211
2018
 
2212
2019
  Returns
2213
2020
  -------
2214
- This method only returns values through the *ac* and *fr* parameters
2021
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
2215
2022
 
2216
2023
  """
2217
2024
  # prepare calculation
2218
- i = self.freq_data.indices
2219
- f = self.freq_data.fftfreq()
2025
+ f = self._f
2220
2026
  numpoints = self.steer.grid.size
2221
2027
  # unit = self.unit_mult
2222
2028
  num_mics = self.steer.mics.num_mics
2223
-
2224
- for i in self.freq_data.indices:
2225
- if not fr[i]:
2029
+ # SODIX needs special treatment as the result from one frequency is used to
2030
+ # determine the initial guess for the next frequency in order to speed up
2031
+ # computation. Instead of just solving for only the frequencies in ind, we
2032
+ # start with index 1 (minimum frequency) and also check if the result is
2033
+ # already computed
2034
+ for i in range(1, ind.max() + 1):
2035
+ if not self._fr[i]:
2226
2036
  # measured csm
2227
2037
  csm = array(self.freq_data.csm[i], dtype='complex128', copy=1)
2228
2038
  # transfer function
@@ -2230,10 +2040,10 @@ class BeamformerSODIX(BeamformerBase):
2230
2040
 
2231
2041
  if self.method == 'fmin_l_bfgs_b':
2232
2042
  # function to minimize
2233
- def function(D):
2043
+ def function(directions):
2234
2044
  """Parameters
2235
2045
  ----------
2236
- D
2046
+ directions
2237
2047
  [numpoints*num_mics]
2238
2048
 
2239
2049
  Returns
@@ -2245,7 +2055,7 @@ class BeamformerSODIX(BeamformerBase):
2245
2055
 
2246
2056
  """
2247
2057
  #### the sodix function ####
2248
- Djm = D.reshape([numpoints, num_mics])
2058
+ Djm = directions.reshape([numpoints, num_mics])
2249
2059
  p = h.T * Djm
2250
2060
  csm_mod = dot(p.T, p.conj())
2251
2061
  Q = csm - csm_mod
@@ -2262,11 +2072,11 @@ class BeamformerSODIX(BeamformerBase):
2262
2072
  return func, derdrl.ravel()
2263
2073
 
2264
2074
  ##### initial guess ####
2265
- if all(ac[(i - 1)] == 0):
2075
+ if not self._fr[(i - 1)]:
2266
2076
  D0 = ones([numpoints, num_mics])
2267
2077
  else:
2268
2078
  D0 = sqrt(
2269
- ac[(i - 1)]
2079
+ self._ac[(i - 1)]
2270
2080
  * real(trace(csm) / trace(array(self.freq_data.csm[i - 1], dtype='complex128', copy=1))),
2271
2081
  )
2272
2082
 
@@ -2295,10 +2105,10 @@ class BeamformerSODIX(BeamformerBase):
2295
2105
  maxls=20,
2296
2106
  )
2297
2107
  # squared pressure
2298
- ac[i] = qi**2
2108
+ self._ac[i] = qi**2
2299
2109
  else:
2300
2110
  pass
2301
- fr[i] = 1
2111
+ self._fr[i] = 1
2302
2112
 
2303
2113
 
2304
2114
  class BeamformerGIB(BeamformerEig): # BeamformerEig #BeamformerBase
@@ -2352,11 +2162,18 @@ class BeamformerGIB(BeamformerEig): # BeamformerEig #BeamformerBase
2352
2162
  # First eigenvalue to consider. Defaults to 0.
2353
2163
  m = Int(0, desc='First eigenvalue to consider')
2354
2164
 
2355
- # internal identifier++++++++++++++++++++++++++++++++++++++++++++++++++
2165
+ #: Energy normalization in case of diagonal removal not implemented for inverse methods.
2166
+ r_diag_norm = Enum(
2167
+ None,
2168
+ desc='Energy normalization in case of diagonal removal not implemented for inverse methods',
2169
+ )
2170
+
2171
+ # internal identifier
2356
2172
  digest = Property(
2357
2173
  depends_on=[
2358
2174
  'steer.inv_digest',
2359
2175
  'freq_data.digest',
2176
+ 'precision',
2360
2177
  'alpha',
2361
2178
  'method',
2362
2179
  'max_iter',
@@ -2381,33 +2198,25 @@ class BeamformerGIB(BeamformerEig): # BeamformerEig #BeamformerBase
2381
2198
  na = max(nm + na, 0)
2382
2199
  return min(nm - 1, na)
2383
2200
 
2384
- def calc(self, ac, fr):
2201
+ def _calc(self, ind):
2385
2202
  """Calculates the result for the frequencies defined by :attr:`freq_data`.
2386
2203
 
2387
2204
  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.
2205
+ accessing the beamformer's :attr:`result` or calling
2206
+ its :meth:`synthetic` method.
2390
2207
 
2391
2208
  Parameters
2392
2209
  ----------
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'
2210
+ ind : array of int
2211
+ This array contains all frequency indices for which (re)calculation is
2212
+ to be performed
2403
2213
 
2404
2214
  Returns
2405
2215
  -------
2406
- This method only returns values through the *ac* and *fr* parameters
2216
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
2407
2217
 
2408
2218
  """
2409
- # prepare calculation
2410
- f = self.freq_data.fftfreq()
2219
+ f = self._f
2411
2220
  n = int(self.na) # number of eigenvalues
2412
2221
  m = int(self.m) # number of first eigenvalue
2413
2222
  numchannels = self.freq_data.numchannels # number of channels
@@ -2415,122 +2224,121 @@ class BeamformerGIB(BeamformerEig): # BeamformerEig #BeamformerBase
2415
2224
  hh = zeros((1, numpoints, numchannels), dtype='D')
2416
2225
 
2417
2226
  # 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
2227
+ for i in ind:
2228
+ # for monopole and source strenght Q needs to define density
2229
+ # calculate a transfer matrix A
2230
+ hh = self.steer.transfer(f[i])
2231
+ A = hh.T
2232
+ # eigenvalues and vectors
2233
+ csm = array(self.freq_data.csm[i], dtype='complex128', copy=1)
2234
+ eva, eve = eigh(csm)
2235
+ eva = eva[::-1]
2236
+ eve = eve[:, ::-1]
2237
+ eva[eva < max(eva) / 1e12] = 0 # set small values zo 0, lowers numerical errors in simulated data
2238
+ # init sources
2239
+ qi = zeros([n + m, numpoints], dtype='complex128')
2240
+ # Select the number of coherent modes to be processed referring to the eigenvalue distribution.
2241
+ # for s in arange(n):
2242
+ for s in list(range(m, n + m)):
2243
+ if eva[s] > 0:
2244
+ # Generate the corresponding eigenmodes
2245
+ emode = array(sqrt(eva[s]) * eve[:, s], dtype='complex128')
2246
+ # choose method for computation
2247
+ if self.method == 'Suzuki':
2248
+ leftpoints = numpoints
2249
+ locpoints = arange(numpoints)
2250
+ weights = diag(ones(numpoints))
2251
+ epsilon = arange(self.max_iter)
2252
+ for it in arange(self.max_iter):
2253
+ if numchannels <= leftpoints:
2254
+ AWA = dot(dot(A[:, locpoints], weights), A[:, locpoints].conj().T)
2255
+ epsilon[it] = max(absolute(eigvals(AWA))) * self.eps_perc
2256
+ qi[s, locpoints] = dot(
2257
+ dot(
2258
+ dot(weights, A[:, locpoints].conj().T),
2259
+ inv(AWA + eye(numchannels) * epsilon[it]),
2260
+ ),
2261
+ emode,
2262
+ )
2263
+ elif numchannels > leftpoints:
2264
+ AA = dot(A[:, locpoints].conj().T, A[:, locpoints])
2265
+ epsilon[it] = max(absolute(eigvals(AA))) * self.eps_perc
2266
+ qi[s, locpoints] = dot(
2267
+ dot(inv(AA + inv(weights) * epsilon[it]), A[:, locpoints].conj().T),
2268
+ emode,
2269
+ )
2270
+ if self.beta < 1 and it > 1:
2271
+ # Reorder from the greatest to smallest magnitude to define a reduced-point source distribution , and reform a reduced transfer matrix
2272
+ leftpoints = int(round(numpoints * self.beta ** (it + 1)))
2273
+ idx = argsort(abs(qi[s, locpoints]))[::-1]
2274
+ # print(it, leftpoints, locpoints, idx )
2275
+ locpoints = delete(locpoints, [idx[leftpoints::]])
2276
+ qix = zeros([n + m, leftpoints], dtype='complex128')
2277
+ qix[s, :] = qi[s, locpoints]
2278
+ # calc weights for next iteration
2279
+ weights = diag(absolute(qix[s, :]) ** (2 - self.pnorm))
2280
+ else:
2281
+ weights = diag(absolute(qi[s, :]) ** (2 - self.pnorm))
2282
+
2283
+ elif self.method == 'InverseIRLS':
2284
+ weights = eye(numpoints)
2285
+ locpoints = arange(numpoints)
2286
+ for _it in arange(self.max_iter):
2287
+ if numchannels <= numpoints:
2288
+ wtwi = inv(dot(weights.T, weights))
2289
+ aH = A.conj().T
2290
+ qi[s, :] = dot(dot(wtwi, aH), dot(inv(dot(A, dot(wtwi, aH))), emode))
2291
+ weights = diag(absolute(qi[s, :]) ** ((2 - self.pnorm) / 2))
2292
+ weights = weights / sum(absolute(weights))
2293
+ elif numchannels > numpoints:
2294
+ wtw = dot(weights.T, weights)
2295
+ qi[s, :] = dot(dot(inv(dot(dot(A.conj.T, wtw), A)), dot(A.conj().T, wtw)), emode)
2296
+ weights = diag(absolute(qi[s, :]) ** ((2 - self.pnorm) / 2))
2297
+ weights = weights / sum(absolute(weights))
2523
2298
  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
2299
+ locpoints = arange(numpoints)
2300
+ unit = self.unit_mult
2301
+ AB = vstack([hstack([A.real, -A.imag]), hstack([A.imag, A.real])])
2302
+ R = hstack([emode.real.T, emode.imag.T]) * unit
2303
+ if self.method == 'LassoLars':
2304
+ model = LassoLars(alpha=self.alpha * unit, max_iter=self.max_iter)
2305
+ elif self.method == 'LassoLarsBIC':
2306
+ model = LassoLarsIC(criterion='bic', max_iter=self.max_iter)
2307
+ elif self.method == 'OMPCV':
2308
+ model = OrthogonalMatchingPursuitCV()
2309
+ elif self.method == 'LassoLarsCV':
2310
+ model = LassoLarsCV()
2311
+ elif self.method == 'NNLS':
2312
+ model = LinearRegression(positive=True)
2313
+ model.normalize = False
2314
+ # from sklearn 1.2, normalize=True does not work
2315
+ # the same way anymore and the pipeline approach
2316
+ # with StandardScaler does scale in a different
2317
+ # way, thus we monkeypatch the code and normalize
2318
+ # ourselves to make results the same over different
2319
+ # sklearn versions
2320
+ norms = norm(AB, axis=0)
2321
+ # get rid of annoying sklearn warnings that appear
2322
+ # for sklearn<1.2 despite any settings
2323
+ with warnings.catch_warnings():
2324
+ warnings.simplefilter('ignore', category=FutureWarning)
2325
+ # normalized A
2326
+ model.fit(AB / norms, R)
2327
+ # recover normalization in the coef's
2328
+ qi_real, qi_imag = hsplit(model.coef_[:] / norms / unit, 2)
2329
+ # print(s,qi.size)
2330
+ qi[s, locpoints] = qi_real + qi_imag * 1j
2331
+ else:
2332
+ warn(
2333
+ f'Eigenvalue {s:g} <= 0 for frequency index {i:g}. Will not be calculated!',
2334
+ Warning,
2335
+ stacklevel=2,
2336
+ )
2337
+ # Generate source maps of all selected eigenmodes, and superpose source intensity for each source type.
2338
+ temp = zeros(numpoints)
2339
+ temp[locpoints] = sum(absolute(qi[:, locpoints]) ** 2, axis=0)
2340
+ self._ac[i] = temp
2341
+ self._fr[i] = 1
2534
2342
 
2535
2343
 
2536
2344
  class BeamformerAdaptiveGrid(BeamformerBase, Grid):
@@ -2581,7 +2389,7 @@ class BeamformerGridlessOrth(BeamformerAdaptiveGrid):
2581
2389
 
2582
2390
  #: List of components to consider, use this to directly set the eigenvalues
2583
2391
  #: used in the beamformer. Alternatively, set :attr:`n`.
2584
- eva_list = CArray(dtype=int, desc='components')
2392
+ eva_list = CArray(dtype=int, value=array([-1]), desc='components')
2585
2393
 
2586
2394
  #: Number of components to consider, defaults to 1. If set,
2587
2395
  #: :attr:`eva_list` will contain
@@ -2601,9 +2409,17 @@ class BeamformerGridlessOrth(BeamformerAdaptiveGrid):
2601
2409
  #: and 1 iteration
2602
2410
  shgo = Dict
2603
2411
 
2412
+ #: No normalization implemented. Defaults to 1.0.
2413
+ r_diag_norm = Enum(
2414
+ 1.0,
2415
+ desc='If diagonal of the csm is removed, some signal energy is lost.'
2416
+ 'This is handled via this normalization factor.'
2417
+ 'For this class, normalization is not implemented. Defaults to 1.0.',
2418
+ )
2419
+
2604
2420
  # internal identifier
2605
2421
  digest = Property(
2606
- depends_on=['freq_data.digest', '_steer_obj.digest', 'r_diag', 'eva_list', 'bounds', 'shgo'],
2422
+ depends_on=['freq_data.digest', '_steer_obj.digest', 'precision', 'r_diag', 'eva_list', 'bounds', 'shgo'],
2607
2423
  )
2608
2424
 
2609
2425
  @cached_property
@@ -2615,41 +2431,30 @@ class BeamformerGridlessOrth(BeamformerAdaptiveGrid):
2615
2431
  """Sets the list of eigenvalues to consider."""
2616
2432
  self.eva_list = arange(-1, -1 - self.n, -1)
2617
2433
 
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
2434
  @property_depends_on('n')
2624
2435
  def _get_size(self):
2625
2436
  return self.n * self.freq_data.fftfreq().shape[0]
2626
2437
 
2627
- def calc(self, ac, fr):
2438
+ def _calc(self, ind):
2628
2439
  """Calculates the result for the frequencies defined by :attr:`freq_data`.
2629
2440
 
2630
2441
  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.
2442
+ accessing the beamformer's :attr:`result` or calling
2443
+ its :meth:`synthetic` method.
2633
2444
 
2634
2445
  Parameters
2635
2446
  ----------
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'
2447
+ ind : array of int
2448
+ This array contains all frequency indices for which (re)calculation is
2449
+ to be performed
2646
2450
 
2647
2451
  Returns
2648
2452
  -------
2649
- This method only returns values through the *ac* and *fr* parameters
2453
+ This method only returns values through :attr:`_ac` and :attr:`_fr`
2650
2454
 
2651
2455
  """
2652
- f = self.freq_data.fftfreq()
2456
+ f = self._f
2457
+ normfactor = self.sig_loss_norm()
2653
2458
  numchannels = self.freq_data.numchannels
2654
2459
  # eigenvalue number list in standard form from largest to smallest
2655
2460
  eva_list = unique(self.eva_list % self.steer.mics.num_mics)[::-1]
@@ -2663,7 +2468,6 @@ class BeamformerGridlessOrth(BeamformerAdaptiveGrid):
2663
2468
  'n': 256,
2664
2469
  'iters': 1,
2665
2470
  'sampling_method': 'sobol',
2666
- 'options': {'local_iter': 1},
2667
2471
  'minimizer_kwargs': {'method': 'Nelder-Mead'},
2668
2472
  }
2669
2473
  shgo_opts.update(self.shgo)
@@ -2675,36 +2479,39 @@ class BeamformerGridlessOrth(BeamformerAdaptiveGrid):
2675
2479
  self.steer.env.roi = array(roi).T
2676
2480
  bmin = array(tuple(map(min, self.bounds)))
2677
2481
  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):
2482
+ for i in ind:
2483
+ eva = array(self.freq_data.eva[i], dtype='float64')
2484
+ eve = array(self.freq_data.eve[i], dtype='complex128')
2485
+ k = 2 * pi * f[i] / env.c
2486
+ for j, n in enumerate(eva_list):
2487
+ # print(f[i],n)
2488
+
2489
+ def func(xy):
2490
+ # function to minimize globally
2491
+ xy = clip(xy, bmin, bmax)
2492
+ r0 = env._r(xy[:, newaxis])
2493
+ rm = env._r(xy[:, newaxis], mpos)
2494
+ return -beamformerFreq(
2495
+ steer_type,
2496
+ self.r_diag,
2497
+ normfactor,
2498
+ (r0, rm, k),
2499
+ (ones(1), eve[:, n : n + 1]),
2500
+ )[0][0] # noqa: B023
2501
+
2502
+ # simplical global homotopy optimizer
2503
+ oR = shgo(func, self.bounds, **shgo_opts)
2504
+ # index in grid
2505
+ i1 = i * self.n + j
2506
+ # store result for position
2507
+ self._gpos[:, i1] = oR['x']
2508
+ # store result for level
2509
+ self._ac[i, i1] = eva[n] / numchannels
2510
+ # print(oR['x'],eva[n]/numchannels,oR)
2511
+ self._fr[i] = 1
2512
+
2513
+
2514
+ def L_p(x): # noqa: N802
2708
2515
  r"""Calculates the sound pressure level from the squared sound pressure.
2709
2516
 
2710
2517
  :math:`L_p = 10 \lg ( x / 4\cdot 10^{-10})`