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.
- sofar/__init__.py +13 -7
- sofar/io.py +423 -0
- sofar/sofa.py +1795 -0
- sofar/sofa_conventions/VERSION +1 -0
- sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.csv +59 -0
- sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.json +444 -0
- sofar/{conventions/source → sofa_conventions/conventions}/FreeFieldHRIR_1.0.csv +3 -3
- sofar/{conventions → sofa_conventions/conventions}/FreeFieldHRIR_1.0.json +3 -3
- sofar/{conventions/source → sofa_conventions/conventions}/FreeFieldHRTF_1.0.csv +2 -2
- sofar/{conventions → sofa_conventions/conventions}/FreeFieldHRTF_1.0.json +3 -3
- sofar/{conventions/source → sofa_conventions/conventions}/GeneralFIR-E_2.0.csv +2 -2
- sofar/{conventions → sofa_conventions/conventions}/GeneralFIR-E_2.0.json +2 -2
- sofar/{conventions/source/GeneralFIR_2.0.csv → sofa_conventions/conventions/GeneralFIR_1.0.csv} +2 -2
- sofar/{conventions/GeneralFIR_2.0.json → sofa_conventions/conventions/GeneralFIR_1.0.json} +2 -2
- sofar/{conventions/source/GeneralFIR_1.0.csv → sofa_conventions/conventions/GeneralSOS_1.0.csv} +11 -11
- sofar/{conventions/GeneralFIR_1.0.json → sofa_conventions/conventions/GeneralSOS_1.0.json} +48 -37
- sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF-E_1.0.csv +3 -3
- sofar/{conventions → sofa_conventions/conventions}/GeneralTF-E_1.0.json +4 -4
- sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF_1.0.csv +1 -1
- sofar/{conventions → sofa_conventions/conventions}/GeneralTF_1.0.json +1 -1
- sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF_2.0.csv +4 -4
- sofar/{conventions → sofa_conventions/conventions}/GeneralTF_2.0.json +4 -4
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRIR_1.0.csv +47 -0
- sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldHRIR_1.0.json +1 -1
- sofar/{conventions/source → sofa_conventions/conventions}/SimpleFreeFieldHRSOS_1.0.csv +1 -1
- sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldHRSOS_1.0.json +1 -1
- sofar/{conventions/source/SimpleFreeFieldHRTF_2.0.csv → sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.csv} +3 -3
- sofar/{conventions/SimpleFreeFieldHRTF_2.0.json → sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.json} +4 -4
- sofar/{conventions/source → sofa_conventions/conventions}/SimpleHeadphoneIR_1.0.csv +9 -9
- sofar/sofa_conventions/conventions/SimpleHeadphoneIR_1.0.json +396 -0
- sofar/{conventions/source → sofa_conventions/conventions}/SingleRoomMIMOSRIR_1.0.csv +18 -8
- sofar/{conventions → sofa_conventions/conventions}/SingleRoomMIMOSRIR_1.0.json +124 -50
- sofar/{conventions/source → sofa_conventions/conventions}/SingleRoomSRIR_1.0.csv +18 -8
- sofar/{conventions → sofa_conventions/conventions}/SingleRoomSRIR_1.0.json +124 -50
- sofar/{conventions/source → sofa_conventions/conventions/deprecated}/FreeFieldDirectivityTF_1.0.csv +2 -2
- sofar/{conventions → sofa_conventions/conventions/deprecated}/FreeFieldDirectivityTF_1.0.json +2 -2
- sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.csv +48 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.csv +43 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.json +333 -0
- sofar/{conventions/source/SimpleFreeFieldHRIR_1.0.csv → sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.csv} +15 -18
- 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/{conventions/SimpleHeadphoneIR_1.0.json → sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.json} +3 -3
- 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/rules/deprecations.json +12 -0
- sofar/sofa_conventions/rules/rules.json +800 -0
- sofar/sofa_conventions/rules/unit_aliases.json +11 -0
- sofar/sofa_conventions/rules/upgrade.json +190 -0
- sofar/update_conventions.py +427 -0
- sofar/utils.py +315 -0
- {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/AUTHORS.rst +1 -0
- sofar-1.1.0.dist-info/METADATA +89 -0
- sofar-1.1.0.dist-info/RECORD +75 -0
- {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/WHEEL +1 -1
- {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/top_level.txt +1 -0
- tests/__init__.py +0 -0
- tests/test_deprecations.py +19 -0
- tests/test_io.py +344 -0
- tests/test_sofa.py +354 -0
- tests/test_sofa_upgrade_conventions.py +102 -0
- tests/test_sofa_verify.py +472 -0
- tests/test_utils.py +241 -0
- sofar/conventions/source/MultiSpeakerBRIR_0.3.csv +0 -48
- sofar/sofar.py +0 -2531
- sofar-0.3.1.dist-info/METADATA +0 -69
- sofar-0.3.1.dist-info/RECORD +0 -46
- /sofar/{conventions/source → sofa_conventions/conventions}/SimpleFreeFieldSOS_1.0.csv +0 -0
- /sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldSOS_1.0.json +0 -0
- /sofar/{conventions/source → sofa_conventions/conventions/deprecated}/GeneralFIRE_1.0.csv +0 -0
- /sofar/{conventions → sofa_conventions/conventions/deprecated}/GeneralFIRE_1.0.json +0 -0
- /sofar/{conventions → sofa_conventions/conventions/deprecated}/MultiSpeakerBRIR_0.3.json +0 -0
- /sofar/{conventions/source → sofa_conventions/conventions/deprecated}/SingleRoomDRIR_0.3.csv +0 -0
- /sofar/{conventions → sofa_conventions/conventions/deprecated}/SingleRoomDRIR_0.3.json +0 -0
- {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/LICENSE +0 -0
@@ -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)
|