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