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
@@ -0,0 +1,102 @@
1
+ import sofar as sf
2
+ from sofar.utils import _get_conventions
3
+ import pytest
4
+ import os
5
+ import json
6
+
7
+ # load deprecation rules
8
+ base = os.path.join(
9
+ os.path.dirname(__file__), "..", "sofar", "sofa_conventions", "rules")
10
+
11
+ with open(os.path.join(base, "upgrade.json"), "r") as file:
12
+ upgrade = json.load(file)
13
+
14
+ # load convention paths
15
+ paths = _get_conventions("paths")
16
+
17
+
18
+ def test_printouts(capfd):
19
+ """
20
+ Test console print for calling upgrade with up to data and deprecated data
21
+ """
22
+
23
+ # test printouts with up to data convention -------------------------------
24
+ sofa = sf.Sofa("SimpleFreeFieldHRIR")
25
+ sofa.upgrade_convention()
26
+ out, _ = capfd.readouterr()
27
+ assert "is up to date" in out
28
+
29
+ # test printouts with outdated conventions --------------------------------
30
+ sofa = sf.Sofa("SimpleFreeFieldTF", verify=False)
31
+ out, _ = capfd.readouterr()
32
+
33
+ # calling with default arguments to get information only
34
+ sofa.upgrade_convention()
35
+ out, _ = capfd.readouterr()
36
+ assert "can be upgraded" in out
37
+ assert "SimpleFreeFieldHRTF v1.0" in out
38
+ assert "invalid" not in out
39
+
40
+ # calling with invalid target
41
+ sofa.upgrade_convention("SimpleFreeFieldHRTF_0.1")
42
+ out, _ = capfd.readouterr()
43
+ assert "can be upgraded" in out
44
+ assert "invalid" in out
45
+ assert "SimpleFreeFieldHRTF v1.0" in out
46
+
47
+ # calling with valid target (nothing to move, remove, no message)
48
+ sofa.upgrade_convention("SimpleFreeFieldHRTF_1.0")
49
+ out, _ = capfd.readouterr()
50
+ assert "can be upgraded" not in out
51
+ assert "invalid" not in out
52
+ assert "Upgrading SimpleFreeFieldTF v1.0" in out
53
+ assert "to SimpleFreeFieldHRTF v1.0" in out
54
+ assert "No data to move" in out
55
+ assert "No data to remove" in out
56
+ assert "All mandatory data contained" in out
57
+
58
+ # calling with valid target (including message)
59
+ sofa = sf.Sofa("SimpleFreeFieldHRIR", version="0.4", verify=False)
60
+ out, _ = capfd.readouterr()
61
+ sofa.upgrade_convention("SimpleFreeFieldHRIR_1.0")
62
+ out, _ = capfd.readouterr()
63
+ assert "Consider to add the optional data 'SourceUp'" in out
64
+
65
+ # calling with valid target (things to move)
66
+ sofa = sf.Sofa("MultiSpeakerBRIR", verify=False)
67
+ out, _ = capfd.readouterr()
68
+ sofa.upgrade_convention("SingleRoomMIMOSRIR_1.0")
69
+ out, _ = capfd.readouterr()
70
+ assert "Moving Data_IR" in out
71
+
72
+
73
+ @pytest.mark.parametrize("path", paths)
74
+ def test_upgrade_conventions(path, capfd):
75
+
76
+ # extract information for testing
77
+ convention, version = os.path.basename(path).split("_")
78
+ version = version[:-5]
79
+ deprecated = "deprecated" in path
80
+
81
+ # get SOFA object and targets for upgrading
82
+ sofa = sf.Sofa(convention, version=version, verify=False)
83
+ out, _ = capfd.readouterr()
84
+ targets = sofa.upgrade_convention()
85
+ out, _ = capfd.readouterr()
86
+
87
+ # don't verify conventions that might require user action after
88
+ if os.path.basename(path) in ["FreeFieldDirectivityTF_1.0.json"]:
89
+ # FreeFieldDirectivityTF_1.0
90
+ # - optional dependency GLOBAL_EmitterDescription
91
+ # might need to be added
92
+ verify = False
93
+ else:
94
+ verify = True
95
+
96
+ if targets:
97
+ for target in targets:
98
+ sofa.upgrade_convention(target, verify=verify)
99
+ out, _ = capfd.readouterr()
100
+ assert "Upgrading" in out
101
+ else:
102
+ assert not deprecated
@@ -0,0 +1,472 @@
1
+ """Tests for sofar.Sofa.verify()"""
2
+ import sofar as sf
3
+ from sofar.utils import _complete_sofa
4
+ import os
5
+ from glob import glob
6
+ import pytest
7
+ from pytest import raises
8
+ import numpy as np
9
+
10
+ # get verification rules
11
+ _, unit_aliases, _, _ = sf.Sofa._verification_rules()
12
+
13
+ # directory containing the verificatin data
14
+ basedir = os.path.join(os.path.dirname(__file__), "..", "sofar",
15
+ "sofa_conventions", "data")
16
+
17
+ # files for testing the verification rules
18
+ restricted_values = glob(
19
+ os.path.join(basedir, "restricted_values", "*.sofa"))
20
+ general_dependencies = glob(
21
+ os.path.join(basedir, "general_dependencies", "*.sofa"))
22
+ specific_dependencies = glob(
23
+ os.path.join(basedir, "specific_dependencies", "*.sofa"))
24
+ deprecations = glob(
25
+ os.path.join(basedir, "deprecations", "*.sofa"))
26
+ restricted_dimensions = glob(
27
+ os.path.join(basedir, "restricted_dimensions", "*.sofa"))
28
+
29
+
30
+ # general tests ---------------------------------------------------------------
31
+ def test_verify_value():
32
+ # example alias for testing as returned by Sofa._verification_rules()
33
+ unit_aliases = {"meter": "metre",
34
+ "degrees": "degree"}
35
+
36
+ # Simple pass: no restriction on value
37
+ assert sf.Sofa._verify_value("goofy", None, unit_aliases, "Some_Units")
38
+
39
+ # simple pass: single unit
40
+ assert sf.Sofa._verify_value(
41
+ "meter", ["metre"], unit_aliases, "Some_Units")
42
+
43
+ # complex pass: list of units
44
+ assert sf.Sofa._verify_value("degrees, degrees, meter",
45
+ ["degree, degree, metre"],
46
+ unit_aliases, "Some_Units")
47
+
48
+ # complex pass: list of units with other separators allowed by AES69
49
+ assert sf.Sofa._verify_value("degrees,degrees, meter",
50
+ ["degree, degree, metre"],
51
+ unit_aliases, "Some_Units")
52
+
53
+ # simple fail: single unit
54
+ assert not sf.Sofa._verify_value("centimeter", ["metre"],
55
+ unit_aliases, "Some_Units")
56
+
57
+ # complex fail: list of units
58
+ assert not sf.Sofa._verify_value("rad, rad, metre",
59
+ ["degree, degree, metre"],
60
+ unit_aliases, "Some_Units")
61
+
62
+
63
+ def test_issue_handling(capfd):
64
+ """Test different methods for handling issues during verification"""
65
+
66
+ error_msg = "\nERRORS\n------\n"
67
+ warning_msg = "\nWARNINGS\n--------\n"
68
+
69
+ # no issue
70
+ issue_handling = "raise"
71
+ error_occurred, issues = sf.Sofa._verify_handle_issues(
72
+ warning_msg, error_msg, issue_handling)
73
+ assert not error_occurred
74
+ assert issues is None
75
+
76
+ # raise
77
+ issue_handling = "raise"
78
+ with pytest.warns(UserWarning, match="warning"):
79
+ sf.Sofa._verify_handle_issues(
80
+ warning_msg + "warning", error_msg, issue_handling)
81
+ with raises(ValueError, match="error"):
82
+ sf.Sofa._verify_handle_issues(
83
+ warning_msg, error_msg + "error", issue_handling)
84
+
85
+ # report warning
86
+ issue_handling = "report"
87
+ with pytest.warns(None) as warning:
88
+ _, issues = sf.Sofa._verify_handle_issues(
89
+ warning_msg + "warning", error_msg, issue_handling)
90
+ assert "warning" in issues
91
+ assert "ERROR" not in issues
92
+ assert len(warning) == 0
93
+ # report error
94
+ _, issues = sf.Sofa._verify_handle_issues(
95
+ warning_msg, error_msg + "error", issue_handling)
96
+ assert "error" in issues
97
+ assert "WARNING" not in issues
98
+
99
+ # test invalid data for netCDF attribute
100
+ issue_handling = "ignore"
101
+ sofa = sf.Sofa("GeneralFIR")
102
+ sofa.GLOBAL_Comment = [1, 2, 3]
103
+ issues = sofa.verify(issue_handling=issue_handling)
104
+
105
+ assert issues is None
106
+ assert capfd.readouterr() == ("", "")
107
+
108
+
109
+ def test_case_insensitivity():
110
+ """
111
+ Reading applications shall ignore the case of
112
+ - units and
113
+ - types of coordinate systems
114
+ """
115
+
116
+ # data type (must be case sensitive) --------------------------------------
117
+ sofa = sf.Sofa("SimpleFreeFieldHRIR")
118
+ sofa.protected = False
119
+ sofa.GLOBAL_DataType = "fir"
120
+ sofa.protected = True
121
+ with raises(ValueError, match="GLOBAL_DataType is fir"):
122
+ sofa.verify()
123
+
124
+ # room type ---------------------------------------------------------------
125
+ sofa = sf.Sofa("FreeFieldHRIR")
126
+ sofa.GLOBAL_RoomType = "Free field"
127
+ assert sofa.verify() is None
128
+
129
+ # units -------------------------------------------------------------------
130
+ # example alias for testing as returned by Sofa._verification_rules()
131
+ unit_aliases = {"meter": "metre",
132
+ "degrees": "degree"}
133
+
134
+ # case insensitive testing
135
+ assert sf.Sofa._verify_value(
136
+ "Meter", ["meter"], unit_aliases, "Some_Units")
137
+ assert sf.Sofa._verify_value("DegrEes, dEgreeS, MeTer",
138
+ ["degree, degree, metre"],
139
+ unit_aliases, "Some_Units")
140
+
141
+ sofa = sf.Sofa("FreeFieldDirectivityTF")
142
+ sofa.N_Units = "HertZ"
143
+ assert sofa.verify(issue_handling="return", mode="read") is None
144
+
145
+ # coordinate types --------------------------------------------------------
146
+ sofa = sf.Sofa("SimpleFreeFieldHRIR")
147
+ sofa.ListenerPosition_Type = "Cartesian"
148
+ assert sofa.verify(issue_handling="return") is None
149
+
150
+
151
+ # 1. check if the mandatory attributes are contained --------------------------
152
+ def test_missing_default_attributes(capfd):
153
+
154
+ # test missing default attribute
155
+ sofa = sf.Sofa("GeneralTF")
156
+ sofa.protected = False
157
+ delattr(sofa, "GLOBAL_Conventions")
158
+ sofa.protected = True
159
+
160
+ # raises error
161
+ with raises(ValueError, match="Detected missing mandatory data"):
162
+ sofa.verify(issue_handling="raise")
163
+
164
+ # prints warning and adds data
165
+ sofa.verify(issue_handling="print")
166
+ out, _ = capfd.readouterr()
167
+
168
+ assert "Added mandatory data with default values" in out
169
+ assert sofa.GLOBAL_Conventions == "SOFA"
170
+
171
+
172
+ # 2. verify data type ---------------------------------------------------------
173
+ def test_data_types(capfd):
174
+
175
+ # test invalid data for netCDF attribute
176
+ sofa = sf.Sofa("GeneralFIR")
177
+ sofa.GLOBAL_Comment = [1, 2, 3]
178
+ with raises(ValueError, match="- GLOBAL_Comment must be string"):
179
+ sofa.verify()
180
+
181
+ # test invalid data for netCDF double variable
182
+ sofa = sf.Sofa("GeneralFIR")
183
+ sofa.Data_IR = np.array("test")
184
+ with raises(ValueError, match="- Data_IR must be int or float"):
185
+ sofa.verify()
186
+
187
+ sofa.Data_IR = "1"
188
+ with raises(ValueError, match="- Data_IR must be int, float"):
189
+ sofa.verify()
190
+
191
+ sofa.Data_IR = 1+1j
192
+ with raises(ValueError, match="- Data_IR must be int, float"):
193
+ sofa.verify()
194
+
195
+ # test invalid data with issue_handling "print" and "return"
196
+ sofa = sf.Sofa("GeneralFIR")
197
+ sofa.Data_IR = np.array("test")
198
+ out, _ = capfd.readouterr()
199
+ issues = sofa.verify(issue_handling="print")
200
+ out, _ = capfd.readouterr()
201
+ assert issues is None
202
+ assert "- Data_IR must be int or float" in out
203
+
204
+ issues = sofa.verify(issue_handling="return")
205
+ out, _ = capfd.readouterr()
206
+ assert "- Data_IR must be int or float" in issues
207
+ assert "- Data_IR must be int or float" not in out
208
+
209
+ # test valid data
210
+ sofa.Data_IR = np.array([1])
211
+ sofa.verify()
212
+ sofa.Data_IR = [1]
213
+ sofa.verify()
214
+ sofa.Data_IR = 1
215
+ sofa.verify()
216
+
217
+ # test invalid data for netCDF attribute
218
+ sofa = sf.Sofa("GeneralFIR")
219
+ sofa.GLOBAL_History = 1
220
+ with raises(ValueError, match="- GLOBAL_History must be string"):
221
+ sofa.verify()
222
+
223
+ # test invalid data for netCDF string variable
224
+ sofa = sf.Sofa("SimpleHeadphoneIR")
225
+ sofa.SourceModel = 1
226
+ with raises(ValueError, match="- SourceModel must be string"):
227
+ sofa.verify()
228
+
229
+ sofa.SourceModel = np.array(1)
230
+ with raises(ValueError, match="- SourceModel must be U or S"):
231
+ sofa.verify()
232
+
233
+ # test valid data
234
+ sofa.SourceModel = ["test"]
235
+ sofa.verify()
236
+ sofa.SourceModel = np.array(["test"])
237
+ sofa.verify()
238
+
239
+
240
+ # 3. Verify names of entries --------------------------------------------------
241
+ def test_wrong_name():
242
+
243
+ # attribute with missing variable
244
+ sofa = sf.Sofa("GeneralTF")
245
+ sofa.protected = False
246
+ sofa.IR_Type = "pressure"
247
+ sofa._custom = {"IR_Type": {"default": None,
248
+ "flags": None,
249
+ "dimensions": None,
250
+ "type": "attribute",
251
+ "comment": ""}}
252
+ sofa.protected = True
253
+
254
+ with raises(ValueError, match="Detected attributes with missing"):
255
+ sofa.verify()
256
+
257
+ # attribute with no underscore
258
+ sofa = sf.Sofa("GeneralTF")
259
+ sofa.protected = False
260
+ sofa.IRType = "pressure"
261
+ sofa._custom = {"IRType": {"default": None,
262
+ "flags": None,
263
+ "dimensions": None,
264
+ "type": "attribute",
265
+ "comment": ""}}
266
+ sofa.protected = True
267
+
268
+ with raises(ValueError, match="Detected attribute names with too many"):
269
+ sofa.verify()
270
+
271
+ # variable with underscore
272
+ sofa = sf.Sofa("GeneralTF")
273
+ sofa.protected = False
274
+ sofa.IR_Data = 1
275
+ sofa._custom = {"IR_Data": {"default": None,
276
+ "flags": None,
277
+ "dimensions": "IM",
278
+ "type": "double",
279
+ "comment": ""}}
280
+ sofa.protected = True
281
+
282
+ with raises(ValueError, match="Detected variable names with too many"):
283
+ sofa.verify()
284
+
285
+ # variables with reserved names
286
+ sofa = sf.Sofa("GeneralTF")
287
+ sofa.add_variable("APIfunk", 1, "double", "I")
288
+ with raises(ValueError, match="with reserved key words"):
289
+ sofa.verify()
290
+
291
+ sofa = sf.Sofa("GeneralTF")
292
+ sofa.add_variable("PRIVATEtreasure", 1, "double", "I")
293
+ with raises(ValueError, match="with reserved key words"):
294
+ sofa.verify()
295
+
296
+ sofa = sf.Sofa("GeneralTF")
297
+ sofa.add_variable("GLOBALdata", 1, "double", "I")
298
+ with raises(ValueError, match="with reserved key words"):
299
+ sofa.verify()
300
+
301
+
302
+ def test_custom_data_name():
303
+ """
304
+ Custom entries can not have the names of data contained in the convention.
305
+ """
306
+
307
+ sofa = sf.Sofa("GeneralTF")
308
+ # add variable Origin, although GLOBAL_Origin exists
309
+ sofa.add_variable("Origin", 1, "double", 'I')
310
+ with raises(ValueError,
311
+ match="custom variable or attribute with reserved names"):
312
+ sofa.verify()
313
+
314
+
315
+ # 4 + 5. get and verify dimensions of data ------------------------------------
316
+ def test_wrong_shape():
317
+
318
+ # test attribute with wrong shape
319
+ sofa = sf.Sofa("GeneralTF")
320
+ sofa.ListenerPosition = 1
321
+ with raises(ValueError, match=("- ListenerPosition has shape")):
322
+ sofa.verify()
323
+
324
+
325
+ # 6. check restrictions on the content of SOFA files --------------------------
326
+ def test_rules_error_messages():
327
+ """
328
+ Test the exact error messages raised by violated rules from rules.json. The
329
+ remaining test check only if errors are raised.
330
+ """
331
+
332
+ # wrong value
333
+ sofa = _complete_sofa()
334
+ sofa.GLOBAL_RoomType = "pentagon"
335
+ error = "GLOBAL_RoomType is pentagon but must be free field, reverberant"
336
+ with raises(ValueError, match=error):
337
+ sofa.verify()
338
+
339
+ # missing general dependency
340
+ sofa = _complete_sofa()
341
+ sofa.delete("ListenerView_Type")
342
+ error = "ListenerView_Type must be given if ListenerView is in SOFA object"
343
+ with raises(ValueError, match=error):
344
+ sofa.verify()
345
+
346
+ # wrong dimensions
347
+ sofa = sf.Sofa("SimpleFreeFieldHRSOS")
348
+ sofa.Data_SOS = np.zeros((1, 2, 1))
349
+ error = ("Dimension N is of size 1 but must be an integer multiple of 6 "
350
+ "greater 0 if GLOBAL_DataType is SOS")
351
+ with raises(ValueError, match=error):
352
+ sofa.verify()
353
+
354
+ # missing specific dependency
355
+ sofa = _complete_sofa()
356
+ sofa.GLOBAL_RoomType = "reverberant"
357
+ sofa.delete("GLOBAL_RoomDescription")
358
+ error = ("GLOBAL_RoomDescription must be given "
359
+ "if GLOBAL_RoomType is reverberant")
360
+ with raises(ValueError, match=error):
361
+ sofa.verify()
362
+
363
+ # wrong value for specific dependency
364
+ sofa = _complete_sofa()
365
+ sofa.ListenerPosition_Type = "spherical"
366
+ error = ("ListenerPosition_Units is metre but must be degree, degree, "
367
+ "metre if ListenerPositionType is spherical")
368
+
369
+
370
+ @pytest.mark.parametrize("file", restricted_values)
371
+ def test_restricted_values(file):
372
+ """Test all rules that restrict data to certain values"""
373
+
374
+ try:
375
+ # load file without verification
376
+ sofa = sf.read_sofa(file, verify=False)
377
+ # test verification
378
+ with raises(ValueError, match="invalid-value"):
379
+ sofa.verify()
380
+ except ValueError as VE:
381
+ assert str(VE) == "Convention 'invalid-value' does not exist"
382
+
383
+
384
+ @pytest.mark.parametrize("file", general_dependencies)
385
+ def test_general_dependencies(file):
386
+ """Test all rules that restrict data to certain values"""
387
+
388
+ # load file without verification
389
+ sofa = sf.read_sofa(file, verify=False)
390
+
391
+ # test verification
392
+ with raises(ValueError, match="missing|must be given"):
393
+ sofa.verify()
394
+
395
+
396
+ @pytest.mark.parametrize("file", specific_dependencies)
397
+ def test_specific_dependencies(file):
398
+
399
+ # load file without verification
400
+ sofa = sf.read_sofa(file, verify=False)
401
+
402
+ # extract match for error message ('invalid-value' or 'missing')
403
+ match = file[file.rfind("=")+1:file.rfind(".")]
404
+
405
+ if match == "missing":
406
+ match += "|must be given"
407
+
408
+ with raises(ValueError, match=match):
409
+ sofa.verify()
410
+
411
+
412
+ @pytest.mark.parametrize("file", restricted_dimensions)
413
+ def test_restricted_dimensions(file):
414
+
415
+ # load file without verification
416
+ sofa = sf.read_sofa(file, verify=False)
417
+
418
+ # extract data for error message from filename
419
+ match = f"Dimension {file[-8]} is of size {file[-6]} but must be"
420
+
421
+ # test verification
422
+ with raises(ValueError, match=match):
423
+ sofa.verify()
424
+
425
+
426
+ # 7. check write only restrictions --------------------------------------------
427
+ def test_read_and_write_mode():
428
+
429
+ # Unit with uppercase is ok when reading but not ok when writing
430
+ sofa = sf.Sofa("SimpleFreeFieldHRIR")
431
+ sofa.ListenerPosition_Units = "Meter"
432
+
433
+ assert sofa.verify(mode="read", issue_handling="return") is None
434
+ with raises(ValueError, match="lower case letters when writing"):
435
+ sofa.verify(mode="write")
436
+
437
+
438
+ # 8. check deprecations -------------------------------------------------------
439
+ @pytest.mark.parametrize("file", deprecations)
440
+ def test_deprecations(file):
441
+ """
442
+ Test if deprecations raise warnings in read mode and errors in write mode.
443
+ """
444
+
445
+ # read file without verification
446
+ sofa = sf.read_sofa(file, verify=False)
447
+
448
+ # check if deprecated and substitute convention exist in sofar
449
+ conventions = sf.utils._get_conventions("name")
450
+ deprecated = sofa.GLOBAL_SOFAConventions
451
+
452
+ if deprecated not in conventions:
453
+ return
454
+
455
+ # check warnings and errors
456
+ sofa = sf.Sofa(deprecated, verify=False)
457
+
458
+ msg = ("Detected deprecations:\n"
459
+ f"- GLOBAL_SOFAConventions is {deprecated}, which is deprecated. ")
460
+
461
+ with pytest.warns(UserWarning, match=msg):
462
+ sofa.verify(mode="read")
463
+
464
+ with raises(ValueError, match=msg):
465
+ sofa.verify(mode="write")
466
+
467
+
468
+ def test_preliminary_conventions_version():
469
+ """Test if using a convention version < 1.0 issues a warning"""
470
+
471
+ with pytest.warns(UserWarning, match="Detected preliminary"):
472
+ sf.Sofa("SingleRoomDRIR", version="0.3")