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