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.
Files changed (79) hide show
  1. sofar/__init__.py +13 -7
  2. sofar/io.py +423 -0
  3. sofar/sofa.py +1795 -0
  4. sofar/sofa_conventions/VERSION +1 -0
  5. sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.csv +59 -0
  6. sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.json +444 -0
  7. sofar/{conventions/source → sofa_conventions/conventions}/FreeFieldHRIR_1.0.csv +3 -3
  8. sofar/{conventions → sofa_conventions/conventions}/FreeFieldHRIR_1.0.json +3 -3
  9. sofar/{conventions/source → sofa_conventions/conventions}/FreeFieldHRTF_1.0.csv +2 -2
  10. sofar/{conventions → sofa_conventions/conventions}/FreeFieldHRTF_1.0.json +3 -3
  11. sofar/{conventions/source → sofa_conventions/conventions}/GeneralFIR-E_2.0.csv +2 -2
  12. sofar/{conventions → sofa_conventions/conventions}/GeneralFIR-E_2.0.json +2 -2
  13. sofar/{conventions/source/GeneralFIR_2.0.csv → sofa_conventions/conventions/GeneralFIR_1.0.csv} +2 -2
  14. sofar/{conventions/GeneralFIR_2.0.json → sofa_conventions/conventions/GeneralFIR_1.0.json} +2 -2
  15. sofar/{conventions/source/GeneralFIR_1.0.csv → sofa_conventions/conventions/GeneralSOS_1.0.csv} +11 -11
  16. sofar/{conventions/GeneralFIR_1.0.json → sofa_conventions/conventions/GeneralSOS_1.0.json} +48 -37
  17. sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF-E_1.0.csv +3 -3
  18. sofar/{conventions → sofa_conventions/conventions}/GeneralTF-E_1.0.json +4 -4
  19. sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF_1.0.csv +1 -1
  20. sofar/{conventions → sofa_conventions/conventions}/GeneralTF_1.0.json +1 -1
  21. sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF_2.0.csv +4 -4
  22. sofar/{conventions → sofa_conventions/conventions}/GeneralTF_2.0.json +4 -4
  23. sofar/sofa_conventions/conventions/SimpleFreeFieldHRIR_1.0.csv +47 -0
  24. sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldHRIR_1.0.json +1 -1
  25. sofar/{conventions/source → sofa_conventions/conventions}/SimpleFreeFieldHRSOS_1.0.csv +1 -1
  26. sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldHRSOS_1.0.json +1 -1
  27. sofar/{conventions/source/SimpleFreeFieldHRTF_2.0.csv → sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.csv} +3 -3
  28. sofar/{conventions/SimpleFreeFieldHRTF_2.0.json → sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.json} +4 -4
  29. sofar/{conventions/source → sofa_conventions/conventions}/SimpleHeadphoneIR_1.0.csv +9 -9
  30. sofar/sofa_conventions/conventions/SimpleHeadphoneIR_1.0.json +396 -0
  31. sofar/{conventions/source → sofa_conventions/conventions}/SingleRoomMIMOSRIR_1.0.csv +18 -8
  32. sofar/{conventions → sofa_conventions/conventions}/SingleRoomMIMOSRIR_1.0.json +124 -50
  33. sofar/{conventions/source → sofa_conventions/conventions}/SingleRoomSRIR_1.0.csv +18 -8
  34. sofar/{conventions → sofa_conventions/conventions}/SingleRoomSRIR_1.0.json +124 -50
  35. sofar/{conventions/source → sofa_conventions/conventions/deprecated}/FreeFieldDirectivityTF_1.0.csv +2 -2
  36. sofar/{conventions → sofa_conventions/conventions/deprecated}/FreeFieldDirectivityTF_1.0.json +2 -2
  37. sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.csv +48 -0
  38. sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.csv +43 -0
  39. sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.json +333 -0
  40. sofar/{conventions/source/SimpleFreeFieldHRIR_1.0.csv → sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.csv} +15 -18
  41. sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.json +340 -0
  42. sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_1.0.csv +44 -0
  43. sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_1.0.json +340 -0
  44. sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.1.csv +51 -0
  45. sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.1.json +396 -0
  46. sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.csv +51 -0
  47. sofar/{conventions/SimpleHeadphoneIR_1.0.json → sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.json} +3 -3
  48. sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.2.csv +47 -0
  49. sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.2.json +360 -0
  50. sofar/sofa_conventions/rules/deprecations.json +12 -0
  51. sofar/sofa_conventions/rules/rules.json +800 -0
  52. sofar/sofa_conventions/rules/unit_aliases.json +11 -0
  53. sofar/sofa_conventions/rules/upgrade.json +190 -0
  54. sofar/update_conventions.py +427 -0
  55. sofar/utils.py +315 -0
  56. {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/AUTHORS.rst +1 -0
  57. sofar-1.1.0.dist-info/METADATA +89 -0
  58. sofar-1.1.0.dist-info/RECORD +75 -0
  59. {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/WHEEL +1 -1
  60. {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/top_level.txt +1 -0
  61. tests/__init__.py +0 -0
  62. tests/test_deprecations.py +19 -0
  63. tests/test_io.py +344 -0
  64. tests/test_sofa.py +354 -0
  65. tests/test_sofa_upgrade_conventions.py +102 -0
  66. tests/test_sofa_verify.py +472 -0
  67. tests/test_utils.py +241 -0
  68. sofar/conventions/source/MultiSpeakerBRIR_0.3.csv +0 -48
  69. sofar/sofar.py +0 -2531
  70. sofar-0.3.1.dist-info/METADATA +0 -69
  71. sofar-0.3.1.dist-info/RECORD +0 -46
  72. /sofar/{conventions/source → sofa_conventions/conventions}/SimpleFreeFieldSOS_1.0.csv +0 -0
  73. /sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldSOS_1.0.json +0 -0
  74. /sofar/{conventions/source → sofa_conventions/conventions/deprecated}/GeneralFIRE_1.0.csv +0 -0
  75. /sofar/{conventions → sofa_conventions/conventions/deprecated}/GeneralFIRE_1.0.json +0 -0
  76. /sofar/{conventions → sofa_conventions/conventions/deprecated}/MultiSpeakerBRIR_0.3.json +0 -0
  77. /sofar/{conventions/source → sofa_conventions/conventions/deprecated}/SingleRoomDRIR_0.3.csv +0 -0
  78. /sofar/{conventions → sofa_conventions/conventions/deprecated}/SingleRoomDRIR_0.3.json +0 -0
  79. {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)