acoular 25.3.post1__py3-none-any.whl → 25.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
acoular/internal.py CHANGED
@@ -2,10 +2,13 @@
2
2
  # Copyright (c) Acoular Development Team.
3
3
  # ------------------------------------------------------------------------------
4
4
 
5
+ """Implements a digest function for caching of traits based on a unique identifier."""
6
+
5
7
  from hashlib import md5
6
8
 
7
9
 
8
10
  def digest(obj, name='digest'):
11
+ """Generate a unique digest for the given object based on its traits."""
9
12
  str_ = [str(obj.__class__).encode('UTF-8')]
10
13
  for do_ in obj.trait(name).depends_on:
11
14
  vobj = obj
@@ -19,6 +22,7 @@ def digest(obj, name='digest'):
19
22
 
20
23
 
21
24
  def ldigest(obj_list):
25
+ """Generate a unique digest for a list of objects based on their traits."""
22
26
  str_ = []
23
27
  for i in obj_list:
24
28
  str_.append(str(i.digest).encode('UTF-8'))
acoular/microphones.py CHANGED
@@ -22,6 +22,7 @@ from traits.api import (
22
22
  HasStrictTraits,
23
23
  List,
24
24
  Property,
25
+ Union,
25
26
  cached_property,
26
27
  on_trait_change,
27
28
  )
@@ -31,7 +32,9 @@ from .deprecation import deprecated_alias
31
32
  from .internal import digest
32
33
 
33
34
 
34
- @deprecated_alias({'mpos_tot': 'pos_total', 'mpos': 'pos', 'from_file': 'file'}, read_only=['mpos'])
35
+ @deprecated_alias(
36
+ {'mpos_tot': 'pos_total', 'mpos': 'pos', 'from_file': 'file'}, read_only=['mpos'], removal_version='25.10'
37
+ )
35
38
  class MicGeom(HasStrictTraits):
36
39
  """
37
40
  Provide the geometric arrangement of microphones in an array.
@@ -46,6 +49,17 @@ class MicGeom(HasStrictTraits):
46
49
  attribute is updated.
47
50
  - Small numerical values in the computed :attr:`center` are set to zero for numerical stability.
48
51
 
52
+ .. _units_note_microphones:
53
+
54
+ Unit System
55
+ -----------
56
+ The source code is agnostic to the unit of length. The microphone positions' coordinates are
57
+ assumed to be in meters. This is consistent with the standard
58
+ :class:`~acoular.environments.Environment` class which uses the speed of sound at 20°C at sea
59
+ level under standard atmosphere pressure in m/s. If the microphone positions' coordinates are
60
+ provided in a unit other than meter, it is advisable to change the
61
+ :attr:`~acoular.environments.Environment.c` attribute to match the given unit.
62
+
49
63
  Examples
50
64
  --------
51
65
  To set a microphone geomerty for ``n`` programmatically, first a ``(3,n)`` array is needed. In
@@ -128,15 +142,17 @@ class MicGeom(HasStrictTraits):
128
142
 
129
143
  #: Path to the XML file containing microphone positions. The XML file should have elements with
130
144
  #: the tag ``pos`` and attributes ``Name``, ``x``, ``y``, and ``z``.
131
- file = File(filter=['*.xml'], exists=True, desc='name of the xml file to import')
145
+ file = Union(None, File(filter=['*.xml'], exists=True), desc='name of the xml file to import')
132
146
 
133
147
  #: Array containing the ``x, y, z`` positions of all microphones, including invalid ones, shape
134
148
  #: ``(3,`` :attr:`num_mics` ``)``. This is set automatically when :attr:`file` changes or
135
- #: explicitly by assigning an array of floats.
149
+ #: explicitly by assigning an array of floats. All coordinates are in meters by default (see
150
+ #: :ref:`notes <units_note_micophones>`).
136
151
  pos_total = CArray(dtype=float, shape=(3, None), desc='x, y, z position of all microphones')
137
152
 
138
153
  #: Array containing the ``x, y, z`` positions of valid microphones (i.e., excluding those in
139
154
  #: :attr:`invalid_channels`), shape ``(3,`` :attr:`num_mics` ``)``. (read-only)
155
+ #: All coordinates are in meters by default (see :ref:`notes <units_note>`).
140
156
  pos = Property(depends_on=['pos_total', 'invalid_channels'], desc='x, y, z position of used microphones')
141
157
 
142
158
  #: List of indices indicating microphones to be excluded from calculations and results.
@@ -250,6 +266,8 @@ class MicGeom(HasStrictTraits):
250
266
  index of the microphone.
251
267
  - This method only exports the positions of the valid microphones (those not listed in
252
268
  :attr:`invalid_channels`).
269
+ - All coordinates (x, y, z) are exported in meters by default (see
270
+ :ref:`notes <units_note_micophones>`).
253
271
  """
254
272
  filepath = Path(filename)
255
273
  basename = filepath.stem
acoular/process.py CHANGED
@@ -10,8 +10,6 @@ General purpose blockwise processing methods independent of the domain (time or
10
10
  Average
11
11
  Cache
12
12
  SampleSplitter
13
- TimeAverage
14
- TimeCache
15
13
  SamplesBuffer
16
14
  """
17
15
 
@@ -65,7 +63,9 @@ class LockedGenerator:
65
63
  return self.it.__next__()
66
64
 
67
65
 
68
- @deprecated_alias({'naverage': 'num_per_average', 'numsamples': 'num_samples'}, read_only=['numsamples'])
66
+ @deprecated_alias(
67
+ {'naverage': 'num_per_average', 'numsamples': 'num_samples'}, read_only=['numsamples'], removal_version='25.10'
68
+ )
69
69
  class Average(InOut):
70
70
  """
71
71
  Calculate the average across consecutive time samples or frequency snapshots.
@@ -675,48 +675,6 @@ class SampleSplitter(InOut):
675
675
  raise OSError(msg)
676
676
 
677
677
 
678
- class TimeAverage(Average):
679
- """
680
- Calculate the average of the signal.
681
-
682
- .. deprecated:: 24.10
683
- The use of :class:`~acoular.process.TimeAverage` is deprecated
684
- and will be removed in Acoular version 25.07.
685
- Please use :class:`~acoular.process.Average` instead for future compatibility.
686
-
687
- Alias for :class:`~acoular.process.Average`.
688
- """
689
-
690
- def __init__(self, *args, **kwargs):
691
- super().__init__(*args, **kwargs)
692
- warn(
693
- 'Using TimeAverage is deprecated and will be removed in Acoular version 25.07. Use Average instead.',
694
- DeprecationWarning,
695
- stacklevel=2,
696
- )
697
-
698
-
699
- class TimeCache(Cache):
700
- """
701
- Cache source signals in cache file.
702
-
703
- .. deprecated:: 24.10
704
- The use of :class:`~acoular.process.TimeCache` is deprecated
705
- and will be removed in Acoular version 25.07.
706
- Please use :class:`~acoular.process.Cache` instead for future compatibility.
707
-
708
- Alias for :class:`~acoular.process.Cache`.
709
- """
710
-
711
- def __init__(self, *args, **kwargs):
712
- super().__init__(*args, **kwargs)
713
- warn(
714
- 'Using TimeCache is deprecated and will be removed in Acoular version 25.07. Use Cache instead.',
715
- DeprecationWarning,
716
- stacklevel=2,
717
- )
718
-
719
-
720
678
  class SamplesBuffer(InOut):
721
679
  """
722
680
  Handle buffering of samples from a source.
acoular/sdinput.py CHANGED
@@ -21,7 +21,10 @@ if config.have_sounddevice:
21
21
  import sounddevice as sd
22
22
 
23
23
 
24
- @deprecated_alias({'numchannels': 'num_channels', 'numsamples': 'num_samples', 'collectsamples': 'collect_samples'})
24
+ @deprecated_alias(
25
+ {'numchannels': 'num_channels', 'numsamples': 'num_samples', 'collectsamples': 'collect_samples'},
26
+ removal_version='25.10',
27
+ )
25
28
  class SoundDeviceSamplesGenerator(SamplesGenerator):
26
29
  """Controller for sound card hardware using sounddevice library.
27
30
 
@@ -87,15 +90,20 @@ class SoundDeviceSamplesGenerator(SamplesGenerator):
87
90
  self._sample_freq = f
88
91
 
89
92
  def device_properties(self):
90
- """Returns
93
+ """
94
+ Display the properties of the sounddevice input device.
95
+
96
+ Returns
91
97
  -------
92
98
  Dictionary of device properties according to sounddevice
93
99
  """
94
100
  return sd.query_devices(self.device)
95
101
 
96
102
  def result(self, num):
97
- """Python generator that yields the output block-wise. Use at least a
98
- block-size of one ring cache block.
103
+ """
104
+ Python generator that yields the output block-wise.
105
+
106
+ Use at least a block-size of one ring cache block.
99
107
 
100
108
  Parameters
101
109
  ----------
acoular/signals.py CHANGED
@@ -43,7 +43,7 @@ from .deprecation import deprecated_alias
43
43
  from .internal import digest
44
44
 
45
45
 
46
- @deprecated_alias({'numsamples': 'num_samples'})
46
+ @deprecated_alias({'numsamples': 'num_samples'}, removal_version='25.10')
47
47
  class SignalGenerator(ABCHasStrictTraits):
48
48
  """
49
49
  ABC for a simple one-channel signal generator.
@@ -556,7 +556,7 @@ class SineGenerator(PeriodicSignalGenerator):
556
556
  return self.amplitude * sin(2 * pi * self.freq * t + self.phase)
557
557
 
558
558
 
559
- @deprecated_alias({'rms': 'amplitude'})
559
+ @deprecated_alias({'rms': 'amplitude'}, removal_version='25.10')
560
560
  class GenericSignalGenerator(SignalGenerator):
561
561
  """
562
562
  Generate signals from a :class:`~acoular.base.SamplesGenerator` or derived object.
acoular/sources.py CHANGED
@@ -45,6 +45,7 @@ from numpy import (
45
45
  mod,
46
46
  newaxis,
47
47
  ones,
48
+ ones_like,
48
49
  pi,
49
50
  real,
50
51
  repeat,
@@ -317,7 +318,7 @@ def get_modes(lOrder, direction, mpos, sourceposition=None): # noqa: N803
317
318
  return modes
318
319
 
319
320
 
320
- @deprecated_alias({'name': 'file'})
321
+ @deprecated_alias({'name': 'file'}, removal_version='25.10')
321
322
  class TimeSamples(SamplesGenerator):
322
323
  """
323
324
  Container for processing time data in ``*.h5`` or NumPy array format.
@@ -369,7 +370,7 @@ class TimeSamples(SamplesGenerator):
369
370
  """
370
371
 
371
372
  #: Full path to the ``.h5`` file containing time-domain data.
372
- file = File(filter=['*.h5'], exists=True, desc='name of data file')
373
+ file = Union(None, File(filter=['*.h5'], exists=True), desc='name of data file')
373
374
 
374
375
  #: Basename of the ``.h5`` file, set automatically from the :attr:`file` attribute.
375
376
  basename = Property(depends_on=['file'], desc='basename of data file')
@@ -746,7 +747,7 @@ class MaskedTimeSamples(TimeSamples):
746
747
  i += num
747
748
 
748
749
 
749
- @deprecated_alias({'numchannels': 'num_channels', 'numsamples': 'num_samples'}, read_only=True)
750
+ @deprecated_alias({'numchannels': 'num_channels', 'numsamples': 'num_samples'}, read_only=True, removal_version='25.10')
750
751
  class PointSource(SamplesGenerator):
751
752
  """
752
753
  Define a fixed point source emitting a signal, intended for simulations.
@@ -1164,46 +1165,48 @@ class MovingPointSource(PointSource):
1164
1165
  # from the end of the calculated signal.
1165
1166
 
1166
1167
  signal = self.signal.usignal(self.up)
1167
- out = empty((num, self.num_channels))
1168
1168
  # shortcuts and initial values
1169
- m = self.mics
1170
- t = self.start * ones(m.num_mics)
1171
- i = 0
1169
+ num_mics = self.num_channels
1170
+ mpos = self.mics.pos[:, :, newaxis]
1171
+ t = self.start + ones(num_mics)[:, newaxis] * arange(num) / self.sample_freq
1172
1172
  epslim = 0.1 / self.up / self.sample_freq
1173
1173
  c0 = self.env.c
1174
1174
  tr = self.trajectory
1175
1175
  n = self.num_samples
1176
- while n:
1177
- n -= 1
1178
- eps = ones(m.num_mics)
1176
+ while n > 0:
1177
+ eps = ones_like(t) # init discrepancy in time
1179
1178
  te = t.copy() # init emission time = receiving time
1180
1179
  j = 0
1181
1180
  # Newton-Rhapson iteration
1182
1181
  while abs(eps).max() > epslim and j < 100:
1183
- loc = array(tr.location(te))
1184
- rm = loc - m.pos # distance vectors to microphones
1182
+ loc = array(tr.location(te.flatten())).reshape((3, num_mics, -1))
1183
+ rm = loc - mpos # distance vectors to microphones
1185
1184
  rm = sqrt((rm * rm).sum(0)) # absolute distance
1186
1185
  loc /= sqrt((loc * loc).sum(0)) # distance unit vector
1187
- der = array(tr.location(te, der=1))
1186
+ der = array(tr.location(te.flatten(), der=1)).reshape((3, num_mics, -1))
1188
1187
  Mr = (der * loc).sum(0) / c0 # radial Mach number
1189
- eps = (te + rm / c0 - t) / (1 + Mr) # discrepancy in time
1188
+ eps[:] = (te + rm / c0 - t) / (1 + Mr) # discrepancy in time
1190
1189
  te -= eps
1191
1190
  j += 1 # iteration count
1192
- t += 1.0 / self.sample_freq
1191
+ t += num / self.sample_freq
1193
1192
  # emission time relative to start time
1194
1193
  ind = (te - self.start_t + self.start) * self.sample_freq
1195
1194
  if self.conv_amp:
1196
1195
  rm *= (1 - Mr) ** 2
1197
1196
  try:
1198
- out[i] = signal[array(0.5 + ind * self.up, dtype=int64)] / rm
1199
- i += 1
1200
- if i == num:
1201
- yield out
1202
- i = 0
1203
- except IndexError: # if no more samples available from the source
1197
+ ind = array(0.5 + ind * self.up, dtype=int64)
1198
+ out = (signal[ind] / rm).T
1199
+ yield out[:n]
1200
+ except IndexError: # last incomplete frame
1201
+ signal_length = signal.shape[0]
1202
+ # Filter ind to exclude columns containing values greater than signal_length
1203
+ mask = (ind < signal_length).all(axis=0)
1204
+ out = (signal[ind[:, mask]] / rm[:, mask]).T
1205
+ # If out is not empty, yield it
1206
+ if out.size > 0:
1207
+ yield out[:n]
1204
1208
  break
1205
- if i > 0: # if there are still samples to yield
1206
- yield out[:i]
1209
+ n -= num
1207
1210
 
1208
1211
 
1209
1212
  class PointSourceDipole(PointSource):
@@ -1947,7 +1950,7 @@ class MovingLineSource(LineSource, MovingPointSource):
1947
1950
  yield out[:i]
1948
1951
 
1949
1952
 
1950
- @deprecated_alias({'numchannels': 'num_channels'}, read_only=True)
1953
+ @deprecated_alias({'numchannels': 'num_channels'}, read_only=True, removal_version='25.10')
1951
1954
  class UncorrelatedNoiseSource(SamplesGenerator):
1952
1955
  """
1953
1956
  Simulate uncorrelated white or pink noise signals at multiple channels.
@@ -2111,7 +2114,7 @@ class UncorrelatedNoiseSource(SamplesGenerator):
2111
2114
  return
2112
2115
 
2113
2116
 
2114
- @deprecated_alias({'numchannels': 'num_channels', 'numsamples': 'num_samples'}, read_only=True)
2117
+ @deprecated_alias({'numchannels': 'num_channels', 'numsamples': 'num_samples'}, read_only=True, removal_version='25.10')
2115
2118
  class SourceMixer(SamplesGenerator):
2116
2119
  """
2117
2120
  Combine signals from multiple sources by mixing their outputs.
acoular/spectra.py CHANGED
@@ -12,7 +12,6 @@
12
12
  """
13
13
 
14
14
  from abc import abstractmethod
15
- from warnings import warn
16
15
 
17
16
  from numpy import (
18
17
  arange,
@@ -62,8 +61,9 @@ from .internal import digest
62
61
  from .tools.utils import find_basename
63
62
 
64
63
 
65
- @deprecated_alias({'numchannels': 'num_channels'}, read_only=True)
66
- @deprecated_alias({'time_data': 'source'}, read_only=False)
64
+ @deprecated_alias(
65
+ {'numchannels': 'num_channels', 'time_data': 'source'}, read_only=['numchannels'], removal_version='25.10'
66
+ )
67
67
  class BaseSpectra(ABCHasStrictTraits):
68
68
  """
69
69
  Base class for handling spectral data in Acoular.
@@ -298,9 +298,10 @@ class PowerSpectra(BaseSpectra):
298
298
  return None
299
299
 
300
300
  def _set_freq_range(self, freq_range): # by setting this the user sets _freqlc and _freqhc
301
- self._index_set_last = False
302
- self._freqlc = freq_range[0]
303
- self._freqhc = freq_range[1]
301
+ if freq_range is not None:
302
+ self._index_set_last = False
303
+ self._freqlc = freq_range[0]
304
+ self._freqhc = freq_range[1]
304
305
 
305
306
  @property_depends_on(['source.sample_freq', 'block_size', '_ind_low', '_freqlc'])
306
307
  def _get_ind_low(self):
@@ -309,7 +310,7 @@ class PowerSpectra(BaseSpectra):
309
310
  if self._index_set_last:
310
311
  return min(self._ind_low, fftfreq.shape[0] - 1)
311
312
  return searchsorted(fftfreq[:-1], self._freqlc)
312
- return None
313
+ return 0
313
314
 
314
315
  @property_depends_on(['source.sample_freq', 'block_size', '_ind_high', '_freqhc'])
315
316
  def _get_ind_high(self):
@@ -631,7 +632,7 @@ class PowerSpectraImport(PowerSpectra):
631
632
 
632
633
  #: The frequencies included in the CSM in ascending order. Accepts list, array, or a single
633
634
  #: float value.
634
- frequencies = Union(CArray, Float, desc='frequencies included in the cross-spectral matrix')
635
+ frequencies = Union(None, CArray, Float, desc='frequencies included in the cross-spectral matrix')
635
636
 
636
637
  #: Number of time data channels, inferred from the shape of the CSM.
637
638
  num_channels = Property(depends_on=['digest'])
@@ -670,7 +671,7 @@ class PowerSpectraImport(PowerSpectra):
670
671
  basename = Property(depends_on=['digest'], desc='basename for cache file')
671
672
 
672
673
  # Shadow trait for storing the CSM, for internal use only.
673
- _csm = CArray()
674
+ _csm = Union(None, CArray(shape=(None, None, None)), desc='cross spectral matrix')
674
675
 
675
676
  # Checksum for the CSM to trigger digest calculation, for internal use only.
676
677
  _csmsum = Float()
@@ -689,12 +690,13 @@ class PowerSpectraImport(PowerSpectra):
689
690
  return self._csm
690
691
 
691
692
  def _set_csm(self, csm):
692
- if (len(csm.shape) != 3) or (csm.shape[1] != csm.shape[2]):
693
- msg = 'The cross spectral matrix must have the following shape: \
694
- (number of frequencies, num_channels, num_channels)!'
695
- raise ValueError(msg)
696
- self._csmsum = real(self._csm).sum() + (imag(self._csm) ** 2).sum() # to trigger new digest creation
697
- self._csm = csm
693
+ if csm is not None:
694
+ if csm.shape[1] != csm.shape[2]:
695
+ msg = 'The cross spectral matrix must have the following shape: \
696
+ (number of frequencies, num_channels, num_channels)!'
697
+ raise ValueError(msg)
698
+ self._csm = csm
699
+ self._csmsum = real(self._csm).sum() + (imag(self._csm) ** 2).sum() # to trigger new digest creation
698
700
 
699
701
  @property_depends_on(['digest'])
700
702
  def _get_eva(self):
@@ -720,6 +722,4 @@ class PowerSpectraImport(PowerSpectra):
720
722
  return array([self.frequencies])
721
723
  if isinstance(self.frequencies, ndarray):
722
724
  return self.frequencies
723
- if self.frequencies is None:
724
- warn('No frequencies defined for PowerSpectraImport object!', stacklevel=1)
725
725
  return self.frequencies
acoular/tbeamform.py CHANGED
@@ -174,6 +174,9 @@ class BeamformerTime(TimeOut):
174
174
  p_res *= weights
175
175
  if p_res.shape[0] < buffer.result_num: # last block shorter
176
176
  num = p_res.shape[0] - max_sample_delay
177
+ # exit loop if there is not enough data left to be processed
178
+ if num <= 0:
179
+ break
177
180
  n_index = arange(0, num + 1)[:, newaxis]
178
181
  # init step
179
182
  Phi, autopow = self._delay_and_sum(num, p_res, d_interp2, d_index, amp)
acoular/tools/helpers.py CHANGED
@@ -11,8 +11,10 @@
11
11
  barspectrum
12
12
  bardata
13
13
  c_air
14
+ get_data_file
14
15
  """
15
16
 
17
+ from pathlib import Path
16
18
  from warnings import warn
17
19
 
18
20
  from numpy import (
@@ -401,3 +403,28 @@ def c_air(t, h, p=101325, co2=0.04):
401
403
  + a14 * x_c**2
402
404
  + a15 * x_w * p * x_c
403
405
  )
406
+
407
+
408
+ def get_data_file(file):
409
+ """
410
+ Ensures a file is available locally.
411
+
412
+ If the file does not exist in ``'../data/'`` or the current directory,
413
+ it is downloaded from the Acoular GitHub repository.
414
+
415
+ Returns
416
+ -------
417
+ :class:`pathlib.Path`
418
+ Path to the file.
419
+ """
420
+ data_file = Path('../data') / file
421
+ if not data_file.exists():
422
+ data_file = Path().cwd() / file
423
+ if not data_file.exists():
424
+ import urllib.request
425
+
426
+ url = 'https://github.com/acoular/acoular/raw/master/examples/data/' + file
427
+ urllib.request.urlretrieve(url, data_file)
428
+ print(f'Calibration file location: {data_file}')
429
+
430
+ return data_file
acoular/tools/utils.py CHANGED
@@ -26,8 +26,7 @@ def get_file_basename(file, alternative_basename='void'):
26
26
  str
27
27
  Basename of the file.
28
28
  """
29
- basename = Path(file).stem
30
- return basename if basename else alternative_basename
29
+ return Path(file).stem if file else alternative_basename
31
30
 
32
31
 
33
32
  def find_basename(source, alternative_basename='void'):