sofar 1.2.1__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.
- docs/Makefile +20 -0
- docs/api_reference.rst +20 -0
- docs/conf.py +167 -0
- docs/contributing.rst +1 -0
- docs/history.rst +1 -0
- docs/index.rst +4 -0
- docs/make.bat +36 -0
- docs/readme.rst +1 -0
- docs/resources/conventions.py +162 -0
- docs/resources/working_with_sofa_HRIR_lateral.png +0 -0
- docs/resources/working_with_sofa_source_horizontal.png +0 -0
- docs/resources/working_with_sofa_source_lateral.png +0 -0
- docs/sofar.rst +82 -0
- sofar/__init__.py +28 -0
- sofar/io.py +427 -0
- sofar/sofa.py +1835 -0
- sofar/sofa_conventions/VERSION +1 -0
- sofar/sofa_conventions/conventions/AnnotatedEmitterAudio_0.2.csv +46 -0
- sofar/sofa_conventions/conventions/AnnotatedEmitterAudio_0.2.json +353 -0
- sofar/sofa_conventions/conventions/AnnotatedReceiverAudio_0.2.csv +46 -0
- sofar/sofa_conventions/conventions/AnnotatedReceiverAudio_0.2.json +353 -0
- sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.csv +59 -0
- sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.json +444 -0
- sofar/sofa_conventions/conventions/FreeFieldHRIR_1.0.csv +43 -0
- sofar/sofa_conventions/conventions/FreeFieldHRIR_1.0.json +333 -0
- sofar/sofa_conventions/conventions/FreeFieldHRTF_1.0.csv +44 -0
- sofar/sofa_conventions/conventions/FreeFieldHRTF_1.0.json +340 -0
- sofar/sofa_conventions/conventions/GeneralFIR-E_2.0.csv +37 -0
- sofar/sofa_conventions/conventions/GeneralFIR-E_2.0.json +270 -0
- sofar/sofa_conventions/conventions/GeneralFIR_1.0.csv +40 -0
- sofar/sofa_conventions/conventions/GeneralFIR_1.0.json +295 -0
- sofar/sofa_conventions/conventions/GeneralSOS_1.0.csv +40 -0
- sofar/sofa_conventions/conventions/GeneralSOS_1.0.json +306 -0
- sofar/sofa_conventions/conventions/GeneralTF-E_1.0.csv +38 -0
- sofar/sofa_conventions/conventions/GeneralTF-E_1.0.json +277 -0
- sofar/sofa_conventions/conventions/GeneralTF_1.0.csv +38 -0
- sofar/sofa_conventions/conventions/GeneralTF_1.0.json +277 -0
- sofar/sofa_conventions/conventions/GeneralTF_2.0.csv +38 -0
- sofar/sofa_conventions/conventions/GeneralTF_2.0.json +277 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRIR_1.0.csv +47 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRIR_1.0.json +369 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRSOS_1.0.csv +43 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRSOS_1.0.json +349 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.csv +44 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.json +340 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldSOS_1.0.csv +43 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldSOS_1.0.json +349 -0
- sofar/sofa_conventions/conventions/SimpleHeadphoneIR_1.0.csv +51 -0
- sofar/sofa_conventions/conventions/SimpleHeadphoneIR_1.0.json +396 -0
- sofar/sofa_conventions/conventions/SingleRoomMIMOSRIR_1.0.csv +78 -0
- sofar/sofa_conventions/conventions/SingleRoomMIMOSRIR_1.0.json +601 -0
- sofar/sofa_conventions/conventions/SingleRoomSRIR_1.0.csv +78 -0
- sofar/sofa_conventions/conventions/SingleRoomSRIR_1.0.json +601 -0
- sofar/sofa_conventions/conventions/deprecated/AnnotatedEmitterAudio_0.1.csv +46 -0
- sofar/sofa_conventions/conventions/deprecated/AnnotatedEmitterAudio_0.1.json +351 -0
- sofar/sofa_conventions/conventions/deprecated/AnnotatedReceiverAudio_0.1.csv +46 -0
- sofar/sofa_conventions/conventions/deprecated/AnnotatedReceiverAudio_0.1.json +351 -0
- sofar/sofa_conventions/conventions/deprecated/FreeFieldDirectivityTF_1.0.csv +58 -0
- sofar/sofa_conventions/conventions/deprecated/FreeFieldDirectivityTF_1.0.json +437 -0
- sofar/sofa_conventions/conventions/deprecated/GeneralFIRE_1.0.csv +37 -0
- sofar/sofa_conventions/conventions/deprecated/GeneralFIRE_1.0.json +270 -0
- sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.csv +48 -0
- sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.json +376 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.csv +43 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.json +333 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.csv +44 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.json +340 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_1.0.csv +44 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_1.0.json +340 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.1.csv +51 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.1.json +396 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.csv +51 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.json +396 -0
- sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.2.csv +47 -0
- sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.2.json +360 -0
- sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.3.csv +47 -0
- sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.3.json +360 -0
- sofar/sofa_conventions/conventions/deprecated/SingleTrackedAudio_0.1.csv +47 -0
- sofar/sofa_conventions/conventions/deprecated/SingleTrackedAudio_0.1.json +366 -0
- sofar/sofa_conventions/conventions/deprecated/SingleTrackedAudio_0.2.csv +51 -0
- sofar/sofa_conventions/conventions/deprecated/SingleTrackedAudio_0.2.json +397 -0
- sofar/sofa_conventions/rules/deprecations.json +13 -0
- sofar/sofa_conventions/rules/rules.json +819 -0
- sofar/sofa_conventions/rules/unit_aliases.json +11 -0
- sofar/sofa_conventions/rules/upgrade.json +226 -0
- sofar/sofa_conventions/write_upgrade_rules.py +139 -0
- sofar/sofa_conventions/write_verification_data.py +313 -0
- sofar/sofa_conventions/write_verification_rules.py +356 -0
- sofar/sofastream.py +301 -0
- sofar/update_conventions.py +449 -0
- sofar/utils.py +316 -0
- sofar-1.2.1.dist-info/LICENSE +22 -0
- sofar-1.2.1.dist-info/METADATA +136 -0
- sofar-1.2.1.dist-info/RECORD +105 -0
- sofar-1.2.1.dist-info/WHEEL +5 -0
- sofar-1.2.1.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/conftest.py +27 -0
- tests/test_deprecations.py +19 -0
- tests/test_io.py +349 -0
- tests/test_sofa.py +353 -0
- tests/test_sofa_upgrade_conventions.py +111 -0
- tests/test_sofa_verify.py +480 -0
- tests/test_sofastream.py +127 -0
- tests/test_utils.py +250 -0
sofar/io.py
ADDED
@@ -0,0 +1,427 @@
|
|
1
|
+
"""Module for reading and writing Sofa files with sofar."""
|
2
|
+
import contextlib
|
3
|
+
import os
|
4
|
+
import numpy as np
|
5
|
+
from netCDF4 import Dataset, chartostring, stringtochar
|
6
|
+
import warnings
|
7
|
+
import pathlib
|
8
|
+
from packaging.version import parse
|
9
|
+
import sofar as sf
|
10
|
+
from .utils import _verify_convention_and_version, _atleast_nd
|
11
|
+
|
12
|
+
|
13
|
+
def read_sofa(filename, verify='auto', verbose=True):
|
14
|
+
"""
|
15
|
+
Read SOFA file from disk and convert it to SOFA object.
|
16
|
+
|
17
|
+
Numeric data is returned as floats or numpy float arrays unless they have
|
18
|
+
missing data, in which case they are returned as numpy masked arrays.
|
19
|
+
|
20
|
+
If you want to read only parts of the data of a large sofa file use
|
21
|
+
:class:`SofaStream` instead.
|
22
|
+
|
23
|
+
Parameters
|
24
|
+
----------
|
25
|
+
filename : str
|
26
|
+
The full path to the sofa data.
|
27
|
+
verify : bool, optional
|
28
|
+
Verify and update the SOFA object by calling :py:func:`~Sofa.verify`.
|
29
|
+
This helps to find potential errors in the default values and is thus
|
30
|
+
recommended. If reading a file does not work, try to call `Sofa` with
|
31
|
+
``verify=False``. The default is ``'auto'`` defaults to ``True`` for
|
32
|
+
stable conventions with versions of 1.0 or higher and to ``False``
|
33
|
+
otherwise.
|
34
|
+
verbose : bool, optional
|
35
|
+
Print the names of detected custom variables and attributes. The
|
36
|
+
default is ``True``
|
37
|
+
|
38
|
+
Returns
|
39
|
+
-------
|
40
|
+
sofa : Sofa
|
41
|
+
Object containing the data from `filename`.
|
42
|
+
|
43
|
+
Notes
|
44
|
+
-----
|
45
|
+
1. Missing dimensions are appended when writing the SOFA object to disk.
|
46
|
+
E.g., if ``sofa.Data_IR`` is of shape (1, 2) it is written as an array
|
47
|
+
of shape (1, 2, 1) because the SOFA standard AES69 defines it as a
|
48
|
+
three dimensional array with the dimensions (`M: measurements`,
|
49
|
+
`R: receivers`, `N: samples`)
|
50
|
+
2. When reading data from a SOFA file, array data is always returned as
|
51
|
+
numpy arrays and singleton trailing dimensions are discarded (numpy
|
52
|
+
default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
|
53
|
+
after writing and reading to and from disk.
|
54
|
+
3. One dimensional arrays with only one element will be converted to scalar
|
55
|
+
values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
|
56
|
+
(1, ) inside SOFA files (according to the SOFA standard AES69) but
|
57
|
+
will be a scalar inside SOFA objects after reading from disk.
|
58
|
+
"""
|
59
|
+
|
60
|
+
return _read_netcdf(filename, verify, verbose, mode="sofa")
|
61
|
+
|
62
|
+
|
63
|
+
def read_sofa_as_netcdf(filename):
|
64
|
+
"""
|
65
|
+
Read corrupted SOFA data from disk.
|
66
|
+
|
67
|
+
.. note::
|
68
|
+
`read_sofa_as_netcdf` is intended to read and fix corrupted SOFA data
|
69
|
+
that could not be read by :py:func:`~read_sofa`. The recommend workflow
|
70
|
+
is
|
71
|
+
|
72
|
+
- Try to read the data with `read_sofa` and ``verify=True``
|
73
|
+
- If this fails, try the above with ``verify=False``
|
74
|
+
- If this fails, use `read_sofa_as_netcdf`
|
75
|
+
|
76
|
+
The SOFA object returned by `read_sofa_as_netcdf` may not work
|
77
|
+
correctly before the issues with the data were fixed, i.e., before the
|
78
|
+
data are in agreement with the SOFA standard AES-69.
|
79
|
+
|
80
|
+
Numeric data is returned as floats or numpy float arrays unless they have
|
81
|
+
missing data, in which case they are returned as numpy masked arrays.
|
82
|
+
|
83
|
+
Parameters
|
84
|
+
----------
|
85
|
+
filename : str
|
86
|
+
The full path to the NetCDF data.
|
87
|
+
|
88
|
+
Returns
|
89
|
+
-------
|
90
|
+
sofa : Sofa
|
91
|
+
Object containing the data from `filename`.
|
92
|
+
|
93
|
+
Notes
|
94
|
+
-----
|
95
|
+
1. Missing dimensions are appended when writing the SOFA object to disk.
|
96
|
+
E.g., if ``sofa.Data_IR`` is of shape (1, 2) it is written as an array
|
97
|
+
of shape (1, 2, 1) because the SOFA standard AES69 defines it as a
|
98
|
+
three dimensional array with the dimensions (`M: measurements`,
|
99
|
+
`R: receivers`, `N: samples`)
|
100
|
+
2. When reading data from a SOFA file, array data is always returned as
|
101
|
+
numpy arrays and singleton trailing dimensions are discarded (numpy
|
102
|
+
default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
|
103
|
+
after writing and reading to and from disk.
|
104
|
+
3. One dimensional arrays with only one element will be converted to scalar
|
105
|
+
values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
|
106
|
+
(1, ) inside SOFA files (according to the SOFA standard AES69) but
|
107
|
+
will be a scalar inside SOFA objects after reading from disk.
|
108
|
+
"""
|
109
|
+
return _read_netcdf(filename, False, False, mode="netcdf")
|
110
|
+
|
111
|
+
|
112
|
+
def _read_netcdf(filename, verify, verbose, mode):
|
113
|
+
|
114
|
+
# check the filename
|
115
|
+
filename = pathlib.Path(filename).with_suffix('.sofa')
|
116
|
+
if not os.path.isfile(filename):
|
117
|
+
raise ValueError(f"{filename} does not exist")
|
118
|
+
|
119
|
+
# attributes that are skipped
|
120
|
+
skip = ["_Encoding"]
|
121
|
+
|
122
|
+
# init list of all and custom attributes
|
123
|
+
all_attr = []
|
124
|
+
custom = []
|
125
|
+
|
126
|
+
# open new NETCDF4 file for reading
|
127
|
+
with Dataset(filename, "r", format="NETCDF4") as file:
|
128
|
+
|
129
|
+
if mode == "sofa":
|
130
|
+
# get convention name and version
|
131
|
+
convention = file.SOFAConventions
|
132
|
+
version = file.SOFAConventionsVersion
|
133
|
+
|
134
|
+
# check if convention and version exist
|
135
|
+
_verify_convention_and_version(version, convention)
|
136
|
+
|
137
|
+
# get SOFA object with default values
|
138
|
+
sofa = sf.Sofa(convention, version=version, verify=verify)
|
139
|
+
else:
|
140
|
+
sofa = sf.Sofa(None)
|
141
|
+
|
142
|
+
# allow writing read only attributes
|
143
|
+
sofa.protected = False
|
144
|
+
|
145
|
+
# load global attributes
|
146
|
+
for attr in file.ncattrs():
|
147
|
+
|
148
|
+
value = getattr(file, attr)
|
149
|
+
all_attr.append(f"GLOBAL_{attr}")
|
150
|
+
|
151
|
+
if not hasattr(sofa, f"GLOBAL_{attr}"):
|
152
|
+
sofa._add_custom_api_entry(
|
153
|
+
f"GLOBAL_{attr}", value, None, None, "attribute")
|
154
|
+
custom.append(f"GLOBAL_{attr}")
|
155
|
+
sofa.protected = False
|
156
|
+
else:
|
157
|
+
setattr(sofa, f"GLOBAL_{attr}", value)
|
158
|
+
|
159
|
+
# load data
|
160
|
+
for var in file.variables.keys():
|
161
|
+
|
162
|
+
value = _format_value_from_netcdf(file[var][:], var)
|
163
|
+
all_attr.append(var.replace(".", "_"))
|
164
|
+
|
165
|
+
if hasattr(sofa, var.replace(".", "_")):
|
166
|
+
setattr(sofa, var.replace(".", "_"), value)
|
167
|
+
else:
|
168
|
+
dimensions = "".join(list(file[var].dimensions))
|
169
|
+
# SOFA only uses dtypes 'double' and 'S1' but netCDF has more
|
170
|
+
dtype = "string" if file[var].datatype == "S1" else "double"
|
171
|
+
sofa._add_custom_api_entry(var.replace(".", "_"), value, None,
|
172
|
+
dimensions, dtype)
|
173
|
+
custom.append(var.replace(".", "_"))
|
174
|
+
sofa.protected = False
|
175
|
+
|
176
|
+
# load variable attributes
|
177
|
+
for attr in [a for a in file[var].ncattrs() if a not in skip]:
|
178
|
+
|
179
|
+
value = getattr(file[var], attr)
|
180
|
+
all_attr.append(var.replace(".", "_") + "_" + attr)
|
181
|
+
|
182
|
+
if not hasattr(sofa, var.replace(".", "_") + "_" + attr):
|
183
|
+
sofa._add_custom_api_entry(
|
184
|
+
var.replace(".", "_") + "_" + attr, value, None,
|
185
|
+
None, "attribute")
|
186
|
+
custom.append(var.replace(".", "_") + "_" + attr)
|
187
|
+
sofa.protected = False
|
188
|
+
else:
|
189
|
+
setattr(sofa, var.replace(".", "_") + "_" + attr, value)
|
190
|
+
|
191
|
+
# remove fields from initial Sofa object that were not contained in NetCDF
|
192
|
+
# file (initial Sofa object contained mandatory and optional fields)
|
193
|
+
attrs = [attr for attr in sofa.__dict__.keys() if not attr.startswith("_")]
|
194
|
+
for attr in attrs:
|
195
|
+
if attr not in all_attr:
|
196
|
+
delattr(sofa, attr)
|
197
|
+
|
198
|
+
# do not allow writing read only attributes any more
|
199
|
+
sofa.protected = True
|
200
|
+
|
201
|
+
# notice about custom entries
|
202
|
+
if custom and verbose:
|
203
|
+
print(("SOFA file contained custom entries\n"
|
204
|
+
"----------------------------------\n"
|
205
|
+
f"{', '.join(custom)}"))
|
206
|
+
|
207
|
+
# set default for verify
|
208
|
+
if verify == 'auto':
|
209
|
+
verify = True if parse(version) >= parse('1.0') else False
|
210
|
+
|
211
|
+
# update api
|
212
|
+
if verify:
|
213
|
+
try:
|
214
|
+
sofa.verify(mode="read")
|
215
|
+
except: # noqa: E722
|
216
|
+
raise ValueError((
|
217
|
+
"The SOFA object could not be verified, maybe due to erroneous"
|
218
|
+
" data. Call sofa=sofar.read_sofa(filename, verify=False) and "
|
219
|
+
"then sofa.verify() to get more information")) from None
|
220
|
+
|
221
|
+
return sofa
|
222
|
+
|
223
|
+
|
224
|
+
def write_sofa(filename: str, sofa: sf.Sofa, compression=4):
|
225
|
+
"""
|
226
|
+
Write a SOFA object to disk as a SOFA file.
|
227
|
+
|
228
|
+
Parameters
|
229
|
+
----------
|
230
|
+
filename : str
|
231
|
+
The filename. '.sofa' is appended to the filename, if it is not
|
232
|
+
explicitly given.
|
233
|
+
sofa : object
|
234
|
+
The SOFA object that is written to disk
|
235
|
+
compression : int
|
236
|
+
The level of compression with ``0`` being no compression and ``9``
|
237
|
+
being the best compression. The default of ``9`` optimizes the file
|
238
|
+
size but increases the time for writing files to disk.
|
239
|
+
|
240
|
+
Notes
|
241
|
+
-----
|
242
|
+
1. Missing dimensions are appended when writing the SOFA object to disk.
|
243
|
+
E.g., if ``sofa.Data_IR`` is of shape (1, 2) it is written as an array
|
244
|
+
of shape (1, 2, 1) because the SOFA standard AES69 defines it as a
|
245
|
+
three dimensional array with the dimensions (`M: measurements`,
|
246
|
+
`R: receivers`, `N: samples`)
|
247
|
+
2. When reading data from a SOFA file, array data is always returned as
|
248
|
+
numpy arrays and singleton trailing dimensions are discarded (numpy
|
249
|
+
default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
|
250
|
+
after writing and reading to and from disk.
|
251
|
+
3. One dimensional arrays with only one element will be converted to scalar
|
252
|
+
values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
|
253
|
+
(1, ) inside SOFA files (according to the SOFA standard AES69) but
|
254
|
+
will be a scalar inside SOFA objects after reading from disk.
|
255
|
+
"""
|
256
|
+
_write_sofa(filename, sofa, compression, verify=True)
|
257
|
+
|
258
|
+
|
259
|
+
def _write_sofa(filename: str, sofa: sf.Sofa, compression=4, verify=True):
|
260
|
+
"""
|
261
|
+
Private write function for writing invalid SOFA files for testing. See
|
262
|
+
write_sofa for documentation.
|
263
|
+
"""
|
264
|
+
|
265
|
+
# check the filename
|
266
|
+
filename = pathlib.Path(filename).with_suffix('.sofa')
|
267
|
+
|
268
|
+
if verify:
|
269
|
+
# check if the latest version is used for writing and warn otherwise
|
270
|
+
# if case required for writing SOFA test data that violates the
|
271
|
+
# conventions
|
272
|
+
if sofa.GLOBAL_SOFAConventions != "invalid-value":
|
273
|
+
latest = sf.Sofa(sofa.GLOBAL_SOFAConventions)
|
274
|
+
latest = latest.GLOBAL_SOFAConventionsVersion
|
275
|
+
current = sofa.GLOBAL_SOFAConventionsVersion
|
276
|
+
|
277
|
+
if parse(current) < parse(latest):
|
278
|
+
warnings.warn((
|
279
|
+
"Writing SOFA object with outdated Convention "
|
280
|
+
f"version {current}. It is recommend to upgrade "
|
281
|
+
" data with Sofa.upgrade_convention() before "
|
282
|
+
"writing to disk if possible."), stacklevel=2)
|
283
|
+
|
284
|
+
# setting the netCDF compression parameter
|
285
|
+
use_zlib = compression != 0
|
286
|
+
|
287
|
+
# update the dimensions
|
288
|
+
if verify:
|
289
|
+
sofa.verify(mode="write")
|
290
|
+
|
291
|
+
# list of all attribute names
|
292
|
+
all_keys = [key for key in sofa.__dict__.keys() if not key.startswith("_")]
|
293
|
+
|
294
|
+
# open new NETCDF4 file for writing
|
295
|
+
with Dataset(filename, "w", format="NETCDF4") as file:
|
296
|
+
|
297
|
+
# write dimensions
|
298
|
+
for dim in sofa._api:
|
299
|
+
file.createDimension(dim, sofa._api[dim])
|
300
|
+
|
301
|
+
# write global attributes
|
302
|
+
keys = [key for key in all_keys if key.startswith("GLOBAL_")]
|
303
|
+
for key in keys:
|
304
|
+
setattr(file, key[7:], str(getattr(sofa, key)))
|
305
|
+
|
306
|
+
# write data
|
307
|
+
for key in all_keys:
|
308
|
+
|
309
|
+
# skip attributes
|
310
|
+
# Note: The used definition of attributes is lax. The strict
|
311
|
+
# definition would parse `key` and assume an attribute if
|
312
|
+
# 1. "_" is in key and key does not start with "DATA_", or
|
313
|
+
# 2. key contains more than one "_"
|
314
|
+
#
|
315
|
+
# The strict definition is implicitly included in the SOFA standard
|
316
|
+
# since underscores only occur for variables starting with Data_
|
317
|
+
if sofa._convention[key]["type"] == "attribute":
|
318
|
+
continue
|
319
|
+
|
320
|
+
# get the data and type and shape
|
321
|
+
value, dtype = _format_value_for_netcdf(
|
322
|
+
getattr(sofa, key), key, sofa._convention[key]["type"],
|
323
|
+
sofa._dimensions[key], sofa._api["S"])
|
324
|
+
|
325
|
+
# create variable and write data
|
326
|
+
shape = list(sofa._dimensions[key])
|
327
|
+
tmp_var = file.createVariable(
|
328
|
+
key.replace("Data_", "Data."), dtype, shape,
|
329
|
+
zlib=use_zlib, complevel=compression)
|
330
|
+
if dtype == "f8":
|
331
|
+
tmp_var[:] = value
|
332
|
+
else:
|
333
|
+
tmp_var[:] = stringtochar(value, encoding='utf-8')
|
334
|
+
|
335
|
+
# write variable attributes
|
336
|
+
sub_keys = [k for k in all_keys if k.startswith(f"{key}_")]
|
337
|
+
for sub_key in sub_keys:
|
338
|
+
setattr(tmp_var, sub_key[len(key)+1:],
|
339
|
+
str(getattr(sofa, sub_key)))
|
340
|
+
|
341
|
+
|
342
|
+
def _format_value_for_netcdf(value, key, dtype, dimensions, S):
|
343
|
+
"""
|
344
|
+
Format value from SOFA object for saving in a NETCDF4 file.
|
345
|
+
|
346
|
+
Parameters
|
347
|
+
----------
|
348
|
+
value : str, array like
|
349
|
+
The value to be formatted
|
350
|
+
key : str
|
351
|
+
The name of the current attribute. Needed for verbose errors.
|
352
|
+
dtype : str
|
353
|
+
The the data type of value
|
354
|
+
dimensions : str
|
355
|
+
The intended dimensions from ``sofa.dimensions``
|
356
|
+
S : int
|
357
|
+
Length of the string array.
|
358
|
+
|
359
|
+
Returns
|
360
|
+
-------
|
361
|
+
value : str, numpy array
|
362
|
+
The formatted value.
|
363
|
+
netcdf_dtype : str
|
364
|
+
The data type as a string for writing to a NETCDF4 file ('attribute',
|
365
|
+
'f8', or 'S1').
|
366
|
+
"""
|
367
|
+
# copy value
|
368
|
+
with contextlib.suppress(AttributeError):
|
369
|
+
value = value.copy()
|
370
|
+
|
371
|
+
# parse data
|
372
|
+
if dtype == "attribute":
|
373
|
+
value = str(value)
|
374
|
+
netcdf_dtype = "attribute"
|
375
|
+
elif dtype == "double":
|
376
|
+
value = _atleast_nd(value, len(dimensions))
|
377
|
+
netcdf_dtype = "f8"
|
378
|
+
elif dtype == "string":
|
379
|
+
value = np.array(value, dtype=f"S{str(S)}")
|
380
|
+
value = _atleast_nd(value, len(dimensions))
|
381
|
+
netcdf_dtype = 'S1'
|
382
|
+
else:
|
383
|
+
raise ValueError(f"Unknown type {dtype} for {key}")
|
384
|
+
|
385
|
+
return value, netcdf_dtype
|
386
|
+
|
387
|
+
|
388
|
+
def _format_value_from_netcdf(value, key):
|
389
|
+
"""
|
390
|
+
Format value from NETCDF4 file for saving in a SOFA object.
|
391
|
+
|
392
|
+
Parameters
|
393
|
+
----------
|
394
|
+
value : np.array of dtype float or S
|
395
|
+
The value to be formatted
|
396
|
+
key : str
|
397
|
+
The variable name of the current value. Needed for verbose errors.
|
398
|
+
|
399
|
+
Returns
|
400
|
+
-------
|
401
|
+
value : str, number, numpy array
|
402
|
+
The formatted value.
|
403
|
+
"""
|
404
|
+
|
405
|
+
if "float" in str(value.dtype) or "int" in str(value.dtype):
|
406
|
+
if np.ma.is_masked(value):
|
407
|
+
warnings.warn(f"Entry {key} contains missing data", stacklevel=3)
|
408
|
+
else:
|
409
|
+
# Convert to numpy array or scalar
|
410
|
+
value = np.asarray(value)
|
411
|
+
elif str(value.dtype)[1] in ["S", "U"]:
|
412
|
+
# string arrays are stored in masked arrays with empty strings '' being
|
413
|
+
# masked. Convert to regular arrays with unmasked empty strings
|
414
|
+
if str(value.dtype)[1] == "S":
|
415
|
+
value = chartostring(value, encoding="ascii")
|
416
|
+
value = np.atleast_1d(value).astype("U")
|
417
|
+
else:
|
418
|
+
raise TypeError(
|
419
|
+
f"{key}: value.dtype is {value.dtype} but must be float, S or, U")
|
420
|
+
|
421
|
+
# convert arrays to scalars if they do not store data that is usually used
|
422
|
+
# as scalar metadata, e.g., the SamplingRate
|
423
|
+
data_keys = ["Data_IR", "Data_Real", "Data_Imag", "Data_SOS" "Data_Delay"]
|
424
|
+
if value.size == 1 and key not in data_keys:
|
425
|
+
value = value[0]
|
426
|
+
|
427
|
+
return value
|