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,11 @@
1
+ {
2
+ "metre": "metre",
3
+ "metres": "metre",
4
+ "meter": "metre",
5
+ "meters": "metre",
6
+ "cubic": "cubic",
7
+ "degree": "degree",
8
+ "degrees": "degree",
9
+ "second": "second",
10
+ "seconds": "second"
11
+ }
@@ -0,0 +1,190 @@
1
+ {
2
+ "FreeFieldDirectivityTF": {
3
+ "from_to": [
4
+ [
5
+ [
6
+ "1.0"
7
+ ],
8
+ [
9
+ "FreeFieldDirectivityTF_1.1"
10
+ ],
11
+ "1"
12
+ ]
13
+ ],
14
+ "1": {
15
+ "move": {
16
+ "EmitterPosition": {
17
+ "target": "EmitterPosition",
18
+ "moveaxis": null,
19
+ "deprecated_dimensions": [
20
+ "IC",
21
+ "MC"
22
+ ]
23
+ },
24
+ "EmitterDescription": {
25
+ "target": "EmitterDescriptions",
26
+ "moveaxis": null,
27
+ "deprecated_dimensions": [
28
+ "IS"
29
+ ]
30
+ }
31
+ },
32
+ "remove": [],
33
+ "message": "Consider to add the optional data 'GLOBAL_EmitterDescription'introduced in convention version 1.1.\nWARNING: Adding 'GLOBAL_EmitterDescription' is required if 'EmitterDescriptions' is contained in the SOFA object."
34
+ }
35
+ },
36
+ "SimpleFreeFieldHRIR": {
37
+ "from_to": [
38
+ [
39
+ [
40
+ "0.4"
41
+ ],
42
+ [
43
+ "SimpleFreeFieldHRIR_1.0"
44
+ ],
45
+ "1"
46
+ ]
47
+ ],
48
+ "1": {
49
+ "move": {},
50
+ "remove": [],
51
+ "message": "Consider to add the optional data 'SourceUp', 'SourceView', 'SourceView:Type', and 'SourceView:Units' with default values that were introduced in convention version 1.0"
52
+ }
53
+ },
54
+ "SimpleFreeFieldTF": {
55
+ "from_to": [
56
+ [
57
+ [
58
+ "0.4",
59
+ "1.0"
60
+ ],
61
+ [
62
+ "SimpleFreeFieldHRTF_1.0"
63
+ ],
64
+ "1"
65
+ ]
66
+ ],
67
+ "1": {
68
+ "move": {},
69
+ "remove": [],
70
+ "message": null
71
+ }
72
+ },
73
+ "SimpleHeadphoneIR": {
74
+ "from_to": [
75
+ [
76
+ [
77
+ "0.1",
78
+ "0.2"
79
+ ],
80
+ [
81
+ "SimpleHeadphoneIR_1.0"
82
+ ],
83
+ "1"
84
+ ]
85
+ ],
86
+ "1": {
87
+ "move": {
88
+ "ReceiverDescription": {
89
+ "target": "ReceiverDescriptions",
90
+ "moveaxis": null,
91
+ "deprecated_dimensions": null
92
+ },
93
+ "EmitterDescription": {
94
+ "target": "EmitterDescriptions",
95
+ "moveaxis": null,
96
+ "deprecated_dimensions": null
97
+ }
98
+ },
99
+ "remove": [],
100
+ "message": null
101
+ }
102
+ },
103
+ "SingleRoomDRIR": {
104
+ "from_to": [
105
+ [
106
+ [
107
+ "0.2",
108
+ "0.3"
109
+ ],
110
+ [
111
+ "SingleRoomSRIR_1.0"
112
+ ],
113
+ "1"
114
+ ]
115
+ ],
116
+ "1": {
117
+ "move": {},
118
+ "remove": [],
119
+ "message": "Consider providing optional data that was introduced in SingleRoomSRIR version 1.0"
120
+ }
121
+ },
122
+ "MultiSpeakerBRIR": {
123
+ "from_to": [
124
+ [
125
+ [
126
+ "0.3"
127
+ ],
128
+ [
129
+ "SingleRoomMIMOSRIR_1.0"
130
+ ],
131
+ "1"
132
+ ]
133
+ ],
134
+ "1": {
135
+ "move": {
136
+ "Data.IR": {
137
+ "target": "Data.IR",
138
+ "moveaxis": [
139
+ 3,
140
+ 2
141
+ ],
142
+ "deprecated_dimensions": null
143
+ },
144
+ "Data.Delay": {
145
+ "target": "Data.Delay",
146
+ "moveaxis": null,
147
+ "deprecated_dimensions": [
148
+ "IRE"
149
+ ]
150
+ }
151
+ },
152
+ "remove": [],
153
+ "message": "Consider providing optional data that was introduced in SingleRoomSRIR version 1.0"
154
+ }
155
+ },
156
+ "GeneralFIRE": {
157
+ "from_to": [
158
+ [
159
+ [
160
+ "1.0"
161
+ ],
162
+ [
163
+ "GeneralFIR-E_2.0"
164
+ ],
165
+ "1"
166
+ ]
167
+ ],
168
+ "1": {
169
+ "move": {
170
+ "Data.IR": {
171
+ "target": "Data.IR",
172
+ "moveaxis": [
173
+ 3,
174
+ 2
175
+ ],
176
+ "deprecated_dimensions": null
177
+ },
178
+ "EmitterPosition": {
179
+ "target": "EmitterPosition",
180
+ "moveaxis": null,
181
+ "deprecated_dimensions": [
182
+ "ECI"
183
+ ]
184
+ }
185
+ },
186
+ "remove": [],
187
+ "message": "Consider providing optional data that was introduced in SingleRoomSRIR version 1.0"
188
+ }
189
+ }
190
+ }
@@ -0,0 +1,427 @@
1
+ import contextlib
2
+ import os
3
+ import glob
4
+ import json
5
+ import requests
6
+ from bs4 import BeautifulSoup
7
+
8
+
9
+ def update_conventions(conventions_path=None, assume_yes=False):
10
+ """
11
+ Update SOFA conventions.
12
+
13
+ SOFA convention define what data is stored in a SOFA file and how it is
14
+ stored. Updating makes sure that sofar is using the latest conventions.
15
+ This is done in three steps
16
+
17
+ 1.
18
+ Download official SOFA conventions as csv files from
19
+ https://www.sofaconventions.org/conventions/ and
20
+ https://www.sofaconventions.org/conventions/deprecated/.
21
+ 2.
22
+ Convert csv files to json files to be read by sofar.
23
+ 3.
24
+ Notify which conventions were newly added or updated.
25
+
26
+ The csv and json files are stored at sofar/conventions. Sofar works only on
27
+ the json files. To get a list of all currently available SOFA conventions
28
+ and their paths see :py:func:`~sofar.list_conventions`.
29
+
30
+ .. note::
31
+ If the official convention contain errors, calling this function might
32
+ break sofar. If this is the case sofar must be re-installed, e.g., by
33
+ running ``pip install --force-reinstall sofar``. Be sure that you want
34
+ to do this.
35
+
36
+ Parameters
37
+ ----------
38
+ conventions_path : str, optional
39
+ Path to the folder where the conventions are saved. The default is
40
+ ``None``, which saves the conventions inside the sofar package.
41
+ Conventions saved under a different path can not be used by sofar. This
42
+ parameter was added mostly for testing and debugging.
43
+ response : bool, optional
44
+
45
+ ``True``
46
+ Updating the conventions must be confirmed by typing "y".
47
+ ``False``
48
+ The conventions are updated without confirmation.
49
+
50
+ The default is ``True``
51
+ """
52
+
53
+ if not assume_yes:
54
+ # these lines were only tested manually. I was too lazy to write a test
55
+ # coping with keyboard input
56
+ print(("Are you sure that you want to update the conventions? "
57
+ "Read the documentation before continuing. "
58
+ "If updateing breaks sofar it has to be re-installed"
59
+ "(y/n)"))
60
+ response = input()
61
+ if response != "y":
62
+ print("Updating the conventions was canceled.")
63
+ return
64
+
65
+ # url for parsing and downloading the convention files
66
+ urls = ("https://www.sofaconventions.org/conventions/",
67
+ "https://www.sofaconventions.org/conventions/deprecated/")
68
+ ext = 'csv'
69
+
70
+ print(f"Reading SOFA conventions from {urls[0]} ...")
71
+
72
+ # get file names of conventions from sofaconventions.org
73
+ page = requests.get(urls[0]).text
74
+ soup = BeautifulSoup(page, 'html.parser')
75
+ standardized = [os.path.split(node.get('href'))[1]
76
+ for node in soup.find_all('a')
77
+ if node.get('href').endswith(ext)]
78
+ page = requests.get(urls[1]).text
79
+ soup = BeautifulSoup(page, 'html.parser')
80
+ deprecated = [os.path.split(node.get('href'))[1]
81
+ for node in soup.find_all('a')
82
+ if node.get('href').endswith(ext)]
83
+
84
+ conventions = standardized + deprecated
85
+
86
+ # directory handling
87
+ if conventions_path is None:
88
+ conventions_path = os.path.join(
89
+ os.path.dirname(__file__), "sofa_conventions", "conventions")
90
+ if not os.path.isdir(conventions_path):
91
+ os.mkdir(conventions_path)
92
+ if not os.path.isdir(os.path.join(conventions_path, "deprecated")):
93
+ os.mkdir(os.path.join(conventions_path, "deprecated"))
94
+
95
+ # Loop and download conventions if they changed
96
+ updated = False
97
+ for convention in conventions:
98
+
99
+ # exclude these conventions
100
+ if convention.startswith(("General_", "GeneralString_")):
101
+ continue
102
+
103
+ # get filename and url
104
+ is_standardized = convention in standardized
105
+ standardized_csv = os.path.join(conventions_path, convention)
106
+ deprecated_csv = os.path.join(
107
+ conventions_path, "deprecated", convention)
108
+ url = (
109
+ f"{urls[0]}/{convention}"
110
+ if is_standardized
111
+ else f"{urls[1]}/{convention}"
112
+ )
113
+
114
+ # download SOFA convention definitions to package directory
115
+ data = requests.get(url)
116
+ # remove windows style line breaks and trailing tabs
117
+ data = data.content.replace(b"\r\n", b"\n").replace(b"\t\n", b"\n")
118
+
119
+ # check if convention needs to be added or updated
120
+ if is_standardized and not os.path.isfile(standardized_csv):
121
+ # add new standardized convention
122
+ updated = True
123
+ with open(standardized_csv, "wb") as file:
124
+ file.write(data)
125
+ print(f"- added convention: {convention[:-4]}")
126
+ if is_standardized and os.path.isfile(standardized_csv):
127
+ # check for update of a standardized convention
128
+ with open(standardized_csv, "rb") as file:
129
+ data_current = b"".join(file.readlines())
130
+ data_current = data_current.replace(
131
+ b"\r\n", b"\n").replace(b"\t\n", b"\n")
132
+ if data_current != data:
133
+ updated = True
134
+ with open(standardized_csv, "wb") as file:
135
+ file.write(data)
136
+ print(f"- updated convention: {convention[:-4]}")
137
+ elif not is_standardized and os.path.isfile(standardized_csv):
138
+ # deprecate standardized convention
139
+ updated = True
140
+ with open(deprecated_csv, "wb") as file:
141
+ file.write(data)
142
+ os.remove(standardized_csv)
143
+ os.remove(f"{standardized_csv[:-3]}json")
144
+ print(f"- deprecated convention: {convention[:-4]}")
145
+ elif not is_standardized and os.path.isfile(deprecated_csv):
146
+ # check for update of a deprecated convention
147
+ with open(deprecated_csv, "rb") as file:
148
+ data_current = b"".join(file.readlines())
149
+ data_current = data_current.replace(
150
+ b"\r\n", b"\n").replace(b"\t\n", b"\n")
151
+ if data_current != data:
152
+ updated = True
153
+ with open(deprecated_csv, "wb") as file:
154
+ file.write(data)
155
+ print(f"- updated deprecated convention: {convention[:-4]}")
156
+ elif not is_standardized and not os.path.isfile(deprecated_csv):
157
+ # add new deprecation
158
+ updated = True
159
+ with open(deprecated_csv, "wb") as file:
160
+ file.write(data)
161
+ print(f"- added deprecated convention: {convention[:-4]}")
162
+
163
+ if updated:
164
+ # compile json files from csv file
165
+ _compile_conventions(conventions_path)
166
+ print("... done.")
167
+ else:
168
+ print("... conventions already up to date.")
169
+
170
+
171
+ def _compile_conventions(conventions_path=None):
172
+ """
173
+ Compile SOFA conventions (json files) from source conventions (csv files
174
+ from SOFA SOFAtoolbox), i.e., only do step 2 from `update_conventions`.
175
+ This is a helper function for debugging and developing and might break
176
+ sofar.
177
+
178
+ Parameters
179
+ ----------
180
+ conventions_path : str
181
+ Path to the `conventions`folder containing csv and json files. The
182
+ default ``None`` uses the default location inside the sofar package.
183
+ """
184
+ # directory handling
185
+ if conventions_path is None:
186
+ conventions_path = os.path.join(
187
+ os.path.dirname(__file__), "sofa_conventions", "conventions")
188
+ if not os.path.isdir(conventions_path):
189
+ raise ValueError(f"{conventions_path} does not exist")
190
+
191
+ # get list of source conventions
192
+ csv_files = glob.glob(os.path.join(conventions_path, "*.csv")) + \
193
+ glob.glob(os.path.join(conventions_path, "deprecated", "*.csv"))
194
+
195
+ for csv_file in csv_files:
196
+
197
+ # convert SOFA conventions from csv to json
198
+ convention_dict = _convention_csv2dict(csv_file)
199
+ with open(f"{csv_file[:-3]}json", 'w') as file:
200
+ json.dump(convention_dict, file, indent=4)
201
+
202
+
203
+ def _convention_csv2dict(file: str):
204
+ """
205
+ Read a SOFA convention as csv file from the official Matlab/Octave API for
206
+ SOFA (SOFAtoolbox) and convert them to a Python dictionary. The dictionary
207
+ can be written for example to a json file using
208
+
209
+ import json
210
+
211
+ with open(filename, 'w') as file:
212
+ json.dump(dictionary, file, indent=4)
213
+
214
+ Parameters
215
+ ----------
216
+ file : str
217
+ filename of the SOFA convention
218
+
219
+ Returns
220
+ -------
221
+ convention : dict
222
+ SOFA convention as nested dictionary. Each attribute is a sub
223
+ dictionary with the keys `default`, `flags`, `dimensions`, `type`, and
224
+ `comment`.
225
+ """
226
+
227
+ # read the file
228
+ # (encoding should be changed to utf-8 after the SOFA conventions repo is
229
+ # clean.)
230
+ # TODO: add explicit test for this function that checks the output
231
+ with open(file, 'r', encoding="windows-1252") as fid:
232
+ lines = fid.readlines()
233
+
234
+ # write into dict
235
+ convention = {}
236
+ for idl, line in enumerate(lines):
237
+
238
+ try:
239
+ # separate by tabs
240
+ line = line.strip().split("\t")
241
+ # parse the line entry by entry
242
+ for idc, cell in enumerate(line):
243
+ # detect empty cells and leading trailing white spaces
244
+ cell = None if cell.replace(' ', '') == '' else cell.strip()
245
+ # nothing to do for empty cells
246
+ if cell is None:
247
+ line[idc] = cell
248
+ continue
249
+ # parse text cells that do not contain arrays
250
+ if cell[0] != '[':
251
+ # check for numbers
252
+ with contextlib.suppress(ValueError):
253
+ cell = float(cell) if '.' in cell else int(cell)
254
+ line[idc] = cell
255
+ continue
256
+
257
+ # parse array cell
258
+ # remove brackets
259
+ cell = cell[1:-1]
260
+
261
+ if ';' not in cell:
262
+ # get rid of white spaces
263
+ cell = cell.strip()
264
+ cell = cell.replace(' ', ',')
265
+ cell = cell.replace(' ', '')
266
+ # create flat list of integers and floats
267
+ numbers = cell.split(',')
268
+ cell = [float(n) if '.' in n else int(n) for n in numbers]
269
+ else:
270
+ # create a nested list of integers and floats
271
+ # separate multidimensional arrays
272
+ cell = cell.split(';')
273
+ cell_nd = [None] * len(cell)
274
+ for idx, cc in enumerate(cell):
275
+ # get rid of white spaces
276
+ cc = cc.strip()
277
+ cc = cc.replace(' ', ',')
278
+ cc = cc.replace(' ', '')
279
+ numbers = cc.split(',')
280
+ cell_nd[idx] = [float(n) if '.' in n else int(n)
281
+ for n in numbers]
282
+
283
+ cell = cell_nd
284
+
285
+ # write parsed cell to line
286
+ line[idc] = cell
287
+
288
+ # first line contains field names
289
+ if idl == 0:
290
+ fields = line[1:]
291
+ continue
292
+
293
+ # add blank comment if it does not exist
294
+ if len(line) == 5:
295
+ line.append("")
296
+ # convert empty defaults from None to ""
297
+ if line[1] is None:
298
+ line[1] = ""
299
+
300
+ # make sure some unusual default values are converted for json
301
+ if line[1] == "permute([0 0 0 1 0 0; 0 0 0 1 0 0], [3 1 2]);":
302
+ # Field Data.SOS in SimpleFreeFieldHRSOS and SimpleFreeFieldSOS
303
+ line[1] = [[[0, 0, 0, 1, 0, 0], [0, 0, 0, 1, 0, 0]]]
304
+ if line[1] == "permute([0 0 0 1 0 0], [3 1 2]);":
305
+ # Field Data.SOS in GeneralSOS
306
+ line[1] = [[[0, 0, 0, 1, 0, 0]]]
307
+ if line[1] == "{''}":
308
+ line[1] = ['']
309
+ # convert versions to strings
310
+ if "Version" in line[0] and not isinstance(line[1], str):
311
+ line[1] = str(float(line[1]))
312
+
313
+ # write second to last line
314
+ convention[line[0]] = {}
315
+ for ff, field in enumerate(fields):
316
+ convention[line[0]][field.lower()] = line[ff + 1]
317
+
318
+ except: # noqa
319
+ raise ValueError((f"Failed to parse line {idl}, entry {idc} in: "
320
+ f"{file}: \n{line}\n"))
321
+
322
+ # reorder the fields to be nicer to read and understand
323
+ # 1. Move everything to the end that is not GLOBAL
324
+ keys = list(convention.keys())
325
+ for key in keys:
326
+ if "GLOBAL" not in key:
327
+ convention[key] = convention.pop(key)
328
+ # 1. Move Data entries to the end
329
+ for key in keys:
330
+ if key.startswith("Data"):
331
+ convention[key] = convention.pop(key)
332
+
333
+ return convention
334
+
335
+
336
+ def _check_congruency(save_dir=None, branch="master"):
337
+ """
338
+ SOFA conventions are stored in two different places - is this a good idea?
339
+ They should be identical, but let's find out.
340
+
341
+ Prints warnings about incongruent conventions
342
+
343
+ Parameters
344
+ ----------
345
+ save : str
346
+ directory to save diverging conventions for further inspections
347
+ """
348
+
349
+ # urls for checking which conventions exist
350
+ urls_check = ["https://www.sofaconventions.org/conventions/",
351
+ ("https://github.com/sofacoustics/SOFAtoolbox/tree/"
352
+ f"{branch}/SOFAtoolbox/conventions/")]
353
+ # urls for loading the convention files
354
+ urls_load = ["https://www.sofaconventions.org/conventions/",
355
+ ("https://raw.githubusercontent.com/sofacoustics/SOFAtoolbox/"
356
+ f"{branch}/SOFAtoolbox/conventions/")]
357
+ subdirs = ["sofaconventions", "sofatoolbox"]
358
+
359
+ # check save_dir
360
+ if save_dir is not None:
361
+ if not os.path.isdir(save_dir):
362
+ raise ValueError(f"{save_dir} does not exist")
363
+ for subdir in subdirs:
364
+ if not os.path.isdir(os.path.join(save_dir, subdir)):
365
+ os.makedirs(os.path.join(save_dir, subdir))
366
+
367
+ # get file names of conventions from sofaconventions.org
368
+ url = urls_check[0]
369
+ page = requests.get(url).text
370
+ soup = BeautifulSoup(page, 'html.parser')
371
+ sofaconventions = [os.path.split(node.get('href'))[1]
372
+ for node in soup.find_all('a')
373
+ if node.get('href').endswith(".csv")]
374
+
375
+ if not sofaconventions:
376
+ raise ValueError(f"Did not find any conventions at {url}")
377
+
378
+ # get file names of conventions from github
379
+ url = urls_check[1]
380
+ page = requests.get(url).json()
381
+ sofatoolbox = []
382
+ for content in page["payload"]["tree"]["items"]:
383
+ if content["contentType"] == "file" and \
384
+ content["path"].startswith("SOFAtoolbox/conventions") and \
385
+ content["name"].endswith("csv"):
386
+ sofatoolbox.append(content["name"])
387
+
388
+ if not sofatoolbox:
389
+ raise ValueError(f"Did not find any conventions at {url}")
390
+
391
+ # check if lists are identical. Remove items not contained in both lists
392
+ report = ""
393
+ for convention in sofaconventions:
394
+ if convention.startswith(("General_", "GeneralString_")):
395
+ sofaconventions.remove(convention)
396
+ elif convention not in sofatoolbox:
397
+ sofaconventions.remove(convention)
398
+ report += (f"- {convention} is missing in SOFAtoolbox\n")
399
+ for convention in sofatoolbox:
400
+ if convention.startswith(("General_", "GeneralString_")):
401
+ sofatoolbox.remove(convention)
402
+ elif convention not in sofaconventions:
403
+ sofatoolbox.remove(convention)
404
+ report += (f"- {convention} is missing on sofaconventions.org\n")
405
+
406
+ # Loop and download conventions to check if they are identical
407
+ for convention in sofaconventions:
408
+
409
+ # download SOFA convention definitions to package directory
410
+ data = [requests.get(url + convention) for url in urls_load]
411
+ # remove trailing tabs and windows style line breaks
412
+ data = [d.content.replace(b"\r\n", b"\n").replace(b"\t\n", b"\n")
413
+ for d in data]
414
+
415
+ # check for equality
416
+ if data[0] != data[1]:
417
+ report += f"- {convention} differs across platforms\n"
418
+
419
+ # save diverging files
420
+ if save_dir is not None:
421
+ for subdir, d in zip(subdirs, data):
422
+ filename = os.path.join(save_dir, subdir, convention)
423
+ with open(filename, "wb") as file:
424
+ file.write(d)
425
+
426
+ if report:
427
+ print("Diverging conventions across platforms:\n" + report)