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/h5cache.py CHANGED
@@ -4,16 +4,16 @@
4
4
 
5
5
  # imports from other packages
6
6
  import gc
7
- from os import listdir, path
7
+ from pathlib import Path
8
8
  from weakref import WeakValueDictionary
9
9
 
10
- from traits.api import Bool, Delegate, Dict, HasPrivateTraits, Instance
10
+ from traits.api import Bool, Delegate, Dict, HasStrictTraits, Instance
11
11
 
12
12
  from .configuration import Config, config
13
13
  from .h5files import _get_cachefile_class
14
14
 
15
15
 
16
- class HDF5Cache(HasPrivateTraits):
16
+ class HDF5Cache(HasStrictTraits):
17
17
  """Cache class that handles opening and closing 'tables.File' objects."""
18
18
 
19
19
  config = Instance(Config)
@@ -24,26 +24,16 @@ class HDF5Cache(HasPrivateTraits):
24
24
 
25
25
  open_files = WeakValueDictionary()
26
26
 
27
- open_file_reference = Dict()
27
+ open_file_reference = Dict(key_trait=Path, value_trait=int)
28
28
 
29
29
  def _idle_if_busy(self):
30
30
  while self.busy:
31
31
  pass
32
32
 
33
- def open_cachefile(self, filename, mode):
34
- file = _get_cachefile_class()
35
- return file(path.join(self.cache_dir, filename), mode)
36
-
37
33
  def close_cachefile(self, cachefile):
38
- self.open_file_reference.pop(get_basename(cachefile))
34
+ self.open_file_reference.pop(Path(cachefile.filename))
39
35
  cachefile.close()
40
36
 
41
- def get_filename(self, file):
42
- file_class = _get_cachefile_class()
43
- if isinstance(file, file_class):
44
- return get_basename(file)
45
- return 0
46
-
47
37
  def get_open_cachefiles(self):
48
38
  try:
49
39
  return self.open_files.itervalues()
@@ -53,7 +43,6 @@ class HDF5Cache(HasPrivateTraits):
53
43
  def close_unreferenced_cachefiles(self):
54
44
  for cachefile in self.get_open_cachefiles():
55
45
  if not self.is_reference_existent(cachefile):
56
- # print("close unreferenced File:",get_basename(cachefile))
57
46
  self.close_cachefile(cachefile)
58
47
 
59
48
  def is_reference_existent(self, file):
@@ -69,46 +58,50 @@ class HDF5Cache(HasPrivateTraits):
69
58
  break
70
59
  return exist_flag
71
60
 
72
- def is_cachefile_existent(self, filename):
73
- if filename in listdir(self.cache_dir):
74
- return True
75
- return False
76
-
77
61
  def _increase_file_reference_counter(self, filename):
78
62
  self.open_file_reference[filename] = self.open_file_reference.get(filename, 0) + 1
79
63
 
80
64
  def _decrease_file_reference_counter(self, filename):
81
65
  self.open_file_reference[filename] = self.open_file_reference[filename] - 1
82
66
 
67
+ def get_cache_directories(self):
68
+ """Return a list of all used cache directories (if multiple paths exist)."""
69
+ return list({str(k.parent) for k in self.open_file_reference})
70
+
83
71
  def _print_open_files(self):
84
- print(list(self.open_file_reference.items()))
72
+ """Prints open cache files and the number of objects referencing a cache file.
73
+
74
+ If multiple cache files are open at different paths, the full path is printed.
75
+ Otherwise, only the filename is logged.
76
+ """
77
+ if len(self.open_file_reference.values()) > 1:
78
+ print(list({str(k): v for k, v in self.open_file_reference.items()}.items()))
79
+ else:
80
+ print(list({str(k.name): v for k, v in self.open_file_reference.items()}.items()))
85
81
 
86
82
  def get_cache_file(self, obj, basename, mode='a'):
87
83
  """Returns pytables .h5 file to h5f trait of calling object for caching."""
88
84
  self._idle_if_busy() #
89
85
  self.busy = True
86
+ file_cls = _get_cachefile_class()
87
+ filename = (Path(self.cache_dir) / (basename + '_cache.h5')).resolve()
90
88
 
91
- filename = basename + '_cache.h5'
92
- obj_filename = self.get_filename(obj.h5f)
89
+ if config.global_caching == 'readonly' and not filename.exists():
90
+ obj.h5f = None
91
+ self.busy = False
92
+ return # cachefile is not created in readonly mode
93
93
 
94
- if obj_filename:
95
- if obj_filename == filename:
94
+ if isinstance(obj.h5f, file_cls):
95
+ if Path(obj.h5f.filename).resolve() == filename:
96
96
  self.busy = False
97
97
  return
98
- self._decrease_file_reference_counter(obj_filename)
98
+ self._decrease_file_reference_counter(obj.h5f.filename)
99
99
 
100
100
  if filename not in self.open_files: # or tables.file._open_files.filenames
101
- if config.global_caching == 'readonly' and not self.is_cachefile_existent(
102
- filename,
103
- ): # condition ensures that cachefile is not created in readonly mode
104
- obj.h5f = None
105
- self.busy = False
106
- # self._print_open_files()
107
- return
108
101
  if config.global_caching == 'readonly':
109
102
  mode = 'r'
110
- f = self.open_cachefile(filename, mode)
111
- self.open_files[filename] = f
103
+ file = file_cls(filename, mode)
104
+ self.open_files[filename] = file
112
105
 
113
106
  obj.h5f = self.open_files[filename]
114
107
  self._increase_file_reference_counter(filename)
@@ -121,7 +114,3 @@ class HDF5Cache(HasPrivateTraits):
121
114
 
122
115
 
123
116
  H5cache = HDF5Cache(config=config)
124
-
125
-
126
- def get_basename(file):
127
- return path.basename(file.filename)
acoular/h5files.py CHANGED
@@ -111,9 +111,7 @@ if config.have_tables:
111
111
  def is_cached(self, nodename, group=None):
112
112
  if not group:
113
113
  group = self.root
114
- if nodename in group:
115
- return True
116
- return False
114
+ return nodename in group
117
115
 
118
116
  def create_compressible_array(self, nodename, shape, precision, group=None):
119
117
  if not group:
@@ -190,9 +188,7 @@ if config.have_h5py:
190
188
  def is_cached(self, nodename, group=None):
191
189
  if not group:
192
190
  group = '/'
193
- if group + nodename in self:
194
- return True
195
- return False
191
+ return group + nodename in self
196
192
 
197
193
  def create_compressible_array(self, nodename, shape, precision, group=None):
198
194
  in_file_path = self._get_in_file_path(nodename, group)
acoular/microphones.py CHANGED
@@ -1,93 +1,180 @@
1
1
  # ------------------------------------------------------------------------------
2
2
  # Copyright (c) Acoular Development Team.
3
3
  # ------------------------------------------------------------------------------
4
- """Implements support for array microphone arrangements.
4
+ """
5
+ Implements support for array microphone arrangements.
5
6
 
6
7
  .. autosummary::
7
8
  :toctree: generated/
8
9
 
9
10
  MicGeom
10
-
11
11
  """
12
12
 
13
13
  # imports from other packages
14
- import errno
15
- from os import path, strerror
14
+ import xml.dom.minidom
15
+ from pathlib import Path
16
16
 
17
17
  from numpy import array, average
18
18
  from scipy.spatial.distance import cdist
19
- from traits.api import Bool, CArray, File, HasPrivateTraits, ListInt, Property, cached_property, on_trait_change
19
+ from traits.api import (
20
+ CArray,
21
+ File,
22
+ HasStrictTraits,
23
+ List,
24
+ Property,
25
+ cached_property,
26
+ on_trait_change,
27
+ )
20
28
 
29
+ # acoular imports
30
+ from .deprecation import deprecated_alias
21
31
  from .internal import digest
22
32
 
23
33
 
24
- class MicGeom(HasPrivateTraits):
25
- """Provides the geometric arrangement of microphones in the array.
26
-
27
- The geometric arrangement of microphones is read in from an
28
- xml-source with element tag names `pos` and attributes Name, `x`, `y` and `z`.
29
- Can also be used with programmatically generated arrangements.
34
+ @deprecated_alias({'mpos_tot': 'pos_total', 'mpos': 'pos', 'from_file': 'file'}, read_only=['mpos'])
35
+ class MicGeom(HasStrictTraits):
30
36
  """
37
+ Provide the geometric arrangement of microphones in an array.
38
+
39
+ This class allows you to define, import, and manage the spatial positions of microphones in a
40
+ microphone array. The positions can be read from an XML file or set programmatically. Invalid
41
+ microphones can be excluded by specifying their indices via :attr:`invalid_channels`.
42
+
43
+ Notes
44
+ -----
45
+ - The microphone geometry as in :attr:`total_pos` is automatically changed if the :attr:`file`
46
+ attribute is updated.
47
+ - Small numerical values in the computed :attr:`center` are set to zero for numerical stability.
48
+
49
+ Examples
50
+ --------
51
+ To set a microphone geomerty for ``n`` programmatically, first a ``(3,n)`` array is needed. In
52
+ this case we'll use ``n=9`` and generate an array containing the positional data.
53
+
54
+ >>> import numpy as np
55
+ >>>
56
+ >>> # Generate a (3,3) grid of points in the x-y plane
57
+ >>> x = np.linspace(-1, 1, 3) # Generate 3 points for x, from -1 to 1
58
+ >>> y = np.linspace(-1, 1, 3) # Generate 3 points for y, from -1 to 1
59
+ >>>
60
+ >>> # Create a meshgrid for 3D coordinates, with z=0 for all points
61
+ >>> X, Y = np.meshgrid(x, y)
62
+ >>> Z = np.zeros_like(X) # Set all z-values to 0
63
+ >>>
64
+ >>> # Stack the coordinates into a single (3,9) array
65
+ >>> points = np.vstack([X.ravel(), Y.ravel(), Z.ravel()])
66
+ >>> points
67
+ array([[-1., 0., 1., -1., 0., 1., -1., 0., 1.],
68
+ [-1., -1., -1., 0., 0., 0., 1., 1., 1.],
69
+ [ 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
70
+
71
+ Now, to implement this array as a microphone geomertry, create a :class:`MicGeom` object and
72
+ assign the array to it the by using the :attr:`pos_total` attribute:
31
73
 
32
- #: Name of the .xml-file from wich to read the data.
33
- from_file = File(filter=['*.xml'], desc='name of the xml file to import')
74
+ >>> from acoular import MicGeom
75
+ >>> mg = MicGeom(pos_total=points)
76
+ >>> mg.pos
77
+ array([[-1., 0., 1., -1., 0., 1., -1., 0., 1.],
78
+ [-1., -1., -1., 0., 0., 0., 1., 1., 1.],
79
+ [ 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
34
80
 
35
- #: Validate mic geom from file
36
- validate_file = Bool(True, desc='Validate mic geom from file')
81
+ The microphones along the diagonal can be removed by setting their indices in the
82
+ :attr:`invalid_channels` attribute:
37
83
 
38
- #: Basename of the .xml-file, without the extension; is set automatically / readonly.
39
- basename = Property(depends_on='from_file', desc='basename of xml file')
84
+ >>> mg.invalid_channels = [0, 4, 9]
85
+ >>> mg.pos
86
+ array([[ 0., 1., -1., 1., -1., 0., 1.],
87
+ [-1., -1., 0., 0., 1., 1., 1.],
88
+ [ 0., 0., 0., 0., 0., 0., 0.]])
40
89
 
41
- #: List that gives the indices of channels that should not be considered.
42
- #: Defaults to a blank list.
43
- invalid_channels = ListInt(desc='list of invalid channels')
90
+ But they will still be included in :attr:`pos_total`:
44
91
 
45
- #: Number of microphones in the array; readonly.
46
- num_mics = Property(depends_on=['mpos'], desc='number of microphones in the geometry')
92
+ >>> mg.pos_total
93
+ array([[-1., 0., 1., -1., 0., 1., -1., 0., 1.],
94
+ [-1., -1., -1., 0., 0., 0., 1., 1., 1.],
95
+ [ 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
47
96
 
48
- #: Center of the array (arithmetic mean of all used array positions); readonly.
49
- center = Property(depends_on=['mpos'], desc='array center')
97
+ To export this microphone geometry, use the :meth:`export_mpos` method. Note that the
98
+ microphones marked as invalid in :attr:`invalid_channels` will not be exported.
50
99
 
51
- #: Aperture of the array (greatest extent between two microphones); readonly.
52
- aperture = Property(depends_on=['mpos'], desc='array aperture')
100
+ >>> mg.export_mpos('micgeom.xml') # doctest: +SKIP
101
+
102
+ The newly generated ``micgeom.xml`` file looks like this:
103
+
104
+ .. code-block:: xml
105
+
106
+ <?xml version="1.1" encoding="utf-8"?><MicArray name="micgeom">
107
+ <pos Name="Point 1" x="0.0" y="-1.0" z="0.0"/>
108
+ <pos Name="Point 2" x="1.0" y="-1.0" z="0.0"/>
109
+ <pos Name="Point 3" x="-1.0" y="0.0" z="0.0"/>
110
+ <pos Name="Point 4" x="1.0" y="0.0" z="0.0"/>
111
+ <pos Name="Point 5" x="-1.0" y="1.0" z="0.0"/>
112
+ <pos Name="Point 6" x="0.0" y="1.0" z="0.0"/>
113
+ <pos Name="Point 7" x="1.0" y="1.0" z="0.0"/>
114
+ </MicArray>
115
+
116
+ Note that when importing a microphone geometry, the XML file needs to look similar to this one:
117
+ There must be ``<pos>`` elements with ``Name``, ``x``, ``y``, and ``z`` attributes.
118
+
119
+ To load this same file as a new :class:`MicGeom` object, the ``micgeom.xml`` file can be
120
+ assigned to the :attr:`file` attribute:
121
+
122
+ >>> new_mg = MicGeom(file='micgeom.xml') # doctest: +SKIP
123
+ >>> new_mg.pos # doctest: +SKIP
124
+ array([[ 0., 1., -1., 1., -1., 0., 1.],
125
+ [-1., -1., 0., 0., 1., 1., 1.],
126
+ [ 0., 0., 0., 0., 0., 0., 0.]])
127
+ """
53
128
 
54
- #: Positions as (3, :attr:`num_mics`) array of floats, may include also invalid
55
- #: microphones (if any). Set either automatically on change of the
56
- #: :attr:`from_file` argument or explicitely by assigning an array of floats.
57
- mpos_tot = CArray(dtype=float, desc='x, y, z position of all microphones')
129
+ #: Path to the XML file containing microphone positions. The XML file should have elements with
130
+ #: 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')
58
132
 
59
- #: Positions as (3, :attr:`num_mics`) array of floats, without invalid
60
- #: microphones; readonly.
61
- mpos = Property(depends_on=['mpos_tot', 'invalid_channels'], desc='x, y, z position of microphones')
133
+ #: Array containing the ``x, y, z`` positions of all microphones, including invalid ones, shape
134
+ #: ``(3,`` :attr:`num_mics` ``)``. This is set automatically when :attr:`file` changes or
135
+ #: explicitly by assigning an array of floats.
136
+ pos_total = CArray(dtype=float, shape=(3, None), desc='x, y, z position of all microphones')
62
137
 
63
- # internal identifier
64
- digest = Property(depends_on=['mpos'])
138
+ #: Array containing the ``x, y, z`` positions of valid microphones (i.e., excluding those in
139
+ #: :attr:`invalid_channels`), shape ``(3,`` :attr:`num_mics` ``)``. (read-only)
140
+ pos = Property(depends_on=['pos_total', 'invalid_channels'], desc='x, y, z position of used microphones')
141
+
142
+ #: List of indices indicating microphones to be excluded from calculations and results.
143
+ #: Default is ``[]``.
144
+ invalid_channels = List(int, desc='list of invalid channels')
145
+
146
+ #: Number of valid microphones in the array. (read-only)
147
+ num_mics = Property(depends_on=['pos'], desc='number of microphones in the geometry')
148
+
149
+ #: The geometric center of the array, calculated as the arithmetic mean of the positions of all
150
+ #: valid microphones. (read-only)
151
+ center = Property(depends_on=['pos'], desc='array center')
152
+
153
+ #: The maximum distance between any two valid microphones in the array. (read-only)
154
+ aperture = Property(depends_on=['pos'], desc='array aperture')
155
+
156
+ #: A unique identifier for the geometry, based on its properties. (read-only)
157
+ digest = Property(depends_on=['pos'])
65
158
 
66
159
  @cached_property
67
160
  def _get_digest(self):
68
161
  return digest(self)
69
162
 
70
163
  @cached_property
71
- def _get_basename(self):
72
- return path.splitext(path.basename(self.from_file))[0]
73
-
74
- @cached_property
75
- def _get_mpos(self):
76
- if self.validate_file:
77
- if len(self.invalid_channels) == 0:
78
- return self.mpos_tot
79
- allr = [i for i in range(self.mpos_tot.shape[-1]) if i not in self.invalid_channels]
80
- return self.mpos_tot[:, array(allr)]
81
- raise FileNotFoundError(errno.ENOENT, strerror(errno.ENOENT), self.from_file)
164
+ def _get_pos(self):
165
+ if len(self.invalid_channels) == 0:
166
+ return self.pos_total
167
+ allr = [i for i in range(self.pos_total.shape[-1]) if i not in self.invalid_channels]
168
+ return self.pos_total[:, array(allr)]
82
169
 
83
170
  @cached_property
84
171
  def _get_num_mics(self):
85
- return self.mpos.shape[-1]
172
+ return self.pos.shape[-1]
86
173
 
87
174
  @cached_property
88
175
  def _get_center(self):
89
- if self.mpos.any():
90
- center = average(self.mpos, axis=1)
176
+ if self.pos.any():
177
+ center = average(self.pos, axis=1)
91
178
  # set very small values to zero
92
179
  center[abs(center) < 1e-16] = 0.0
93
180
  return center
@@ -95,45 +182,81 @@ class MicGeom(HasPrivateTraits):
95
182
 
96
183
  @cached_property
97
184
  def _get_aperture(self):
98
- if self.mpos.any():
99
- return cdist(self.mpos.T, self.mpos.T).max()
185
+ if self.pos.any():
186
+ return cdist(self.pos.T, self.pos.T).max()
100
187
  return None
101
188
 
102
- @on_trait_change('basename')
103
- def import_mpos(self):
104
- """Import the microphone positions from .xml file.
105
- Called when :attr:`basename` changes.
106
- """
107
- if not path.isfile(self.from_file):
108
- # no file there
109
- self.mpos_tot = array([], 'd')
110
- # raise error: File not found on _get functions
111
- self.validate_file = False
112
-
113
- import xml.dom.minidom
114
-
115
- doc = xml.dom.minidom.parse(self.from_file)
189
+ @on_trait_change('file')
190
+ def _import_mpos(self):
191
+ # Import the microphone positions from an XML file.
192
+ #
193
+ # This method parses the XML file specified in :attr:`file` and extracts the ``x``, ``y``,
194
+ # and ``z`` positions of microphones. The data is stored in :attr:`pos_total` attribute as
195
+ # an array of shape ``(3,`` :attr:`num_mics` ``)``.
196
+ #
197
+ # This method is called when :attr:`file` changes.
198
+ #
199
+ # Raises
200
+ # ------
201
+ # xml.parsers.expat.ExpatError
202
+ # If the XML file is malformed or cannot be parsed.
203
+ # ValueError
204
+ # If the attributes ``x``, ``y``, or ``z`` in any ``<pos>`` element are missing or
205
+ # cannot be converted to a float.
206
+ #
207
+ # Examples
208
+ # --------
209
+ # The microphone geometry changes by changing the :attr:`file` attribute.
210
+ #
211
+ # >>> from acoular import MicGeom # doctest: +SKIP
212
+ # >>> mg = MicGeom(file='/path/to/geom1.xml') # doctest: +SKIP
213
+ # >>> mg.center # doctest: +SKIP
214
+ # array([-0.25, 0. , 0.25]) # doctest: +SKIP
215
+ # >>> mg.file = '/path/to/geom2.xml' # doctest: +SKIP
216
+ # >>> mg.center # doctest: +SKIP
217
+ # array([0. , 0.33333333, 0.66666667]) # doctest: +SKIP
218
+ doc = xml.dom.minidom.parse(self.file)
116
219
  names = []
117
220
  xyz = []
118
221
  for el in doc.getElementsByTagName('pos'):
119
222
  names.append(el.getAttribute('Name'))
120
223
  xyz.append([float(el.getAttribute(a)) for a in 'xyz'])
121
- self.mpos_tot = array(xyz, 'd').swapaxes(0, 1)
122
- self.validate_file = True
224
+ self.pos_total = array(xyz, 'd').swapaxes(0, 1)
123
225
 
124
226
  def export_mpos(self, filename):
125
- """Export the microphone positions to .xml file.
227
+ """
228
+ Export the microphone positions to an XML file.
229
+
230
+ This method generates an XML file containing the positions of all valid microphones in the
231
+ array. Each microphone is represented by a ``<pos>`` element with ``Name``, ``x``, ``y``,
232
+ and ``z`` attributes. The generated XML is formatted to match the structure required for
233
+ importing into the :class:`MicGeom` class.
126
234
 
127
235
  Parameters
128
236
  ----------
129
- filename : str
130
- Name of the file to which the microphone positions are written.
237
+ filename : :class:`str`
238
+ The path to the file to which the microphone positions will be written. The file
239
+ extension must be ``.xml``.
240
+
241
+ Raises
242
+ ------
243
+ :obj:`OSError`
244
+ If the file cannot be written due to permissions issues or invalid file paths.
245
+
246
+ Notes
247
+ -----
248
+ - The file will be saved in UTF-8 encoding.
249
+ - The ``Name`` attribute for each microphone is set as ``"Point {i+1}"``, where ``i`` is the
250
+ index of the microphone.
251
+ - This method only exports the positions of the valid microphones (those not listed in
252
+ :attr:`invalid_channels`).
131
253
  """
132
- basename = path.splitext(path.basename(filename))[0]
133
- with open(filename, 'w') as f:
254
+ filepath = Path(filename)
255
+ basename = filepath.stem
256
+ with filepath.open('w', encoding='utf-8') as f:
134
257
  f.write(f'<?xml version="1.1" encoding="utf-8"?><MicArray name="{basename}">\n')
135
- for i in range(self.mpos.shape[-1]):
258
+ for i in range(self.pos.shape[-1]):
136
259
  f.write(
137
- f' <pos Name="Point {i+1}" x="{self.mpos[0, i]}" y="{self.mpos[1, i]}" z="{self.mpos[2, i]}"/>\n',
260
+ f' <pos Name="Point {i+1}" x="{self.pos[0, i]}" y="{self.pos[1, i]}" z="{self.pos[2, i]}"/>\n',
138
261
  )
139
262
  f.write('</MicArray>')