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