sofar 0.3.1__py2.py3-none-any.whl → 1.1.0__py2.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.
- sofar/__init__.py +13 -7
- sofar/io.py +423 -0
- sofar/sofa.py +1795 -0
- sofar/sofa_conventions/VERSION +1 -0
- sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.csv +59 -0
- sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.json +444 -0
- sofar/{conventions/source → sofa_conventions/conventions}/FreeFieldHRIR_1.0.csv +3 -3
- sofar/{conventions → sofa_conventions/conventions}/FreeFieldHRIR_1.0.json +3 -3
- sofar/{conventions/source → sofa_conventions/conventions}/FreeFieldHRTF_1.0.csv +2 -2
- sofar/{conventions → sofa_conventions/conventions}/FreeFieldHRTF_1.0.json +3 -3
- sofar/{conventions/source → sofa_conventions/conventions}/GeneralFIR-E_2.0.csv +2 -2
- sofar/{conventions → sofa_conventions/conventions}/GeneralFIR-E_2.0.json +2 -2
- sofar/{conventions/source/GeneralFIR_2.0.csv → sofa_conventions/conventions/GeneralFIR_1.0.csv} +2 -2
- sofar/{conventions/GeneralFIR_2.0.json → sofa_conventions/conventions/GeneralFIR_1.0.json} +2 -2
- sofar/{conventions/source/GeneralFIR_1.0.csv → sofa_conventions/conventions/GeneralSOS_1.0.csv} +11 -11
- sofar/{conventions/GeneralFIR_1.0.json → sofa_conventions/conventions/GeneralSOS_1.0.json} +48 -37
- sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF-E_1.0.csv +3 -3
- sofar/{conventions → sofa_conventions/conventions}/GeneralTF-E_1.0.json +4 -4
- sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF_1.0.csv +1 -1
- sofar/{conventions → sofa_conventions/conventions}/GeneralTF_1.0.json +1 -1
- sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF_2.0.csv +4 -4
- sofar/{conventions → sofa_conventions/conventions}/GeneralTF_2.0.json +4 -4
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRIR_1.0.csv +47 -0
- sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldHRIR_1.0.json +1 -1
- sofar/{conventions/source → sofa_conventions/conventions}/SimpleFreeFieldHRSOS_1.0.csv +1 -1
- sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldHRSOS_1.0.json +1 -1
- sofar/{conventions/source/SimpleFreeFieldHRTF_2.0.csv → sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.csv} +3 -3
- sofar/{conventions/SimpleFreeFieldHRTF_2.0.json → sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.json} +4 -4
- sofar/{conventions/source → sofa_conventions/conventions}/SimpleHeadphoneIR_1.0.csv +9 -9
- sofar/sofa_conventions/conventions/SimpleHeadphoneIR_1.0.json +396 -0
- sofar/{conventions/source → sofa_conventions/conventions}/SingleRoomMIMOSRIR_1.0.csv +18 -8
- sofar/{conventions → sofa_conventions/conventions}/SingleRoomMIMOSRIR_1.0.json +124 -50
- sofar/{conventions/source → sofa_conventions/conventions}/SingleRoomSRIR_1.0.csv +18 -8
- sofar/{conventions → sofa_conventions/conventions}/SingleRoomSRIR_1.0.json +124 -50
- sofar/{conventions/source → sofa_conventions/conventions/deprecated}/FreeFieldDirectivityTF_1.0.csv +2 -2
- sofar/{conventions → sofa_conventions/conventions/deprecated}/FreeFieldDirectivityTF_1.0.json +2 -2
- sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.csv +48 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.csv +43 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.json +333 -0
- sofar/{conventions/source/SimpleFreeFieldHRIR_1.0.csv → sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.csv} +15 -18
- 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/{conventions/SimpleHeadphoneIR_1.0.json → sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.json} +3 -3
- 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/rules/deprecations.json +12 -0
- sofar/sofa_conventions/rules/rules.json +800 -0
- sofar/sofa_conventions/rules/unit_aliases.json +11 -0
- sofar/sofa_conventions/rules/upgrade.json +190 -0
- sofar/update_conventions.py +427 -0
- sofar/utils.py +315 -0
- {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/AUTHORS.rst +1 -0
- sofar-1.1.0.dist-info/METADATA +89 -0
- sofar-1.1.0.dist-info/RECORD +75 -0
- {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/WHEEL +1 -1
- {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/top_level.txt +1 -0
- tests/__init__.py +0 -0
- tests/test_deprecations.py +19 -0
- tests/test_io.py +344 -0
- tests/test_sofa.py +354 -0
- tests/test_sofa_upgrade_conventions.py +102 -0
- tests/test_sofa_verify.py +472 -0
- tests/test_utils.py +241 -0
- sofar/conventions/source/MultiSpeakerBRIR_0.3.csv +0 -48
- sofar/sofar.py +0 -2531
- sofar-0.3.1.dist-info/METADATA +0 -69
- sofar-0.3.1.dist-info/RECORD +0 -46
- /sofar/{conventions/source → sofa_conventions/conventions}/SimpleFreeFieldSOS_1.0.csv +0 -0
- /sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldSOS_1.0.json +0 -0
- /sofar/{conventions/source → sofa_conventions/conventions/deprecated}/GeneralFIRE_1.0.csv +0 -0
- /sofar/{conventions → sofa_conventions/conventions/deprecated}/GeneralFIRE_1.0.json +0 -0
- /sofar/{conventions → sofa_conventions/conventions/deprecated}/MultiSpeakerBRIR_0.3.json +0 -0
- /sofar/{conventions/source → sofa_conventions/conventions/deprecated}/SingleRoomDRIR_0.3.csv +0 -0
- /sofar/{conventions → sofa_conventions/conventions/deprecated}/SingleRoomDRIR_0.3.json +0 -0
- {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/LICENSE +0 -0
tests/test_io.py
ADDED
@@ -0,0 +1,344 @@
|
|
1
|
+
import sofar as sf
|
2
|
+
from sofar.utils import (_get_conventions,
|
3
|
+
_verify_convention_and_version,
|
4
|
+
_atleast_nd,
|
5
|
+
_nd_newaxis)
|
6
|
+
from sofar.io import (_format_value_for_netcdf,
|
7
|
+
_format_value_from_netcdf)
|
8
|
+
import os
|
9
|
+
import pathlib
|
10
|
+
from tempfile import TemporaryDirectory
|
11
|
+
import pytest
|
12
|
+
from pytest import raises
|
13
|
+
import numpy as np
|
14
|
+
import numpy.testing as npt
|
15
|
+
from netCDF4 import Dataset
|
16
|
+
|
17
|
+
|
18
|
+
def test_read_write_sofa(capfd):
|
19
|
+
|
20
|
+
temp_dir = TemporaryDirectory()
|
21
|
+
filename = os.path.join(temp_dir.name, "test.sofa")
|
22
|
+
sofa = sf.Sofa("SimpleFreeFieldHRIR")
|
23
|
+
|
24
|
+
# test defaults
|
25
|
+
sf.write_sofa(filename, sofa)
|
26
|
+
sofa = sf.read_sofa(filename)
|
27
|
+
assert hasattr(sofa, "_api")
|
28
|
+
|
29
|
+
# test with path object
|
30
|
+
sf.write_sofa(pathlib.Path(filename), sofa)
|
31
|
+
sofa = sf.read_sofa(pathlib.Path(filename))
|
32
|
+
assert hasattr(sofa, "_api")
|
33
|
+
|
34
|
+
# reading without updating API
|
35
|
+
sofa = sf.read_sofa(filename, verify=False)
|
36
|
+
assert not hasattr(sofa, "_api")
|
37
|
+
|
38
|
+
# read non-existing file
|
39
|
+
with raises(ValueError, match="test.sofa does not exist"):
|
40
|
+
sf.read_sofa("test.sofa")
|
41
|
+
|
42
|
+
# read file of unknown convention
|
43
|
+
sofa = sf.Sofa("SimpleFreeFieldHRIR")
|
44
|
+
sf.write_sofa(filename, sofa)
|
45
|
+
with Dataset(filename, "r+", format="NETCDF4") as file:
|
46
|
+
setattr(file, "SOFAConventions", "Funky")
|
47
|
+
with raises(ValueError, match="Convention 'Funky' does not exist"):
|
48
|
+
sf.read_sofa(filename)
|
49
|
+
|
50
|
+
# read file of unknown version (stored in file)
|
51
|
+
sofa = sf.Sofa("SimpleFreeFieldHRIR")
|
52
|
+
sf.write_sofa(filename, sofa)
|
53
|
+
with Dataset(filename, "r+", format="NETCDF4") as file:
|
54
|
+
setattr(file, "SOFAConventionsVersion", "0.1")
|
55
|
+
# ValueError when version should be matched
|
56
|
+
with raises(ValueError, match="v0.1 is not a valid SOFA Convention"):
|
57
|
+
sf.read_sofa(filename)
|
58
|
+
|
59
|
+
# read file containing a variable with wrong shape
|
60
|
+
sofa = sf.Sofa("SimpleFreeFieldHRIR")
|
61
|
+
sf.write_sofa(filename, sofa)
|
62
|
+
# create variable with wrong shape
|
63
|
+
with Dataset(filename, "r+", format="NETCDF4") as file:
|
64
|
+
file.createDimension('A', 10)
|
65
|
+
var = file.createVariable("Data_IR", "f8", ('I', 'A'))
|
66
|
+
var[:] = np.zeros((1, 10)).astype("double")
|
67
|
+
# reading data with update API generates an error
|
68
|
+
with raises(ValueError, match="The SOFA object could not be"):
|
69
|
+
sf.read_sofa(filename)
|
70
|
+
# data can be read without updating API
|
71
|
+
sf.read_sofa(filename, verify=False)
|
72
|
+
|
73
|
+
|
74
|
+
def test_read_sofa_custom_data():
|
75
|
+
"""Test if sofa files with custom data are loaded correctly"""
|
76
|
+
|
77
|
+
temp_dir = TemporaryDirectory()
|
78
|
+
filename = os.path.join(temp_dir.name, "test.sofa")
|
79
|
+
sofa = sf.Sofa("SimpleFreeFieldHRIR")
|
80
|
+
|
81
|
+
# GLOBAL attribute
|
82
|
+
sofa.add_attribute('GLOBAL_Warming', 'critical')
|
83
|
+
sf.write_sofa(filename, sofa)
|
84
|
+
sofa = sf.read_sofa(filename)
|
85
|
+
assert sofa.GLOBAL_Warming == 'critical'
|
86
|
+
|
87
|
+
|
88
|
+
def test_read_netcdf():
|
89
|
+
tmp = TemporaryDirectory()
|
90
|
+
files = [os.path.join(tmp.name, "invalid.sofa"),
|
91
|
+
os.path.join(tmp.name, "invalid.netcdf")]
|
92
|
+
|
93
|
+
# create data with invalid SOFA convention and version
|
94
|
+
sofa = sf.Sofa("GeneralTF")
|
95
|
+
sofa.protected = False
|
96
|
+
sofa.GLOBAL_SOFAConventions = "MadeUp"
|
97
|
+
sofa.protected = True
|
98
|
+
|
99
|
+
# test reading
|
100
|
+
for file in files:
|
101
|
+
# write data
|
102
|
+
sf.io._write_sofa(file, sofa, verify=False)
|
103
|
+
# can not be read with read_sofa
|
104
|
+
with raises(ValueError):
|
105
|
+
sf.read_sofa(file)
|
106
|
+
sofa_read = sf.read_sofa_as_netcdf(file)
|
107
|
+
sf.equals(sofa, sofa_read)
|
108
|
+
|
109
|
+
|
110
|
+
def test_write_sofa_outdated_version():
|
111
|
+
"""Test the warning for writing SOFA files with outdated versions"""
|
112
|
+
|
113
|
+
# generate test data and directory
|
114
|
+
tmp = TemporaryDirectory()
|
115
|
+
sofa = sf.Sofa("GeneralTF", version="1.0")
|
116
|
+
|
117
|
+
# write with outdated version
|
118
|
+
with pytest.warns(UserWarning, match="Writing SOFA object with outdated"):
|
119
|
+
sf.write_sofa(os.path.join(tmp.name, "outdated.sofa"), sofa)
|
120
|
+
|
121
|
+
|
122
|
+
def test_write_sofa_compression():
|
123
|
+
"""Test writing SOFA files with compression"""
|
124
|
+
|
125
|
+
# create temporary directory
|
126
|
+
temp_dir = TemporaryDirectory()
|
127
|
+
|
128
|
+
# create test data
|
129
|
+
sofa = sf.Sofa('SimpleFreeFieldHRIR')
|
130
|
+
sofa.Data_IR = np.zeros((1, 2, 2048))
|
131
|
+
sofa.Data_IR[0, 0] = np.array([1, 0, -1, 0] * 512)
|
132
|
+
|
133
|
+
filesize = None
|
134
|
+
|
135
|
+
for compression in range(10):
|
136
|
+
# write with current compression level
|
137
|
+
filename = os.path.join(temp_dir.name, f"test_{0}.sofa")
|
138
|
+
sf.write_sofa(filename, sofa, compression=compression)
|
139
|
+
|
140
|
+
# get and compare the file sizes
|
141
|
+
print(f"Assessing compression level {compression}")
|
142
|
+
if compression > 0:
|
143
|
+
assert os.stat(filename).st_size <= filesize
|
144
|
+
filesize = os.stat(filename).st_size
|
145
|
+
|
146
|
+
|
147
|
+
# mandatory=True can not be tested because some conventions have default values
|
148
|
+
# that have optional variables as dependencies
|
149
|
+
@pytest.mark.parametrize("mandatory", [(False)])
|
150
|
+
def test_roundtrip(mandatory):
|
151
|
+
""""
|
152
|
+
Cyclic test of create, write, read functions
|
153
|
+
|
154
|
+
1. create_sofa
|
155
|
+
2. write_sofa
|
156
|
+
3. read_sofa
|
157
|
+
4. compare SOFA from 1. and 3.
|
158
|
+
"""
|
159
|
+
|
160
|
+
temp_dir = TemporaryDirectory()
|
161
|
+
names_versions = _get_conventions(return_type="name_version")
|
162
|
+
|
163
|
+
_, _, deprecations, _ = sf.Sofa._verification_rules()
|
164
|
+
|
165
|
+
for name, version in names_versions:
|
166
|
+
print(f"Testing: {name} {version}")
|
167
|
+
|
168
|
+
if name in deprecations["GLOBAL:SOFAConventions"]:
|
169
|
+
# deprecated conventions can not be written
|
170
|
+
sofa = sf.Sofa(name, mandatory, version, verify=False)
|
171
|
+
with pytest.warns(UserWarning, match="deprecations"):
|
172
|
+
sofa.verify(mode="read")
|
173
|
+
else:
|
174
|
+
# test full round-trip for other conventions
|
175
|
+
file = os.path.join(temp_dir.name, name + ".sofa")
|
176
|
+
sofa = sf.Sofa(name, mandatory, version)
|
177
|
+
sf.write_sofa(file, sofa)
|
178
|
+
sofa_r = sf.read_sofa(file)
|
179
|
+
identical = sf.equals(sofa, sofa_r, verbose=True, exclude="DATE")
|
180
|
+
assert identical
|
181
|
+
|
182
|
+
|
183
|
+
def test_roundtrip_multidimensional_string_variable():
|
184
|
+
"""
|
185
|
+
Test writing and reading multidimensional string variables (Wringting
|
186
|
+
string variables with one dimension is done in the other roundtrip test).
|
187
|
+
"""
|
188
|
+
|
189
|
+
temp_dir = TemporaryDirectory()
|
190
|
+
file = os.path.join(temp_dir.name, "HeadphoneIR.sofa")
|
191
|
+
|
192
|
+
sofa = sf.Sofa("SimpleHeadphoneIR")
|
193
|
+
# add dummy matrix that contains 4 measurements
|
194
|
+
sofa.Data_IR = np.zeros((4, 2, 10))
|
195
|
+
# add (4, 1) string variable
|
196
|
+
sofa.SourceManufacturer = [["someone"], ["else"], ["did"], ["this"]]
|
197
|
+
# remove other string variables for simplicity
|
198
|
+
delattr(sofa, "SourceModel")
|
199
|
+
delattr(sofa, "ReceiverDescriptions")
|
200
|
+
delattr(sofa, "EmitterDescriptions")
|
201
|
+
delattr(sofa, "MeasurementDate")
|
202
|
+
|
203
|
+
# read write and assert
|
204
|
+
sf.write_sofa(file, sofa)
|
205
|
+
sofa_r = sf.read_sofa(file)
|
206
|
+
|
207
|
+
identical = sf.equals(sofa, sofa_r, exclude="DATE")
|
208
|
+
assert identical
|
209
|
+
|
210
|
+
|
211
|
+
def test_format_value_for_netcdf():
|
212
|
+
|
213
|
+
# string and None dimensions (a.k.a NETCDF attribute)
|
214
|
+
value, dtype = _format_value_for_netcdf(
|
215
|
+
"string", "test_attr", "attribute", None, 12)
|
216
|
+
assert value == "string"
|
217
|
+
assert dtype == "attribute"
|
218
|
+
|
219
|
+
# int that should be converted to a string
|
220
|
+
value, dtype = _format_value_for_netcdf(
|
221
|
+
1, "test_attr", "attribute", None, 12)
|
222
|
+
assert value == "1"
|
223
|
+
assert dtype == "attribute"
|
224
|
+
|
225
|
+
# float that should be converted to a string
|
226
|
+
value, dtype = _format_value_for_netcdf(
|
227
|
+
0.2, "test_attr", "attribute", None, 12)
|
228
|
+
assert value == "0.2"
|
229
|
+
assert dtype == "attribute"
|
230
|
+
|
231
|
+
# string and IS dimensions
|
232
|
+
value, dtype = _format_value_for_netcdf(
|
233
|
+
"string", "TestVar", "string", "IS", 12)
|
234
|
+
assert value == np.array("string", dtype="S12")
|
235
|
+
assert dtype == "S1"
|
236
|
+
assert value.ndim == 2
|
237
|
+
|
238
|
+
# single entry array and none Dimensions
|
239
|
+
value, dtype = _format_value_for_netcdf(
|
240
|
+
["string"], "TestVar", "string", "IS", 12)
|
241
|
+
assert value == np.array(["string"], dtype="S12")
|
242
|
+
assert dtype == "S1"
|
243
|
+
assert value.ndim == 2
|
244
|
+
|
245
|
+
# array of strings
|
246
|
+
value, dtype = _format_value_for_netcdf(
|
247
|
+
[["a"], ["bc"]], "TestVar", "string", "MS", 12)
|
248
|
+
assert all(value == np.array([["a"], ["bc"]], "S12"))
|
249
|
+
assert dtype == "S1"
|
250
|
+
assert value.ndim == 2
|
251
|
+
|
252
|
+
# test with list
|
253
|
+
value, dtype = _format_value_for_netcdf(
|
254
|
+
[0, 0], "TestVar", "double", "MR", 12)
|
255
|
+
npt.assert_allclose(value, np.array([0, 0])[np.newaxis, ])
|
256
|
+
assert dtype == "f8"
|
257
|
+
assert value.ndim == 2
|
258
|
+
|
259
|
+
# test with numpy array
|
260
|
+
value, dtype = _format_value_for_netcdf(
|
261
|
+
np.array([0, 0]), "TestVar", "double", "MR", 12)
|
262
|
+
npt.assert_allclose(value, np.array([0, 0])[np.newaxis, ])
|
263
|
+
assert dtype == "f8"
|
264
|
+
assert value.ndim == 2
|
265
|
+
|
266
|
+
# unknown data type
|
267
|
+
with raises(ValueError, match="Unknown type int for TestVar"):
|
268
|
+
value, dtype = _format_value_for_netcdf(1, "TestVar", "int", "MR", 12)
|
269
|
+
|
270
|
+
|
271
|
+
def test_format_value_from_netcdf():
|
272
|
+
|
273
|
+
# single string (emulate NetCDF binary format)
|
274
|
+
value_in = np.array(["s", "t", "r"], dtype="S1")
|
275
|
+
value = _format_value_from_netcdf(value_in, "Some_Attribute")
|
276
|
+
assert value == "str"
|
277
|
+
|
278
|
+
# array of strings (emulate NetCDF binary format)
|
279
|
+
value_in = np.array([["s", "t", "r", "1"], ["s", "t", "r", "2"]],
|
280
|
+
dtype="S1")
|
281
|
+
value = _format_value_from_netcdf(value_in, "Some_Attribute")
|
282
|
+
assert all(value == np.array(["str1", "str2"], dtype="U"))
|
283
|
+
|
284
|
+
# single string (emulate NetCDF ascii encoding)
|
285
|
+
value_in = np.array(["str"], dtype="U")
|
286
|
+
value = _format_value_from_netcdf(value_in, "Some_Attribute")
|
287
|
+
assert value == "str"
|
288
|
+
|
289
|
+
# array of strings (emulate NetCDF binary format)
|
290
|
+
value_in = np.array(["str1", "str2"], dtype="U")
|
291
|
+
value = _format_value_from_netcdf(value_in, "Some_Attribute")
|
292
|
+
assert all(value == np.array(["str1", "str2"], dtype="U"))
|
293
|
+
|
294
|
+
# numerical array that can be scalar
|
295
|
+
value = _format_value_from_netcdf(
|
296
|
+
np.array([44100], dtype="float"), "Data_SamplingRate")
|
297
|
+
assert value == 44100.
|
298
|
+
|
299
|
+
# numerical array that can not be scalar
|
300
|
+
value = _format_value_from_netcdf(
|
301
|
+
np.array([44100], dtype="float"), "Data_IR")
|
302
|
+
assert value == np.array(44100., dtype="float")
|
303
|
+
|
304
|
+
# masked array with missing data
|
305
|
+
array = np.ma.masked_array([1, 2], mask=[0, 1], dtype="float")
|
306
|
+
with pytest.warns(UserWarning, match="Entry Data_IR contains missing"):
|
307
|
+
value = _format_value_from_netcdf(array, "Data_IR")
|
308
|
+
npt.assert_allclose(value, array)
|
309
|
+
|
310
|
+
# test with invalid data dtype
|
311
|
+
with raises(TypeError, match="Data_IR: value.dtype is complex"):
|
312
|
+
_format_value_from_netcdf(
|
313
|
+
np.array([44100], dtype="complex"), "Data_IR")
|
314
|
+
|
315
|
+
|
316
|
+
def test_verify_convention_and_version():
|
317
|
+
|
318
|
+
# test with existing convention and version (no error returns None)
|
319
|
+
out = _verify_convention_and_version("1.0", "GeneralTF")
|
320
|
+
assert out is None
|
321
|
+
|
322
|
+
# test assertions
|
323
|
+
with raises(ValueError, match="Convention 'Funky' does not exist"):
|
324
|
+
_verify_convention_and_version("1.0", "Funky")
|
325
|
+
with raises(ValueError, match="v1.1 is not a valid SOFA Convention"):
|
326
|
+
_verify_convention_and_version("1.1", "GeneralTF")
|
327
|
+
|
328
|
+
|
329
|
+
def test_atleast_nd():
|
330
|
+
# test with single dimension array
|
331
|
+
for ndim in range(1, 6):
|
332
|
+
array = _atleast_nd(1, ndim)
|
333
|
+
assert array.ndim == ndim
|
334
|
+
assert array.flatten() == np.array([1])
|
335
|
+
|
336
|
+
# test with two-dimensional array
|
337
|
+
for ndim in range(1, 6):
|
338
|
+
array = _atleast_nd(np.atleast_2d(1), ndim)
|
339
|
+
assert array.ndim == max(2, ndim)
|
340
|
+
assert array.flatten() == np.array([1])
|
341
|
+
|
342
|
+
|
343
|
+
def test_nd_newaxis():
|
344
|
+
assert _nd_newaxis([1, 2, 3, 4, 5, 6], 2).shape == (6, 1)
|
tests/test_sofa.py
ADDED
@@ -0,0 +1,354 @@
|
|
1
|
+
"""Tests for sofar.Sofa (test for Sofa.verifycontained in test_sofa_verify)"""
|
2
|
+
import sofar as sf
|
3
|
+
import os
|
4
|
+
from tempfile import TemporaryDirectory
|
5
|
+
import pytest
|
6
|
+
from pytest import raises
|
7
|
+
import numpy as np
|
8
|
+
|
9
|
+
|
10
|
+
def test_create_sofa_object(capfd):
|
11
|
+
# test assertion for type of convention parameter
|
12
|
+
with raises(TypeError, match="Convention must be a string"):
|
13
|
+
sf.Sofa(1)
|
14
|
+
# test assertion for invalid conventions
|
15
|
+
with raises(ValueError, match="Convention 'invalid' not found"):
|
16
|
+
sf.Sofa("invalid")
|
17
|
+
|
18
|
+
# test creation with defaults
|
19
|
+
sofa = sf.Sofa("GeneralTF")
|
20
|
+
assert isinstance(sofa, sf.Sofa)
|
21
|
+
assert sofa.GLOBAL_SOFAConventionsVersion == "2.0"
|
22
|
+
assert hasattr(sofa, '_convention')
|
23
|
+
assert hasattr(sofa, '_dimensions')
|
24
|
+
assert hasattr(sofa, '_api')
|
25
|
+
|
26
|
+
# assert __repr__
|
27
|
+
print(sofa)
|
28
|
+
out, _ = capfd.readouterr()
|
29
|
+
assert out == "sofar.SOFA object: GeneralTF 2.0\n"
|
30
|
+
|
31
|
+
# test returning only mandatory fields
|
32
|
+
sofa_all = sf.Sofa("GeneralTF")
|
33
|
+
sofa_man = sf.Sofa("GeneralTF", mandatory=True)
|
34
|
+
assert len(sofa_all.__dict__) > len(sofa_man.__dict__)
|
35
|
+
|
36
|
+
# test version parameter
|
37
|
+
sofa = sf.Sofa("GeneralTF", version="1.0")
|
38
|
+
assert str(sofa.GLOBAL_SOFAConventionsVersion) == "1.0"
|
39
|
+
|
40
|
+
# test invalid version
|
41
|
+
with raises(ValueError, match="Version 0.25 not found. Available"):
|
42
|
+
sf.Sofa("GeneralTF", version="0.25")
|
43
|
+
|
44
|
+
# test without updating the api
|
45
|
+
sofa = sf.Sofa("GeneralTF", verify=False)
|
46
|
+
assert hasattr(sofa, '_convention')
|
47
|
+
assert not hasattr(sofa, '_dimensions')
|
48
|
+
assert not hasattr(sofa, '_api')
|
49
|
+
|
50
|
+
|
51
|
+
def test_set_attributes_of_sofa_object():
|
52
|
+
sofa = sf.Sofa("GeneralTF")
|
53
|
+
|
54
|
+
# set attribute
|
55
|
+
assert (sofa.ListenerPosition == [0, 0, 0]).all()
|
56
|
+
sofa.ListenerPosition = np.array([1, 1, 1])
|
57
|
+
assert (sofa.ListenerPosition == [1, 1, 1]).all()
|
58
|
+
|
59
|
+
# set read only attribute
|
60
|
+
with raises(TypeError, match="GLOBAL_Version is a read only"):
|
61
|
+
sofa.GLOBAL_Version = 1
|
62
|
+
|
63
|
+
# set non-existing attribute
|
64
|
+
with raises(TypeError, match="new is an invalid attribute"):
|
65
|
+
sofa.new = 1
|
66
|
+
|
67
|
+
|
68
|
+
def test_delete_attribute_from_sofa_object():
|
69
|
+
sofa = sf.Sofa("GeneralTF")
|
70
|
+
|
71
|
+
# delete optional attribute
|
72
|
+
delattr(sofa, "GLOBAL_ApplicationName")
|
73
|
+
|
74
|
+
# delete mandatory attribute
|
75
|
+
with raises(TypeError, match="GLOBAL_Version is a mandatory"):
|
76
|
+
delattr(sofa, "GLOBAL_Version")
|
77
|
+
|
78
|
+
# delete not existing attribute
|
79
|
+
with raises(TypeError, match="new is not an attribute"):
|
80
|
+
delattr(sofa, "new")
|
81
|
+
|
82
|
+
|
83
|
+
def test_copy_sofa_object():
|
84
|
+
sofa_org = sf.Sofa("GeneralTF")
|
85
|
+
sofa_cp = sofa_org.copy()
|
86
|
+
|
87
|
+
assert sf.equals(sofa_org, sofa_cp, verbose=False)
|
88
|
+
assert id(sofa_org) != id(sofa_cp)
|
89
|
+
|
90
|
+
|
91
|
+
def test_list_dimensions(capfd):
|
92
|
+
|
93
|
+
# test FIR Data
|
94
|
+
sofa = sf.Sofa("GeneralFIR")
|
95
|
+
sofa.list_dimensions
|
96
|
+
out, _ = capfd.readouterr()
|
97
|
+
assert "N = 1 samples (set by Data_IR of dimension MRN)" in out
|
98
|
+
|
99
|
+
# test TF Data
|
100
|
+
sofa = sf.Sofa("GeneralTF")
|
101
|
+
sofa.list_dimensions
|
102
|
+
out, _ = capfd.readouterr()
|
103
|
+
assert "N = 1 frequencies (set by Data_Real of dimension MRN)" in out
|
104
|
+
|
105
|
+
# test SOS Data
|
106
|
+
sofa = sf.Sofa("SimpleFreeFieldHRSOS")
|
107
|
+
sofa.list_dimensions
|
108
|
+
out, _ = capfd.readouterr()
|
109
|
+
assert "N = 6 SOS coefficients (set by Data_SOS of dimension MRN)" in out
|
110
|
+
|
111
|
+
# test non spherical harmonics data
|
112
|
+
sofa = sf.Sofa("GeneralFIR")
|
113
|
+
sofa.list_dimensions
|
114
|
+
out, _ = capfd.readouterr()
|
115
|
+
assert "E = 1 emitter" in out
|
116
|
+
assert "R = 1 receiver" in out
|
117
|
+
|
118
|
+
sofa.EmitterPosition_Type = "spherical harmonics"
|
119
|
+
sofa.ReceiverPosition_Type = "spherical harmonics"
|
120
|
+
sofa.EmitterPosition_Units = "degree, degree, metre"
|
121
|
+
sofa.ReceiverPosition_Units = "degree, degree, metre"
|
122
|
+
sofa.list_dimensions
|
123
|
+
out, _ = capfd.readouterr()
|
124
|
+
assert "E = 1 emitter spherical harmonics coefficients" in out
|
125
|
+
assert "R = 1 receiver spherical harmonics coefficients" in out
|
126
|
+
|
127
|
+
# test assertion in case of variables with wrong type or shape
|
128
|
+
sofa = sf.Sofa("GeneralFIR")
|
129
|
+
sofa.Data_IR = "test"
|
130
|
+
with raises(ValueError, match="Dimensions can not be shown"):
|
131
|
+
sofa.list_dimensions
|
132
|
+
sofa.Data_IR = [1, 2, 3, 4]
|
133
|
+
with raises(ValueError, match="Dimensions can not be shown"):
|
134
|
+
sofa.list_dimensions
|
135
|
+
|
136
|
+
|
137
|
+
def test_get_dimension():
|
138
|
+
"""Test getting the size of dimensions"""
|
139
|
+
|
140
|
+
# test FIR Data
|
141
|
+
sofa = sf.Sofa("GeneralFIR")
|
142
|
+
size = sofa.get_dimension("N")
|
143
|
+
assert size == 1
|
144
|
+
|
145
|
+
# test with wrong dimension
|
146
|
+
with raises(ValueError, match="Q is not a valid dimension"):
|
147
|
+
size = sofa.get_dimension("Q")
|
148
|
+
|
149
|
+
|
150
|
+
def test_info(capfd):
|
151
|
+
|
152
|
+
sofa = sf.Sofa("SimpleFreeFieldHRIR")
|
153
|
+
|
154
|
+
# test with wrong info string
|
155
|
+
with raises(
|
156
|
+
ValueError, match="info='invalid' is invalid"):
|
157
|
+
sofa.info("invalid")
|
158
|
+
|
159
|
+
# test with default parameter
|
160
|
+
sofa.info()
|
161
|
+
out, _ = capfd.readouterr()
|
162
|
+
assert "showing all entries" in out
|
163
|
+
|
164
|
+
# test listing all entry names
|
165
|
+
for info in ["all", "mandatory", "optional", "read only", "data"]:
|
166
|
+
sofa.info(info)
|
167
|
+
out, _ = capfd.readouterr()
|
168
|
+
assert f"showing {info} entries" in out
|
169
|
+
|
170
|
+
# list information for specific entry
|
171
|
+
sofa.info("ListenerPosition")
|
172
|
+
out, _ = capfd.readouterr()
|
173
|
+
assert "ListenerPosition\n type: double" in out
|
174
|
+
assert "ListenerPosition_Type\n type: attribute" in out
|
175
|
+
assert "ListenerPosition_Units\n type: attribute" in out
|
176
|
+
|
177
|
+
|
178
|
+
def test_inspect(capfd):
|
179
|
+
|
180
|
+
temp_dir = TemporaryDirectory()
|
181
|
+
file = os.path.join(temp_dir.name, "info.txt")
|
182
|
+
|
183
|
+
sofa = sf.Sofa("SimpleFreeFieldHRIR")
|
184
|
+
|
185
|
+
# inspect file
|
186
|
+
sofa.inspect(file)
|
187
|
+
|
188
|
+
# check terminal output
|
189
|
+
out, _ = capfd.readouterr()
|
190
|
+
assert "GLOBAL_SOFAConventions : SimpleFreeFieldHRIR" in out
|
191
|
+
assert ("ReceiverPosition : (R=2, C=3, I=1)\n"
|
192
|
+
" [[ 0. 0.09 0. ]\n"
|
193
|
+
" [ 0. -0.09 0. ]]") in out
|
194
|
+
|
195
|
+
# check text file
|
196
|
+
with open(file, "r") as f_id:
|
197
|
+
text = f_id.readlines()
|
198
|
+
assert out == "".join(text)
|
199
|
+
|
200
|
+
|
201
|
+
def test_add_entry():
|
202
|
+
|
203
|
+
sofa = sf.Sofa("GeneralTF")
|
204
|
+
|
205
|
+
tmp_dir = TemporaryDirectory()
|
206
|
+
|
207
|
+
# test adding a single variable entry
|
208
|
+
sofa.add_variable("Temperature", 25.1, "double", "MI")
|
209
|
+
entry = {"flags": None, "dimensions": "MI", "type": "double",
|
210
|
+
"default": None, "comment": ""}
|
211
|
+
assert sofa.Temperature == 25.1
|
212
|
+
assert sofa._custom["Temperature"] == entry
|
213
|
+
assert sofa._convention["Temperature"] == entry
|
214
|
+
|
215
|
+
# test adding string variable, global and local attributes
|
216
|
+
sofa.add_variable("Mood", "good", "string", "MS")
|
217
|
+
assert sofa.Mood == "good"
|
218
|
+
sofa.add_attribute("GLOBAL_Mood", "good")
|
219
|
+
assert sofa.GLOBAL_Mood == "good"
|
220
|
+
sofa.add_attribute("Temperature_Units", "degree celsius")
|
221
|
+
assert sofa.Temperature_Units == "degree celsius"
|
222
|
+
|
223
|
+
# check if everything can be verified and written, and read correctly
|
224
|
+
sf.write_sofa(os.path.join(tmp_dir.name, "tmp.sofa"), sofa)
|
225
|
+
sofa_read = sf.read_sofa(os.path.join(tmp_dir.name, "tmp.sofa"))
|
226
|
+
assert sf.equals(sofa, sofa_read)
|
227
|
+
|
228
|
+
# test deleting an entry
|
229
|
+
delattr(sofa, "Temperature_Units")
|
230
|
+
assert not hasattr(sofa, "Temperature_Units")
|
231
|
+
assert "Temperature_Units" not in sofa._custom
|
232
|
+
|
233
|
+
# test adding missing entry defined in convention
|
234
|
+
sofa.protected = False
|
235
|
+
delattr(sofa, "ListenerPosition")
|
236
|
+
sofa.protected = True
|
237
|
+
sofa.add_variable("ListenerPosition", [0, 0, 0], "double", "IC")
|
238
|
+
assert "ListenerPosition" not in sofa._custom
|
239
|
+
|
240
|
+
# test assertions
|
241
|
+
# add existing entry
|
242
|
+
with raises(ValueError, match="Entry Temperature already exists"):
|
243
|
+
sofa.add_variable("Temperature", 25.1, "double", "MI")
|
244
|
+
# entry violating the naming convention
|
245
|
+
with raises(ValueError, match="underscores '_' in the name"):
|
246
|
+
sofa.add_variable("Temperature_Celsius", 25.1, "double", "MI")
|
247
|
+
with raises(ValueError, match="The name of Data"):
|
248
|
+
sofa.add_attribute("Data_Time_measured", "midnight")
|
249
|
+
# entry with wrong type
|
250
|
+
with raises(ValueError, match="dtype is float but must be"):
|
251
|
+
sofa.add_variable("TemperatureCelsius", 25.1, "float", "MI")
|
252
|
+
# variable without dimensions
|
253
|
+
with raises(ValueError, match="dimensions must be provided"):
|
254
|
+
sofa.add_variable("TemperatureCelsius", 25.1, "double", None)
|
255
|
+
# invalid dimensins
|
256
|
+
with pytest.warns(UserWarning, match="Added custom dimension T"):
|
257
|
+
sofa.add_variable("TemperatureCelsius", [25.1, 25.2], "double", "T")
|
258
|
+
# attribute with missing variable
|
259
|
+
with raises(ValueError, match="Adding Attribute Variable"):
|
260
|
+
sofa.add_attribute("Variable_Unit", "celsius")
|
261
|
+
|
262
|
+
|
263
|
+
@pytest.mark.parametrize("mandatory,optional",
|
264
|
+
[(True, False), (False, True), [True, True]])
|
265
|
+
@pytest.mark.parametrize("verbose", [True, False])
|
266
|
+
def test_add_missing(
|
267
|
+
mandatory, optional, verbose, capfd):
|
268
|
+
|
269
|
+
sofa = sf.Sofa("GeneralTF")
|
270
|
+
|
271
|
+
# attributes for testing
|
272
|
+
man = "GLOBAL_AuthorContact"
|
273
|
+
opt = "GLOBAL_History"
|
274
|
+
|
275
|
+
# remove data before adding it again
|
276
|
+
sofa.protected = False
|
277
|
+
sofa.delete(man)
|
278
|
+
sofa.delete(opt)
|
279
|
+
sofa.protected = False
|
280
|
+
|
281
|
+
# add missing data
|
282
|
+
sofa.add_missing(mandatory, optional, verbose)
|
283
|
+
out, _ = capfd.readouterr()
|
284
|
+
|
285
|
+
if mandatory and optional:
|
286
|
+
assert hasattr(sofa, man) and hasattr(sofa, opt)
|
287
|
+
elif mandatory:
|
288
|
+
assert hasattr(sofa, man) and not hasattr(sofa, opt)
|
289
|
+
elif optional:
|
290
|
+
assert not hasattr(sofa, man) and hasattr(sofa, opt)
|
291
|
+
|
292
|
+
if verbose:
|
293
|
+
if mandatory and optional:
|
294
|
+
assert man in out and opt in out
|
295
|
+
elif mandatory:
|
296
|
+
assert man in out and opt not in out
|
297
|
+
elif optional:
|
298
|
+
assert man not in out and opt in out
|
299
|
+
else:
|
300
|
+
assert out == ""
|
301
|
+
|
302
|
+
|
303
|
+
def test_delete_entry():
|
304
|
+
|
305
|
+
sofa = sf.Sofa("SimpleHeadphoneIR")
|
306
|
+
assert hasattr(sofa, "GLOBAL_History")
|
307
|
+
assert hasattr(sofa, "SourceManufacturer")
|
308
|
+
# delete one optional attribute and variable
|
309
|
+
sofa.delete("GLOBAL_History")
|
310
|
+
sofa.delete("SourceManufacturer")
|
311
|
+
# check if data were removed
|
312
|
+
assert not hasattr(sofa, "GLOBAL_History")
|
313
|
+
assert not hasattr(sofa, "SourceManufacturer")
|
314
|
+
|
315
|
+
|
316
|
+
def test__get_size_and_shape_of_string_var():
|
317
|
+
|
318
|
+
# test with string
|
319
|
+
S, shape = sf.Sofa._get_size_and_shape_of_string_var("four", "key")
|
320
|
+
assert S == 4
|
321
|
+
assert shape == (1, 1)
|
322
|
+
|
323
|
+
# test with single string list
|
324
|
+
S, shape = sf.Sofa._get_size_and_shape_of_string_var(["four"], "key")
|
325
|
+
assert S == 4
|
326
|
+
assert shape == (1, )
|
327
|
+
|
328
|
+
# test with list of strings
|
329
|
+
S, shape = sf.Sofa._get_size_and_shape_of_string_var(
|
330
|
+
["four", "fivee"], "key")
|
331
|
+
assert S == 5
|
332
|
+
assert shape == (2, )
|
333
|
+
|
334
|
+
# test with numpy strings array
|
335
|
+
S, shape = sf.Sofa._get_size_and_shape_of_string_var(
|
336
|
+
np.array(["four", "fivee"], dtype="S256"), "key")
|
337
|
+
assert S == 5
|
338
|
+
assert shape == (2, )
|
339
|
+
|
340
|
+
# test with wrong type
|
341
|
+
with raises(TypeError, match="key must be a string"):
|
342
|
+
sf.Sofa._get_size_and_shape_of_string_var(1, "key")
|
343
|
+
|
344
|
+
|
345
|
+
def test___mandatory():
|
346
|
+
assert sf.Sofa._mandatory("rm")
|
347
|
+
assert not sf.Sofa._mandatory("r")
|
348
|
+
assert not sf.Sofa._mandatory(None)
|
349
|
+
|
350
|
+
|
351
|
+
def test___readonly():
|
352
|
+
assert sf.Sofa._read_only("rm")
|
353
|
+
assert not sf.Sofa._read_only("m")
|
354
|
+
assert not sf.Sofa._read_only(None)
|