acoular 24.10__py3-none-any.whl → 25.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
acoular/calib.py CHANGED
@@ -10,65 +10,115 @@
10
10
  """
11
11
 
12
12
  # imports from other packages
13
- from os import path
13
+ import xml.dom.minidom
14
14
 
15
- from numpy import array
16
- from traits.api import CArray, CLong, File, HasPrivateTraits, Property, cached_property, on_trait_change
15
+ from numpy import array, newaxis
16
+ from traits.api import CArray, CInt, File, List, Property, cached_property, on_trait_change
17
+
18
+ import acoular as ac
19
+
20
+ from .base import InOut
17
21
 
18
22
  # acoular imports
23
+ from .deprecation import deprecated_alias
19
24
  from .internal import digest
20
25
 
21
26
 
22
- class Calib(HasPrivateTraits):
23
- """Container for calibration data in `*.xml` format.
24
-
27
+ @deprecated_alias({'from_file': 'file'})
28
+ class Calib(InOut):
29
+ """Processing block for handling calibration data in `*.xml` or NumPy format.
30
+
31
+ This class implements the application of calibration factors to the data obtained from its
32
+ :attr:`source`. The calibrated data can be accessed (e.g. for use in a block chain) via the
33
+ :meth:`result` generator. Depending on the source type, calibration can be performed in the
34
+ time or frequency domain.
35
+
36
+
37
+ Examples
38
+ --------
39
+ Consider calibrating a time signal by specifying the calibration factors in NumPy format.
40
+ Assume that the following white noise signal is in Volt and the sensitivity of the virtual
41
+ sensor is 1e-2 V/Pa. Then, the voltage signal can be converted to a calibrated sound
42
+ pressure signal by multiplying it with a calibration factor of 100 Pa/V.
43
+
44
+ >>> import acoular as ac
45
+ >>> import numpy as np
46
+ >>>
47
+ >>> signal = ac.WNoiseGenerator(sample_freq=51200,# doctest: +SKIP
48
+ >>> num_samples=51200,# doctest: +SKIP
49
+ >>> rms=0.01).signal()# doctest: +SKIP
50
+ >>> ts = ac.TimeSamples(data=signal[:, np.newaxis], sample_freq=51200) # doctest: +SKIP
51
+ >>> calib = ac.Calib(source=ts) # doctest: +SKIP
52
+ >>> calib.data = np.array([100]) # doctest: +SKIP
53
+ >>> print(next(calib.result(num=1))) # doctest: +SKIP
54
+ [[1.76405235]]
55
+
56
+ The calibrated data can then be further processed, e.g. by calculating the FFT of the
57
+ calibrated data.
58
+
59
+ >>> fft = ac.RFFT(source=calib, block_size=16) # doctest: +SKIP
60
+ >>> print(next(fft.result(num=1))) # doctest: +SKIP
61
+ [[10.63879909+0.j 3.25957562-1.57652611j -2.27342854-3.39108312j
62
+ 0.07458428+0.49657939j 1.772696 +3.92233098j 3.19543248+0.17988554j
63
+ 0.3379041 -3.93342331j 0.93949242+2.5328611j 2.97352574+0.j ]]
64
+
65
+ One could also apply the calibration after the FFT calculation.
66
+
67
+ >>> fft = ac.RFFT(source=ts, block_size=16) # doctest: +SKIP
68
+ >>> calib = ac.Calib(source=fft) # doctest: +SKIP
69
+ >>> calib.data = 100 * np.ones(ts.num_channels * fft.num_freqs) # doctest: +SKIP
70
+ >>> print(next(calib.result(num=1))) # doctest: +SKIP
71
+ [[10.63879909+0.j 3.25957562-1.57652611j -2.27342854-3.39108312j
72
+ 0.07458428+0.49657939j 1.772696 +3.92233098j 3.19543248+0.17988554j
73
+ 0.3379041 -3.93342331j 0.93949242+2.5328611j 2.97352574+0.j ]]
74
+
75
+ Deprecated and will be removed in Acoular 25.10:
25
76
  This class serves as interface to load calibration data for the used
26
77
  microphone array. The calibration factors are stored as [Pa/unit].
27
78
  """
28
79
 
29
80
  #: Name of the .xml file to be imported.
30
- from_file = File(filter=['*.xml'], desc='name of the xml file to import')
31
-
32
- #: Basename of the .xml-file. Readonly / is set automatically.
33
- basename = Property(depends_on='from_file', desc='basename of xml file')
81
+ file = File(filter=['*.xml'], exists=True, desc='name of the xml file to import')
34
82
 
35
83
  #: Number of microphones in the calibration data,
36
- #: is set automatically / read from file.
37
- num_mics = CLong(0, desc='number of microphones in the geometry')
84
+ #: is set automatically when read from file or when data is set.
85
+ num_mics = CInt(0, desc='number of microphones in the geometry')
38
86
 
39
87
  #: Array of calibration factors,
40
- #: is set automatically / read from file.
88
+ #: is set automatically when read from file.
89
+ #: Can be set manually by specifying a NumPy array with shape (num_channels, ) if
90
+ #: :attr:`source` yields time domain signals. For frequency domain signals, the expected
91
+ #: shape is (num_channels * num_freqs).
41
92
  data = CArray(desc='calibration data')
42
93
 
94
+ #: Channels that are to be treated as invalid.
95
+ invalid_channels = List(int, desc='list of invalid channels')
96
+
97
+ #: Channel mask to serve as an index for all valid channels, is set automatically.
98
+ channels = Property(depends_on=['invalid_channels', 'num_mics'], desc='channel mask')
99
+
43
100
  # Internal identifier
44
- digest = Property(depends_on=['basename'])
101
+ digest = Property(depends_on=['source.digest', 'data'])
102
+
103
+ @on_trait_change('data')
104
+ def set_num_mics(self):
105
+ self.num_mics = self.data.shape[0]
45
106
 
46
107
  @cached_property
47
- def _get_digest(self):
48
- return digest(self)
108
+ def _get_channels(self):
109
+ if len(self.invalid_channels) == 0:
110
+ return slice(0, None, None)
111
+ allr = [i for i in range(self.num_mics) if i not in self.invalid_channels]
112
+ return array(allr)
49
113
 
50
114
  @cached_property
51
- def _get_basename(self):
52
- if not path.isfile(self.from_file):
53
- return ''
54
- return path.splitext(path.basename(self.from_file))[0]
115
+ def _get_digest(self):
116
+ return digest(self)
55
117
 
56
- @on_trait_change('basename')
118
+ @on_trait_change('file')
57
119
  def import_data(self):
58
120
  """Loads the calibration data from `*.xml` file ."""
59
- if not path.isfile(self.from_file):
60
- # empty calibration
61
- if self.basename == '':
62
- self.data = None
63
- self.num_mics = 0
64
- # no file there
65
- else:
66
- self.data = array([1.0], 'd')
67
- self.num_mics = 1
68
- return
69
- import xml.dom.minidom
70
-
71
- doc = xml.dom.minidom.parse(self.from_file)
121
+ doc = xml.dom.minidom.parse(self.file)
72
122
  names = []
73
123
  data = []
74
124
  for element in doc.getElementsByTagName('pos'):
@@ -76,3 +126,48 @@ class Calib(HasPrivateTraits):
76
126
  data.append(float(element.getAttribute('factor')))
77
127
  self.data = array(data, 'd')
78
128
  self.num_mics = self.data.shape[0]
129
+
130
+ def __validate_data(self):
131
+ """Validates the calibration data."""
132
+ if self.data is None:
133
+ msg = 'No calibration data available.'
134
+ raise ValueError(msg)
135
+ if self.source is None:
136
+ msg = 'No source data available.'
137
+ raise ValueError(msg)
138
+ tobj = self.source
139
+ while isinstance(tobj, ac.InOut):
140
+ tobj = tobj.source
141
+ if isinstance(tobj, ac.SamplesGenerator) and (self.data[self.channels].shape[0] != tobj.num_channels):
142
+ msg = f'calibration data shape {self.data[self.channels].shape[0]} does not match \
143
+ source data shape {tobj.num_channels}'
144
+ raise ValueError(msg)
145
+ if isinstance(tobj, ac.SpectraGenerator) and (
146
+ self.data[self.channels].shape[0] != tobj.num_channels * tobj.num_freqs
147
+ ):
148
+ msg = f'calibration data shape {self.data[self.channels].shape[0]} does not match \
149
+ source data shape {tobj.num_channels * tobj.num_freqs}'
150
+ raise ValueError(msg)
151
+
152
+ def result(self, num):
153
+ """Python generator that processes the source data and yields the time-signal block-wise.
154
+
155
+ This method needs to be implemented by the derived classes.
156
+
157
+ Parameters
158
+ ----------
159
+ num : int
160
+ This parameter defines the size of the blocks to be yielded
161
+ (i.e. the number of samples per block)
162
+
163
+ Yields
164
+ ------
165
+ numpy.ndarray
166
+ Two-dimensional output data block of shape (num, sourcechannels)
167
+ where sourcechannels is num_channels if the source data is in the time domain
168
+ or sourcechannels is num_channels*num_freqs if the source data is in the frequency
169
+ domain.
170
+ """
171
+ self.__validate_data()
172
+ for block in self.source.result(num):
173
+ yield block * self.data[self.channels][newaxis]
acoular/configuration.py CHANGED
@@ -15,6 +15,8 @@ import sys
15
15
  from os import environ, mkdir, path
16
16
  from warnings import warn
17
17
 
18
+ # WARNING: DO NOT ADD ANY IMPORTS HERE THAT MIGHT IMPORT NUMPY
19
+
18
20
  # When numpy is using OpenBLAS then it runs with OPENBLAS_NUM_THREADS which may lead to
19
21
  # overcommittment when called from within numba jitted function that run on
20
22
  # NUMBA_NUM_THREADS. If overcommitted, things get extremely! slow. Therefore we make an
@@ -50,11 +52,11 @@ if 'numpy' in sys.modules:
50
52
  stacklevel=2,
51
53
  )
52
54
  else:
53
- # numpy is not loaded
55
+ # numpy is not loaded, make sure that OpenBLAS runs single threaded
54
56
  environ['OPENBLAS_NUM_THREADS'] = '1'
55
57
 
56
58
  # this loads numpy, so we have to defer loading until OpenBLAS check is done
57
- from traits.api import Bool, Enum, HasStrictTraits, Property, Str, Trait, cached_property
59
+ from traits.api import Bool, Enum, HasStrictTraits, File, Property, cached_property # noqa: I001
58
60
 
59
61
 
60
62
  class Config(HasStrictTraits):
@@ -90,11 +92,11 @@ class Config(HasStrictTraits):
90
92
  #: * 'overwrite': Acoular classes replace existing cachefile content with new data.
91
93
  global_caching = Property()
92
94
 
93
- _global_caching = Trait('individual', 'all', 'none', 'readonly', 'overwrite')
95
+ _global_caching = Enum('individual', 'all', 'none', 'readonly', 'overwrite')
94
96
 
95
97
  #: Flag that globally defines package used to read and write .h5 files
96
- #: defaults to 'pytables'. It is also possible to set it to 'tables', which is an alias for 'pytables'.
97
- #: If 'pytables' can not be imported, 'h5py' is used.
98
+ #: defaults to 'pytables'. It is also possible to set it to 'tables', which is an alias for
99
+ #: 'pytables'. If pytables cannot be imported, 'h5py' is used.
98
100
  h5library = Property()
99
101
 
100
102
  _h5library = Enum('pytables', 'tables', 'h5py')
@@ -104,14 +106,14 @@ class Config(HasStrictTraits):
104
106
  #: it will be created. :attr:`cache_dir` defaults to current session path.
105
107
  cache_dir = Property()
106
108
 
107
- _cache_dir = Str('')
109
+ _cache_dir = File()
108
110
 
109
111
  #: Defines the working directory containing data files. Used only by
110
112
  #: :class:`~acoular.tprocess.WriteH5` class.
111
113
  #: Defaults to current session path.
112
114
  td_dir = Property()
113
115
 
114
- _td_dir = Str(path.curdir)
116
+ _td_dir = File(path.curdir)
115
117
 
116
118
  #: Boolean Flag that determines whether user has access to traitsui features.
117
119
  #: Defaults to False.
@@ -232,8 +234,8 @@ by :attr:`h5library`:
232
234
  Some Acoular classes support GUI elements for usage with tools from the TraitsUI package.
233
235
  If desired, this package has to be installed manually, as it is not a prerequisite for
234
236
  installing Acoular.
235
- To enable the functionality, the flag attribute :attr:`use_traitsui` has to be set to True (default: False).
236
- Note: this is independent from the GUI tools implemented in the spectAcoular package.
237
+ To enable the functionality, the flag attribute :attr:`use_traitsui` has to be set to True (default:
238
+ False). Note: this is independent from the GUI tools implemented in the spectAcoular package.
237
239
 
238
240
 
239
241
  Example:
acoular/demo/__init__.py CHANGED
@@ -11,3 +11,4 @@
11
11
  """
12
12
 
13
13
  from . import acoular_demo
14
+ from .acoular_demo import create_three_sources
@@ -32,6 +32,29 @@ Source Location RMS
32
32
  """
33
33
 
34
34
 
35
+ def create_three_sources(mg, h5savefile='three_sources.h5'):
36
+ """Create three noise sources and return them as Mixer."""
37
+ import acoular as ac
38
+
39
+ # set up the parameters
40
+
41
+ sfreq = 51200
42
+ duration = 1
43
+ nsamples = duration * sfreq
44
+
45
+ n1 = ac.WNoiseGenerator(sample_freq=sfreq, num_samples=nsamples, seed=1)
46
+ n2 = ac.WNoiseGenerator(sample_freq=sfreq, num_samples=nsamples, seed=2, rms=0.7)
47
+ n3 = ac.WNoiseGenerator(sample_freq=sfreq, num_samples=nsamples, seed=3, rms=0.5)
48
+ p1 = ac.PointSource(signal=n1, mics=mg, loc=(-0.1, -0.1, -0.3))
49
+ p2 = ac.PointSource(signal=n2, mics=mg, loc=(0.15, 0, -0.3))
50
+ p3 = ac.PointSource(signal=n3, mics=mg, loc=(0, 0.1, -0.3))
51
+ pa = ac.Mixer(source=p1, sources=[p2, p3])
52
+ if h5savefile:
53
+ wh5 = ac.WriteH5(source=pa, file=h5savefile)
54
+ wh5.save()
55
+ return pa
56
+
57
+
35
58
  def run():
36
59
  """Run the Acoular demo."""
37
60
  from pathlib import Path
@@ -40,30 +63,20 @@ def run():
40
63
 
41
64
  ac.config.global_caching = 'none'
42
65
 
43
- # set up the parameters
44
- sfreq = 51200
45
- duration = 1
46
- nsamples = duration * sfreq
66
+ # set up microphone geometry
67
+
47
68
  micgeofile = Path(ac.__file__).parent / 'xml' / 'array_64.xml'
48
- h5savefile = 'three_sources.h5'
69
+ mg = ac.MicGeom(file=micgeofile)
49
70
 
50
71
  # generate test data, in real life this would come from an array measurement
51
- mg = ac.MicGeom(from_file=micgeofile)
52
- n1 = ac.WNoiseGenerator(sample_freq=sfreq, numsamples=nsamples, seed=1)
53
- n2 = ac.WNoiseGenerator(sample_freq=sfreq, numsamples=nsamples, seed=2, rms=0.7)
54
- n3 = ac.WNoiseGenerator(sample_freq=sfreq, numsamples=nsamples, seed=3, rms=0.5)
55
- p1 = ac.PointSource(signal=n1, mics=mg, loc=(-0.1, -0.1, 0.3))
56
- p2 = ac.PointSource(signal=n2, mics=mg, loc=(0.15, 0, 0.3))
57
- p3 = ac.PointSource(signal=n3, mics=mg, loc=(0, 0.1, 0.3))
58
- pa = ac.Mixer(source=p1, sources=[p2, p3])
59
- wh5 = ac.WriteH5(source=pa, name=h5savefile)
60
- wh5.save()
72
+
73
+ pa = create_three_sources(mg)
61
74
 
62
75
  # analyze the data and generate map
63
76
 
64
77
  ps = ac.PowerSpectra(source=pa, block_size=128, window='Hanning')
65
78
 
66
- rg = ac.RectGrid(x_min=-0.2, x_max=0.2, y_min=-0.2, y_max=0.2, z=0.3, increment=0.01)
79
+ rg = ac.RectGrid(x_min=-0.2, x_max=0.2, y_min=-0.2, y_max=0.2, z=-0.3, increment=0.01)
67
80
  st = ac.SteeringVector(grid=rg, mics=mg)
68
81
 
69
82
  bb = ac.BeamformerBase(freq_data=ps, steer=st)
@@ -71,7 +84,7 @@ def run():
71
84
  spl = ac.L_p(pm)
72
85
 
73
86
  if ac.config.have_matplotlib:
74
- from pylab import axis, colorbar, figure, imshow, plot, show
87
+ from matplotlib.pyplot import axis, colorbar, figure, imshow, plot, show
75
88
 
76
89
  # show map
77
90
  imshow(spl.T, origin='lower', vmin=spl.max() - 10, extent=rg.extend(), interpolation='bicubic')
@@ -79,7 +92,7 @@ def run():
79
92
 
80
93
  # plot microphone geometry
81
94
  figure(2)
82
- plot(mg.mpos[0], mg.mpos[1], 'o')
95
+ plot(mg.pos[0], mg.pos[1], 'o')
83
96
  axis('equal')
84
97
 
85
98
  show()
acoular/deprecation.py ADDED
@@ -0,0 +1,85 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Copyright (c) Acoular Development Team.
3
+ # ------------------------------------------------------------------------------
4
+
5
+ from warnings import warn
6
+
7
+ from traits.api import Property
8
+
9
+
10
+ def deprecated_alias(old2new, read_only=False, removal_version=''):
11
+ """Decorator function for deprecating renamed class traits.
12
+ Replaced traits should no longer be part of the class definition
13
+ and only mentioned in this decorator's parameter list.
14
+ The replacement trait has to be defined in the updated class and
15
+ will be mapped to the deprecated name via this function.
16
+
17
+ Parameters
18
+ ----------
19
+ old2new: dict
20
+ Dictionary containing the deprecated trait names as keys and
21
+ their new names as values.
22
+ read_only: bool or list
23
+ If True, all deprecated traits will be "read only".
24
+ If False (default), all deprecated traits can be read and set.
25
+ If list, traits whose names are in list will be "read only".
26
+ removal_version: string or dict
27
+ Adds the acoular version of trait removal to the deprecation message.
28
+ If a non-empty string, it will be interpreted as the acoular version when
29
+ all traits in the list will be deprecated.
30
+ If a dictionary, the keys are expected to be trait names and the values
31
+ are the removal version as strings.
32
+ """
33
+
34
+ def decorator(cls):
35
+ """Decorator function that gets applied to the class `cls`."""
36
+
37
+ def _alias_accessor_factory(old, new, trait_read_only=False, trait_removal_version=''):
38
+ """Function to define setter and getter routines for alias property trait."""
39
+ if trait_removal_version:
40
+ trait_removal_version = f' (will be removed in version {trait_removal_version})'
41
+ msg = f"Deprecated use of '{old}' trait{trait_removal_version}. Please use the '{new}' trait instead."
42
+
43
+ def getter(cls):
44
+ warn(msg, DeprecationWarning, stacklevel=2)
45
+ return getattr(cls, new)
46
+
47
+ if trait_read_only:
48
+ return (getter,)
49
+
50
+ def setter(cls, value):
51
+ warn(msg, DeprecationWarning, stacklevel=2)
52
+ setattr(cls, new, value)
53
+
54
+ return (getter, setter)
55
+
56
+ # Add deprecated traits to class traits and link them to
57
+ # the new ones with a deprecation warning.
58
+ for old, new in old2new.items():
59
+ # Set "read only" status depending on global read_only argument
60
+ current_read_only = (old in read_only) if isinstance(read_only, list) else read_only
61
+ # If version for trait removal is given, pass info to accessors
62
+ if isinstance(removal_version, str) and (len(removal_version) > 0):
63
+ current_removal_version = removal_version
64
+ elif isinstance(removal_version, dict) and (old in removal_version):
65
+ current_removal_version = removal_version[old]
66
+ else:
67
+ current_removal_version = ''
68
+ # Define Trait Property type
69
+ trait_type = Property(*_alias_accessor_factory(old, new, current_read_only, current_removal_version))
70
+
71
+ # If the new trait exists, set or update alias
72
+ if new in cls.class_traits():
73
+ if old not in cls.class_traits():
74
+ cls.add_class_trait(old, trait_type)
75
+ else:
76
+ # Access class dictionary to change trait
77
+ cls.__dict__['__class_traits__'][old] = trait_type
78
+
79
+ else:
80
+ error_msg = f"Cannot create trait '{old}' because its replacement trait '{new}' does not exist."
81
+ raise ValueError(error_msg)
82
+
83
+ return cls
84
+
85
+ return decorator