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/__init__.py +5 -2
- acoular/aiaa/__init__.py +12 -0
- acoular/{tools → aiaa}/aiaa.py +23 -28
- acoular/base.py +75 -55
- acoular/calib.py +129 -34
- acoular/configuration.py +11 -9
- acoular/demo/__init__.py +1 -0
- acoular/demo/acoular_demo.py +31 -18
- acoular/deprecation.py +85 -0
- acoular/environments.py +481 -229
- acoular/fastFuncs.py +90 -84
- acoular/fbeamform.py +203 -411
- acoular/fprocess.py +233 -123
- acoular/grids.py +793 -424
- acoular/h5cache.py +29 -40
- acoular/h5files.py +2 -6
- acoular/microphones.py +197 -74
- acoular/process.py +660 -149
- acoular/sdinput.py +23 -20
- acoular/signals.py +461 -159
- acoular/sources.py +1311 -489
- acoular/spectra.py +328 -352
- acoular/tbeamform.py +79 -202
- acoular/tfastfuncs.py +21 -21
- acoular/tools/__init__.py +2 -8
- acoular/tools/helpers.py +216 -2
- acoular/tools/metrics.py +4 -4
- acoular/tools/utils.py +106 -200
- acoular/tprocess.py +348 -309
- acoular/traitsviews.py +10 -10
- acoular/trajectory.py +126 -53
- acoular/version.py +2 -2
- {acoular-24.10.dist-info → acoular-25.3.dist-info}/METADATA +39 -17
- acoular-25.3.dist-info/RECORD +56 -0
- {acoular-24.10.dist-info → acoular-25.3.dist-info}/WHEEL +1 -1
- acoular-24.10.dist-info/RECORD +0 -54
- {acoular-24.10.dist-info → acoular-25.3.dist-info}/licenses/AUTHORS.rst +0 -0
- {acoular-24.10.dist-info → acoular-25.3.dist-info}/licenses/LICENSE +0 -0
acoular/h5cache.py
CHANGED
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
# imports from other packages
|
|
6
6
|
import gc
|
|
7
|
-
from
|
|
7
|
+
from pathlib import Path
|
|
8
8
|
from weakref import WeakValueDictionary
|
|
9
9
|
|
|
10
|
-
from traits.api import Bool, Delegate, Dict,
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
|
95
|
-
if
|
|
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(
|
|
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
|
-
|
|
111
|
-
self.open_files[filename] =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
|
15
|
-
from
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
81
|
+
The microphones along the diagonal can be removed by setting their indices in the
|
|
82
|
+
:attr:`invalid_channels` attribute:
|
|
37
83
|
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
#:
|
|
55
|
-
#:
|
|
56
|
-
|
|
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
|
-
#:
|
|
60
|
-
#:
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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.
|
|
172
|
+
return self.pos.shape[-1]
|
|
86
173
|
|
|
87
174
|
@cached_property
|
|
88
175
|
def _get_center(self):
|
|
89
|
-
if self.
|
|
90
|
-
center = average(self.
|
|
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.
|
|
99
|
-
return cdist(self.
|
|
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('
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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.
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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.
|
|
258
|
+
for i in range(self.pos.shape[-1]):
|
|
136
259
|
f.write(
|
|
137
|
-
f' <pos Name="Point {i+1}" x="{self.
|
|
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>')
|