sofar 1.0.0__py2.py3-none-any.whl → 1.1.1__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 +3 -2
- sofar/io.py +92 -39
- sofar/sofa.py +93 -69
- sofar/sofa_conventions/rules/upgrade.json +34 -0
- sofar/update_conventions.py +27 -14
- sofar/utils.py +18 -38
- {sofar-1.0.0.dist-info → sofar-1.1.1.dist-info}/METADATA +4 -6
- {sofar-1.0.0.dist-info → sofar-1.1.1.dist-info}/RECORD +20 -19
- {sofar-1.0.0.dist-info → sofar-1.1.1.dist-info}/WHEEL +1 -1
- tests/test_deprecations.py +19 -0
- tests/test_io.py +34 -26
- tests/test_sofa.py +4 -9
- tests/test_sofa_upgrade_conventions.py +10 -1
- tests/test_sofa_verify.py +10 -10
- tests/test_utils.py +30 -16
- /sofar/sofa_conventions/conventions/{FreeFieldDirectivityTF_1.0.csv → deprecated/FreeFieldDirectivityTF_1.0.csv} +0 -0
- /sofar/sofa_conventions/conventions/{FreeFieldDirectivityTF_1.0.json → deprecated/FreeFieldDirectivityTF_1.0.json} +0 -0
- {sofar-1.0.0.dist-info → sofar-1.1.1.dist-info}/AUTHORS.rst +0 -0
- {sofar-1.0.0.dist-info → sofar-1.1.1.dist-info}/LICENSE +0 -0
- {sofar-1.0.0.dist-info → sofar-1.1.1.dist-info}/top_level.txt +0 -0
sofar/__init__.py
CHANGED
@@ -4,11 +4,11 @@
|
|
4
4
|
|
5
5
|
__author__ = """The pyfar developers"""
|
6
6
|
__email__ = 'info@pyfar.org'
|
7
|
-
__version__ = '1.
|
7
|
+
__version__ = '1.1.1'
|
8
8
|
|
9
9
|
from .sofa import Sofa
|
10
10
|
|
11
|
-
from .io import read_sofa, write_sofa
|
11
|
+
from .io import read_sofa, read_sofa_as_netcdf, write_sofa
|
12
12
|
|
13
13
|
from .utils import (list_conventions,
|
14
14
|
equals,
|
@@ -21,6 +21,7 @@ __all__ = ['Sofa',
|
|
21
21
|
'update_conventions',
|
22
22
|
'list_conventions',
|
23
23
|
'read_sofa',
|
24
|
+
'read_sofa_as_netcdf',
|
24
25
|
'write_sofa',
|
25
26
|
'equals',
|
26
27
|
'version']
|
sofar/io.py
CHANGED
@@ -3,7 +3,8 @@ import os
|
|
3
3
|
import numpy as np
|
4
4
|
from netCDF4 import Dataset, chartostring, stringtochar
|
5
5
|
import warnings
|
6
|
-
import
|
6
|
+
import pathlib
|
7
|
+
from packaging.version import parse
|
7
8
|
import sofar as sf
|
8
9
|
from .utils import _verify_convention_and_version, _atleast_nd
|
9
10
|
|
@@ -18,8 +19,7 @@ def read_sofa(filename, verify=True, verbose=True):
|
|
18
19
|
Parameters
|
19
20
|
----------
|
20
21
|
filename : str
|
21
|
-
The
|
22
|
-
explicitly given.
|
22
|
+
The full path to the sofa data.
|
23
23
|
verify : bool, optional
|
24
24
|
Verify and update the SOFA object by calling :py:func:`~Sofa.verify`.
|
25
25
|
This helps to find potential errors in the default values and is thus
|
@@ -32,7 +32,7 @@ def read_sofa(filename, verify=True, verbose=True):
|
|
32
32
|
Returns
|
33
33
|
-------
|
34
34
|
sofa : Sofa
|
35
|
-
|
35
|
+
Object containing the data from `filename`.
|
36
36
|
|
37
37
|
Notes
|
38
38
|
-----
|
@@ -52,9 +52,63 @@ def read_sofa(filename, verify=True, verbose=True):
|
|
52
52
|
will be a scalar inside SOFA objects after reading from disk.
|
53
53
|
"""
|
54
54
|
|
55
|
+
return _read_netcdf(filename, verify, verbose, mode="sofa")
|
56
|
+
|
57
|
+
|
58
|
+
def read_sofa_as_netcdf(filename):
|
59
|
+
"""
|
60
|
+
Read corrupted SOFA data from disk.
|
61
|
+
|
62
|
+
.. note::
|
63
|
+
`read_sofa_as_netcdf` is intended to read and fix corrupted SOFA data
|
64
|
+
that could not be read by :py:func:`~read_sofa`. The recommend workflow
|
65
|
+
is
|
66
|
+
|
67
|
+
- Try to read the data with `read_sofa` and ``verify=True``
|
68
|
+
- If this fails, try the above with ``verify=False``
|
69
|
+
- If this fails, use `read_sofa_as_netcdf`
|
70
|
+
|
71
|
+
The SOFA object returned by `read_sofa_as_netcdf` may not work
|
72
|
+
correctly before the issues with the data were fixed, i.e., before the
|
73
|
+
data are in agreement with the SOFA standard AES-69.
|
74
|
+
|
75
|
+
Numeric data is returned as floats or numpy float arrays unless they have
|
76
|
+
missing data, in which case they are returned as numpy masked arrays.
|
77
|
+
|
78
|
+
Parameters
|
79
|
+
----------
|
80
|
+
filename : str
|
81
|
+
The full path to the NetCDF data.
|
82
|
+
|
83
|
+
Returns
|
84
|
+
-------
|
85
|
+
sofa : Sofa
|
86
|
+
Object containing the data from `filename`.
|
87
|
+
|
88
|
+
Notes
|
89
|
+
-----
|
90
|
+
|
91
|
+
1. Missing dimensions are appended when writing the SOFA object to disk.
|
92
|
+
E.g., if ``sofa.Data_IR`` is of shape (1, 2) it is written as an array
|
93
|
+
of shape (1, 2, 1) because the SOFA standard AES69-2020 defines it as a
|
94
|
+
three dimensional array with the dimensions (`M: measurements`,
|
95
|
+
`R: receivers`, `N: samples`)
|
96
|
+
2. When reading data from a SOFA file, array data is always returned as
|
97
|
+
numpy arrays and singleton trailing dimensions are discarded (numpy
|
98
|
+
default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
|
99
|
+
after writing and reading to and from disk.
|
100
|
+
3. One dimensional arrays with only one element will be converted to scalar
|
101
|
+
values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
|
102
|
+
(1, ) inside SOFA files (according to the SOFA standard AES69-2020) but
|
103
|
+
will be a scalar inside SOFA objects after reading from disk.
|
104
|
+
"""
|
105
|
+
return _read_netcdf(filename, False, False, mode="netcdf")
|
106
|
+
|
107
|
+
|
108
|
+
def _read_netcdf(filename, verify, verbose, mode):
|
109
|
+
|
55
110
|
# check the filename
|
56
|
-
|
57
|
-
raise ValueError("Filename must end with .sofa")
|
111
|
+
filename = pathlib.Path(filename).with_suffix('.sofa')
|
58
112
|
if not os.path.isfile(filename):
|
59
113
|
raise ValueError(f"{filename} does not exist")
|
60
114
|
|
@@ -68,29 +122,25 @@ def read_sofa(filename, verify=True, verbose=True):
|
|
68
122
|
# open new NETCDF4 file for reading
|
69
123
|
with Dataset(filename, "r", format="NETCDF4") as file:
|
70
124
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
all_attr.append("GLOBAL_SOFAConventionsVersion")
|
125
|
+
if mode == "sofa":
|
126
|
+
# get convention name and version
|
127
|
+
convention = getattr(file, "SOFAConventions")
|
128
|
+
version = getattr(file, "SOFAConventionsVersion")
|
76
129
|
|
77
|
-
|
78
|
-
|
79
|
-
version_in, version_in, convention)
|
130
|
+
# check if convention and version exist
|
131
|
+
_verify_convention_and_version(version, convention)
|
80
132
|
|
81
|
-
|
82
|
-
|
133
|
+
# get SOFA object with default values
|
134
|
+
sofa = sf.Sofa(convention, version=version, verify=verify)
|
135
|
+
else:
|
136
|
+
sofa = sf.Sofa(None)
|
83
137
|
|
84
138
|
# allow writing read only attributes
|
85
|
-
sofa.
|
139
|
+
sofa.protected = False
|
86
140
|
|
87
141
|
# load global attributes
|
88
142
|
for attr in file.ncattrs():
|
89
143
|
|
90
|
-
if attr in ["SOFAConventionsVersion", "SOFAConventions"]:
|
91
|
-
# convention and version were already set above
|
92
|
-
continue
|
93
|
-
|
94
144
|
value = getattr(file, attr)
|
95
145
|
all_attr.append(f"GLOBAL_{attr}")
|
96
146
|
|
@@ -98,7 +148,7 @@ def read_sofa(filename, verify=True, verbose=True):
|
|
98
148
|
sofa._add_custom_api_entry(
|
99
149
|
f"GLOBAL_{attr}", value, None, None, "attribute")
|
100
150
|
custom.append(f"GLOBAL_{attr}")
|
101
|
-
sofa.
|
151
|
+
sofa.protected = False
|
102
152
|
else:
|
103
153
|
setattr(sofa, f"GLOBAL_{attr}", value)
|
104
154
|
|
@@ -117,7 +167,7 @@ def read_sofa(filename, verify=True, verbose=True):
|
|
117
167
|
sofa._add_custom_api_entry(var.replace(".", "_"), value, None,
|
118
168
|
dimensions, dtype)
|
119
169
|
custom.append(var.replace(".", "_"))
|
120
|
-
sofa.
|
170
|
+
sofa.protected = False
|
121
171
|
|
122
172
|
# load variable attributes
|
123
173
|
for attr in [a for a in file[var].ncattrs() if a not in skip]:
|
@@ -130,7 +180,7 @@ def read_sofa(filename, verify=True, verbose=True):
|
|
130
180
|
var.replace(".", "_") + "_" + attr, value, None,
|
131
181
|
None, "attribute")
|
132
182
|
custom.append(var.replace(".", "_") + "_" + attr)
|
133
|
-
sofa.
|
183
|
+
sofa.protected = False
|
134
184
|
else:
|
135
185
|
setattr(sofa, var.replace(".", "_") + "_" + attr, value)
|
136
186
|
|
@@ -142,7 +192,7 @@ def read_sofa(filename, verify=True, verbose=True):
|
|
142
192
|
delattr(sofa, attr)
|
143
193
|
|
144
194
|
# do not allow writing read only attributes any more
|
145
|
-
sofa.
|
195
|
+
sofa.protected = True
|
146
196
|
|
147
197
|
# notice about custom entries
|
148
198
|
if custom and verbose:
|
@@ -206,20 +256,23 @@ def _write_sofa(filename: str, sofa: sf.Sofa, compression=4, verify=True):
|
|
206
256
|
"""
|
207
257
|
|
208
258
|
# check the filename
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
259
|
+
filename = pathlib.Path(filename).with_suffix('.sofa')
|
260
|
+
|
261
|
+
if verify:
|
262
|
+
# check if the latest version is used for writing and warn otherwise
|
263
|
+
# if case required for writing SOFA test data that violates the
|
264
|
+
# conventions
|
265
|
+
if sofa.GLOBAL_SOFAConventions != "invalid-value":
|
266
|
+
latest = sf.Sofa(sofa.GLOBAL_SOFAConventions)
|
267
|
+
latest = latest.GLOBAL_SOFAConventionsVersion
|
268
|
+
current = sofa.GLOBAL_SOFAConventionsVersion
|
269
|
+
|
270
|
+
if parse(current) < parse(latest):
|
271
|
+
warnings.warn((
|
272
|
+
"Writing SOFA object with outdated Convention "
|
273
|
+
f"version {current}. It is recommend to upgrade "
|
274
|
+
" data with Sofa.upgrade_convention() before "
|
275
|
+
"writing to disk if possible."))
|
223
276
|
|
224
277
|
# setting the netCDF compression parameter
|
225
278
|
use_zlib = compression != 0
|
sofar/sofa.py
CHANGED
@@ -89,35 +89,43 @@ class Sofa():
|
|
89
89
|
"""See class docstring"""
|
90
90
|
|
91
91
|
# get convention
|
92
|
-
|
92
|
+
if convention is not None:
|
93
|
+
self._convention = self._load_convention(convention, version)
|
93
94
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
95
|
+
# update read only attributes
|
96
|
+
self._read_only_attr = [
|
97
|
+
key for key in self._convention.keys()
|
98
|
+
if self._read_only(self._convention[key]["flags"])]
|
98
99
|
|
99
|
-
|
100
|
-
|
100
|
+
# add attributes with default values
|
101
|
+
self._convention_to_sofa(mandatory)
|
101
102
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
103
|
+
# add and update the API
|
104
|
+
# (mandatory=False can not be verified because some conventions
|
105
|
+
# have default values that have optional variables as dependencies)
|
106
|
+
if verify and not mandatory:
|
107
|
+
self.verify(mode="read")
|
107
108
|
|
108
|
-
|
109
|
+
self.protected = True
|
110
|
+
else:
|
111
|
+
verify = False
|
112
|
+
self._convention = {}
|
109
113
|
|
110
114
|
def __setattr__(self, name: str, value):
|
111
115
|
# don't allow new attributes to be added outside the class
|
112
|
-
if self.
|
116
|
+
if self.protected and not hasattr(self, name):
|
113
117
|
raise TypeError(f"{name} is an invalid attribute")
|
114
118
|
|
115
119
|
# don't allow setting read only attributes
|
116
|
-
if name in self._read_only_attr and self.
|
117
|
-
raise TypeError(
|
120
|
+
if name in self._read_only_attr and self.protected:
|
121
|
+
raise TypeError((
|
122
|
+
f"{name} is a read only attribute. Iy you know what you are "
|
123
|
+
"doing, you can set Sofa.protected = False to write read "
|
124
|
+
"only data (e.g., to repair corrupted SOFA data)."))
|
118
125
|
|
119
126
|
# convert to numpy array or scalar
|
120
|
-
if not isinstance(value, (str, dict, np.ndarray))
|
127
|
+
if not isinstance(value, (str, dict, np.ndarray)) \
|
128
|
+
and name != "protected":
|
121
129
|
value = np.atleast_2d(value)
|
122
130
|
if value.size == 1:
|
123
131
|
value = value.flatten()[0]
|
@@ -129,7 +137,7 @@ class Sofa():
|
|
129
137
|
if not hasattr(self, name):
|
130
138
|
raise TypeError(f"{name} is not an attribute")
|
131
139
|
# delete anything if not frozen, delete non mandatory
|
132
|
-
if not self.
|
140
|
+
if not self.protected or \
|
133
141
|
not self._mandatory(self._convention[name]["flags"]):
|
134
142
|
super().__delattr__(name)
|
135
143
|
|
@@ -287,9 +295,17 @@ class Sofa():
|
|
287
295
|
attribute will be printed.
|
288
296
|
"""
|
289
297
|
|
298
|
+
# warn for upcoming deprecation
|
299
|
+
warnings.warn((
|
300
|
+
'Sofa.info() will be deprecated in sofar 1.3.0 The conventions are'
|
301
|
+
' now documented at '
|
302
|
+
'https://sofar.readthedocs.io/en/stable/resources/conventions.html'), # noqa
|
303
|
+
UserWarning)
|
304
|
+
|
290
305
|
# update the private attribute `_convention` to make sure the required
|
291
306
|
# meta data is in place
|
292
|
-
self
|
307
|
+
if not hasattr(self, "_convention"):
|
308
|
+
self._reset_convention()
|
293
309
|
|
294
310
|
# list of all attributes
|
295
311
|
keys = [k for k in self.__dict__.keys() if not k.startswith("_")]
|
@@ -461,13 +477,13 @@ class Sofa():
|
|
461
477
|
"""
|
462
478
|
|
463
479
|
# initialize
|
464
|
-
self.
|
480
|
+
self._reset_convention()
|
465
481
|
added = "Added the following missing data with their default values:\n"
|
466
482
|
|
467
483
|
# current data
|
468
484
|
keys = [key for key in self.__dict__.keys() if not key.startswith("_")]
|
469
485
|
|
470
|
-
self.
|
486
|
+
self.protected = False
|
471
487
|
|
472
488
|
# loop data in convention
|
473
489
|
for key in self._convention.keys():
|
@@ -480,7 +496,7 @@ class Sofa():
|
|
480
496
|
added += f"- {key} "
|
481
497
|
added += f"({'mandatory' if is_mandatory else 'optional'})\n"
|
482
498
|
|
483
|
-
self.
|
499
|
+
self.protected = True
|
484
500
|
|
485
501
|
if verbose:
|
486
502
|
if "-" in added:
|
@@ -510,7 +526,7 @@ class Sofa():
|
|
510
526
|
|
511
527
|
dimensions : str
|
512
528
|
The shape of the new entry as a string. See
|
513
|
-
|
529
|
+
:py:func:`~Sofa.list_dimensions`.
|
514
530
|
|
515
531
|
Examples
|
516
532
|
--------
|
@@ -565,9 +581,10 @@ class Sofa():
|
|
565
581
|
"""
|
566
582
|
Delete variable or attribute from SOFA object.
|
567
583
|
|
568
|
-
Note that mandatory data can not be deleted.
|
569
|
-
|
570
|
-
|
584
|
+
Note that mandatory data can not be deleted. Check the
|
585
|
+
`sofar documentation
|
586
|
+
<https://sofar.readthedocs.io/en/stable/resources/conventions.html>`_
|
587
|
+
for a complete list of optional variables and attributes.
|
571
588
|
|
572
589
|
Parameters
|
573
590
|
----------
|
@@ -576,6 +593,26 @@ class Sofa():
|
|
576
593
|
"""
|
577
594
|
delattr(self, name)
|
578
595
|
|
596
|
+
@property
|
597
|
+
def protected(self):
|
598
|
+
"""
|
599
|
+
If Sofa.protected is ``True``, read only data can not be changed. Only
|
600
|
+
change this to ``False`` if you know what you are doing, e.g., if you
|
601
|
+
need to repair corrupted SOFA data.
|
602
|
+
"""
|
603
|
+
return self._protected
|
604
|
+
|
605
|
+
@protected.setter
|
606
|
+
def protected(self, value: bool):
|
607
|
+
"""
|
608
|
+
If Sofa.protected is ``True``, read only data can not be changed. Only
|
609
|
+
change this to ``False`` if you know what you are doing, e.g., if you
|
610
|
+
need to repair corrupted SOFA data.
|
611
|
+
"""
|
612
|
+
if not isinstance(value, bool):
|
613
|
+
raise ValueError("Sofa.protected can only be True or False")
|
614
|
+
self._protected = value
|
615
|
+
|
579
616
|
def _add_entry(self, name, value, dtype, dimensions):
|
580
617
|
"""
|
581
618
|
Add custom data to a SOFA object. See add_variable and add_attribute
|
@@ -633,7 +670,7 @@ class Sofa():
|
|
633
670
|
double, string, or attribute
|
634
671
|
"""
|
635
672
|
# create custom API if it not exists
|
636
|
-
self.
|
673
|
+
self.protected = False
|
637
674
|
|
638
675
|
# lower case letters to indicate custom dimensions
|
639
676
|
if dimensions is not None:
|
@@ -652,11 +689,11 @@ class Sofa():
|
|
652
689
|
"type": dtype,
|
653
690
|
"default": None,
|
654
691
|
"comment": ""}
|
655
|
-
|
692
|
+
self._convention[key] = self._custom[key]
|
656
693
|
|
657
694
|
# add attribute to object
|
658
695
|
setattr(self, key, value)
|
659
|
-
self.
|
696
|
+
self.protected = True
|
660
697
|
|
661
698
|
def upgrade_convention(self, target=None, verify=True):
|
662
699
|
"""
|
@@ -687,7 +724,7 @@ class Sofa():
|
|
687
724
|
"""
|
688
725
|
|
689
726
|
# check input ---------------------------------------------------------
|
690
|
-
self.
|
727
|
+
self._reset_convention()
|
691
728
|
|
692
729
|
# get deprecations and information about Sofa object
|
693
730
|
_, _, deprecations, upgrade = self._verification_rules()
|
@@ -763,7 +800,7 @@ class Sofa():
|
|
763
800
|
"GLOBAL_Version",
|
764
801
|
"GLOBAL_DataType"]
|
765
802
|
|
766
|
-
self.
|
803
|
+
self.protected = False
|
767
804
|
for key in keys:
|
768
805
|
setattr(self, key, self._convention[key]["default"])
|
769
806
|
|
@@ -813,7 +850,7 @@ class Sofa():
|
|
813
850
|
|
814
851
|
# check for missing mandatory data
|
815
852
|
self.add_missing(True, False)
|
816
|
-
self.
|
853
|
+
self.protected = True
|
817
854
|
|
818
855
|
# display general message
|
819
856
|
if upgrade["message"] is not None:
|
@@ -908,7 +945,7 @@ class Sofa():
|
|
908
945
|
|
909
946
|
# ---------------------------------------------------------------------
|
910
947
|
# 0. update the convention
|
911
|
-
self.
|
948
|
+
self._reset_convention()
|
912
949
|
|
913
950
|
# ---------------------------------------------------------------------
|
914
951
|
# 1. check if the mandatory attributes are contained
|
@@ -921,9 +958,9 @@ class Sofa():
|
|
921
958
|
|
922
959
|
if issue_handling != "raise":
|
923
960
|
# add missing data with default value
|
924
|
-
self.
|
961
|
+
self.protected = False
|
925
962
|
setattr(self, key, self._convention[key]["default"])
|
926
|
-
self.
|
963
|
+
self.protected = True
|
927
964
|
|
928
965
|
# prepare to raise warning
|
929
966
|
missing += "- " + key + "\n"
|
@@ -1104,10 +1141,10 @@ class Sofa():
|
|
1104
1141
|
# 4. Get dimensions (E, R, M, N, S, c, I, and custom)
|
1105
1142
|
|
1106
1143
|
# initialize required API fields
|
1107
|
-
self.
|
1144
|
+
self.protected = False
|
1108
1145
|
self._dimensions = {}
|
1109
1146
|
self._api = {}
|
1110
|
-
self.
|
1147
|
+
self.protected = True
|
1111
1148
|
|
1112
1149
|
# get keys for checking the dimensions (all SOFA variables)
|
1113
1150
|
keys = [key for key in self.__dict__.keys()
|
@@ -1548,49 +1585,36 @@ class Sofa():
|
|
1548
1585
|
"""Return a copy of the SOFA object."""
|
1549
1586
|
return deepcopy(self)
|
1550
1587
|
|
1551
|
-
def
|
1588
|
+
def _reset_convention(self):
|
1552
1589
|
"""
|
1553
|
-
Add SOFA convention to SOFA object in private attribute
|
1554
|
-
|
1555
|
-
|
1556
|
-
|
1557
|
-
|
1558
|
-
|
1559
|
-
|
1560
|
-
|
1561
|
-
``'match'``
|
1562
|
-
Match the version of the sofa file.
|
1563
|
-
str
|
1564
|
-
Version string, e.g., ``'1.0'``.
|
1590
|
+
- Add SOFA convention to SOFA object in private attribute
|
1591
|
+
`_convention`. If The object already contains a convention, it will
|
1592
|
+
be overwritten.
|
1593
|
+
- If the SOFA object contains custom entries, check if any of the
|
1594
|
+
custom entries part of the convention. If yes, delete the entry from
|
1595
|
+
self._custom
|
1596
|
+
- If the SOFA objects contains custom entries, add entries from
|
1597
|
+
self._custom to self._convention
|
1565
1598
|
"""
|
1566
1599
|
|
1567
1600
|
# verify convention and version
|
1568
1601
|
c_current = self.GLOBAL_SOFAConventions
|
1569
1602
|
v_current = str(self.GLOBAL_SOFAConventionsVersion)
|
1570
1603
|
|
1571
|
-
|
1572
|
-
version, v_current, c_current)
|
1604
|
+
_verify_convention_and_version(v_current, c_current)
|
1573
1605
|
|
1574
1606
|
# load and add convention and version
|
1575
1607
|
convention = self._load_convention(
|
1576
|
-
c_current,
|
1608
|
+
c_current, v_current)
|
1577
1609
|
self._convention = convention
|
1578
1610
|
|
1579
|
-
if v_current != v_new:
|
1580
|
-
self._protected = False
|
1581
|
-
self.GLOBAL_SOFAConventionsVersion = v_new
|
1582
|
-
self._protected = True
|
1583
|
-
|
1584
|
-
# feedback in case of up/downgrade
|
1585
|
-
if float(v_current) < float(v_new):
|
1586
|
-
warnings.warn(("Upgraded SOFA object from "
|
1587
|
-
f"version {v_current} to {v_new}"))
|
1588
|
-
elif float(v_current) > float(v_new):
|
1589
|
-
warnings.warn(("Downgraded SOFA object from "
|
1590
|
-
f"version {v_current} to {v_new}"))
|
1591
|
-
|
1592
|
-
# check if custom fields can be added
|
1593
1611
|
if hasattr(self, "_custom"):
|
1612
|
+
# check of custom fields can be removed
|
1613
|
+
for key in self._convention:
|
1614
|
+
if key in self._custom:
|
1615
|
+
del self._custom[key]
|
1616
|
+
|
1617
|
+
# check if custom fields can be added
|
1594
1618
|
for key in self._custom:
|
1595
1619
|
self._convention[key] = self._custom[key]
|
1596
1620
|
|
@@ -1684,7 +1708,7 @@ class Sofa():
|
|
1684
1708
|
|
1685
1709
|
# write API and date specific fields (some read only)
|
1686
1710
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
1687
|
-
self.
|
1711
|
+
self.protected = False
|
1688
1712
|
self.GLOBAL_DateCreated = now
|
1689
1713
|
self.GLOBAL_DateModified = now
|
1690
1714
|
self.GLOBAL_APIName = "sofar SOFA API for Python (pyfar.org)"
|
@@ -1694,7 +1718,7 @@ class Sofa():
|
|
1694
1718
|
f"{platform.python_version()} "
|
1695
1719
|
f"[{platform.python_implementation()} - "
|
1696
1720
|
f"{platform.python_compiler()}]")
|
1697
|
-
self.
|
1721
|
+
self.protected = True
|
1698
1722
|
|
1699
1723
|
@staticmethod
|
1700
1724
|
def _get_size_and_shape_of_string_var(value, key):
|
@@ -1,4 +1,38 @@
|
|
1
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
|
+
},
|
2
36
|
"SimpleFreeFieldHRIR": {
|
3
37
|
"from_to": [
|
4
38
|
[
|
sofar/update_conventions.py
CHANGED
@@ -333,7 +333,7 @@ def _convention_csv2dict(file: str):
|
|
333
333
|
return convention
|
334
334
|
|
335
335
|
|
336
|
-
def _check_congruency(save_dir=None):
|
336
|
+
def _check_congruency(save_dir=None, branch="master"):
|
337
337
|
"""
|
338
338
|
SOFA conventions are stored in two different places - is this a good idea?
|
339
339
|
They should be identical, but let's find out.
|
@@ -346,9 +346,14 @@ def _check_congruency(save_dir=None):
|
|
346
346
|
directory to save diverging conventions for further inspections
|
347
347
|
"""
|
348
348
|
|
349
|
-
urls
|
350
|
-
|
351
|
-
|
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/")]
|
352
357
|
subdirs = ["sofaconventions", "sofatoolbox"]
|
353
358
|
|
354
359
|
# check save_dir
|
@@ -359,21 +364,29 @@ def _check_congruency(save_dir=None):
|
|
359
364
|
if not os.path.isdir(os.path.join(save_dir, subdir)):
|
360
365
|
os.makedirs(os.path.join(save_dir, subdir))
|
361
366
|
|
362
|
-
# get file names of conventions from sofaconventions.org
|
363
|
-
url =
|
367
|
+
# get file names of conventions from sofaconventions.org
|
368
|
+
url = urls_check[0]
|
364
369
|
page = requests.get(url).text
|
365
370
|
soup = BeautifulSoup(page, 'html.parser')
|
366
371
|
sofaconventions = [os.path.split(node.get('href'))[1]
|
367
372
|
for node in soup.find_all('a')
|
368
373
|
if node.get('href').endswith(".csv")]
|
369
374
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
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}")
|
377
390
|
|
378
391
|
# check if lists are identical. Remove items not contained in both lists
|
379
392
|
report = ""
|
@@ -394,7 +407,7 @@ def _check_congruency(save_dir=None):
|
|
394
407
|
for convention in sofaconventions:
|
395
408
|
|
396
409
|
# download SOFA convention definitions to package directory
|
397
|
-
data = [requests.get(url + convention) for url in
|
410
|
+
data = [requests.get(url + convention) for url in urls_load]
|
398
411
|
# remove trailing tabs and windows style line breaks
|
399
412
|
data = [d.content.replace(b"\r\n", b"\n").replace(b"\t\n", b"\n")
|
400
413
|
for d in data]
|
sofar/utils.py
CHANGED
@@ -2,7 +2,6 @@ import os
|
|
2
2
|
import glob
|
3
3
|
import numpy as np
|
4
4
|
import numpy.testing as npt
|
5
|
-
from packaging.version import parse as version_parse
|
6
5
|
import warnings
|
7
6
|
import sofar as sf
|
8
7
|
|
@@ -19,58 +18,39 @@ def version():
|
|
19
18
|
f"SOFA standard {sofa_conventions}")
|
20
19
|
|
21
20
|
|
22
|
-
def _verify_convention_and_version(version,
|
21
|
+
def _verify_convention_and_version(version, convention):
|
23
22
|
"""
|
24
|
-
Verify if convention and version exist
|
23
|
+
Verify if convention and version exist. Raise a Value error if it does not.
|
25
24
|
|
26
25
|
Parameters
|
27
26
|
----------
|
28
27
|
version : str
|
29
|
-
|
30
|
-
version_in : str
|
31
|
-
The version to be checked against
|
28
|
+
The version to be checked
|
32
29
|
convention : str
|
33
30
|
The name of the convention to be checked
|
34
|
-
|
35
|
-
Returns
|
36
|
-
-------
|
37
|
-
version_out : str
|
38
|
-
The version to be used depending on `version`, and `version_in`
|
39
31
|
"""
|
40
32
|
|
41
|
-
# check if the convention exists
|
33
|
+
# check if the convention exists
|
42
34
|
if convention not in _get_conventions("name"):
|
43
35
|
raise ValueError(
|
44
36
|
f"Convention '{convention}' does not exist")
|
45
37
|
|
46
38
|
name_version = _get_conventions("name_version")
|
47
39
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
for versions in name_version:
|
63
|
-
# check if convention and version match
|
64
|
-
if versions[0] == convention \
|
65
|
-
and str(float(versions[1])) == match:
|
66
|
-
version_out = str(float(versions[1]))
|
67
|
-
|
68
|
-
if version_out is None:
|
69
|
-
raise ValueError((
|
70
|
-
f"Version {match} does not exist for convention {convention}. "
|
71
|
-
"Try to access the data with version='latest'"))
|
72
|
-
|
73
|
-
return version_out
|
40
|
+
# check which version is wanted
|
41
|
+
version_exists = False
|
42
|
+
for versions in name_version:
|
43
|
+
# check if convention and version match
|
44
|
+
if versions[0] == convention \
|
45
|
+
and str(float(versions[1])) == version:
|
46
|
+
version_exists = True
|
47
|
+
|
48
|
+
if not version_exists:
|
49
|
+
raise ValueError((
|
50
|
+
f"{convention} v{version} is not a valid SOFA Convention."
|
51
|
+
"If you are trying to read the data use "
|
52
|
+
"sofar.read_sofa_as_netcdf(). Call sofar.list_conventions() for a "
|
53
|
+
"list of valid Conventions"))
|
74
54
|
|
75
55
|
|
76
56
|
def list_conventions():
|
@@ -1,17 +1,16 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: sofar
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.1.1
|
4
4
|
Summary: Maybe the most complete python package for SOFA files so far
|
5
5
|
Home-page: https://pyfar.org/
|
6
|
+
Download-URL: https://pypi.org/project/sofar/
|
6
7
|
Author: The pyfar developers
|
7
8
|
Author-email: info@pyfar.org
|
8
9
|
License: MIT license
|
9
|
-
Download-URL: https://pypi.org/project/sofar/
|
10
10
|
Project-URL: Bug Tracker, https://github.com/pyfar/sofar/issues
|
11
11
|
Project-URL: Documentation, https://sofar.readthedocs.io/
|
12
12
|
Project-URL: Source Code, https://github.com/pyfar/sofar
|
13
13
|
Keywords: sofar
|
14
|
-
Platform: UNKNOWN
|
15
14
|
Classifier: Development Status :: 4 - Beta
|
16
15
|
Classifier: Intended Audience :: Science/Research
|
17
16
|
Classifier: License :: OSI Approved :: MIT License
|
@@ -20,6 +19,7 @@ Classifier: Programming Language :: Python :: 3
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.8
|
21
20
|
Classifier: Programming Language :: Python :: 3.9
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
23
23
|
Requires-Python: >=3.8
|
24
24
|
License-File: LICENSE
|
25
25
|
License-File: AUTHORS.rst
|
@@ -85,7 +85,5 @@ format*, Audio Engineering Society, Inc., New York, NY, USA.
|
|
85
85
|
|
86
86
|
P. Majdak, F. Zotter, F. Brinkmann, J. De Muynke, M. Mihocic, and M.
|
87
87
|
Noisternig, "Spatially Oriented Format for Acoustics 2.1: Introduction and
|
88
|
-
Recent Advances
|
88
|
+
Recent Advances", *J. Audio Eng. Soc.*, vol. 70, no. 7/8, pp. 565-584,
|
89
89
|
Jul. 2022. DOI: https://doi.org/10.17743/jaes.2022.0026
|
90
|
-
|
91
|
-
|
@@ -1,11 +1,9 @@
|
|
1
|
-
sofar/__init__.py,sha256=
|
2
|
-
sofar/io.py,sha256=
|
3
|
-
sofar/sofa.py,sha256=
|
4
|
-
sofar/update_conventions.py,sha256=
|
5
|
-
sofar/utils.py,sha256=
|
1
|
+
sofar/__init__.py,sha256=Jcf2PHRtcQo3gOPJwXrjpYoB613twZJ2faZREU8dosQ,595
|
2
|
+
sofar/io.py,sha256=XdkfG6jo4RI8JqUgXEfx2ka06_CfffPJXs0HqeMSri0,15761
|
3
|
+
sofar/sofa.py,sha256=QVr0UKr-aqo2K7_PEHtXk9VuvrjE36QgcYIqFJ6KBrc,68255
|
4
|
+
sofar/update_conventions.py,sha256=36ba0cAVtKppJCrsEhP2aiv1DGEtxNx06hsUAERDlwM,16753
|
5
|
+
sofar/utils.py,sha256=0JwYLLyVu1ZcAfUQKSucywcQPlHB2Ojez7fxIXhuaFw,10754
|
6
6
|
sofar/sofa_conventions/VERSION,sha256=IuQRql5P3qfPUQuJwuh-RX5TvI_Wd4bDdSm8VLNQqds,34
|
7
|
-
sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.0.csv,sha256=Ha6178_LxRod-L2UcI7SX3o6Cf1YRFs1zgIpn0kd0rA,5125
|
8
|
-
sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.0.json,sha256=GcOb3cbv63C7S4cJ7IVvU7NAAd1h8mA7RHaAFQ_jqz8,12627
|
9
7
|
sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.csv,sha256=Wait37FTh6ROKgpX7-JsVM-ohtKfsdHjxVL5vN7uTTs,5510
|
10
8
|
sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.json,sha256=sm8Pj_Z1n1tW2jYlu00eikR3yCcANEm_Kw5Eq9jrbw4,13138
|
11
9
|
sofar/sofa_conventions/conventions/FreeFieldHRIR_1.0.csv,sha256=C_fc7wHjBLc2krPja33sQaDSZDvlr80GSsgOj904GBY,2479
|
@@ -38,6 +36,8 @@ sofar/sofa_conventions/conventions/SingleRoomMIMOSRIR_1.0.csv,sha256=2OzG5t9rYzD
|
|
38
36
|
sofar/sofa_conventions/conventions/SingleRoomMIMOSRIR_1.0.json,sha256=5tjPriXZk8WEIr15i41L7hQRwAxzw_gA0nxWkZfe_7g,14346
|
39
37
|
sofar/sofa_conventions/conventions/SingleRoomSRIR_1.0.csv,sha256=l7aeLNxbOyXgfQIjCo3QzpRpBonMp9ssfhps4fejHxc,4199
|
40
38
|
sofar/sofa_conventions/conventions/SingleRoomSRIR_1.0.json,sha256=gVwyzGni9e3w5wehnxe6Oe5knO2Np5doHYLzRsfrlfc,14491
|
39
|
+
sofar/sofa_conventions/conventions/deprecated/FreeFieldDirectivityTF_1.0.csv,sha256=Ha6178_LxRod-L2UcI7SX3o6Cf1YRFs1zgIpn0kd0rA,5125
|
40
|
+
sofar/sofa_conventions/conventions/deprecated/FreeFieldDirectivityTF_1.0.json,sha256=GcOb3cbv63C7S4cJ7IVvU7NAAd1h8mA7RHaAFQ_jqz8,12627
|
41
41
|
sofar/sofa_conventions/conventions/deprecated/GeneralFIRE_1.0.csv,sha256=2_ed7Y0CGJmCx9V3X7L4gipJ94nLzH7Jl9fXXtI7rLQ,1988
|
42
42
|
sofar/sofa_conventions/conventions/deprecated/GeneralFIRE_1.0.json,sha256=y3A1fhyS4XUAIbEGv92pwrYp01HutgIKajI4WmJYYKQ,6603
|
43
43
|
sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.csv,sha256=E_jW15CNWUsp5W9Uw2EbNNyvDxDMFvs-CAELDmYtp0I,2560
|
@@ -59,16 +59,17 @@ sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.3.json,sha256=ccC
|
|
59
59
|
sofar/sofa_conventions/rules/deprecations.json,sha256=hyq112XNfojROZsAEbcV7EOLin-xaTiNi4o4WdplS74,453
|
60
60
|
sofar/sofa_conventions/rules/rules.json,sha256=iH8e5UUVRQcGZ_AoULk4A2IiAT1yK7R2CvRQyCMYhYA,22501
|
61
61
|
sofar/sofa_conventions/rules/unit_aliases.json,sha256=QXgOfpe8bnmUybVhSSj3nIUYw50CAVehh7sdFNPFl0k,212
|
62
|
-
sofar/sofa_conventions/rules/upgrade.json,sha256=
|
62
|
+
sofar/sofa_conventions/rules/upgrade.json,sha256=viHPPqBaPblE-fY8BOuVUraQYrBhckrS3w5XKqHCTIE,5167
|
63
63
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
64
|
-
tests/
|
65
|
-
tests/
|
66
|
-
tests/
|
67
|
-
tests/
|
68
|
-
tests/
|
69
|
-
|
70
|
-
sofar-1.
|
71
|
-
sofar-1.
|
72
|
-
sofar-1.
|
73
|
-
sofar-1.
|
74
|
-
sofar-1.
|
64
|
+
tests/test_deprecations.py,sha256=coHOEq5XUquYxRge8XKeg005M84wcgSHTEE--CoEar4,584
|
65
|
+
tests/test_io.py,sha256=xMBdWP3vdnk_6aYAhg30nEgGvPfYm30a1qouGHIwRNo,11808
|
66
|
+
tests/test_sofa.py,sha256=DpiI517T-gLoajy9nQCHT7ouxM40JwirsqkVMFT5cWo,11314
|
67
|
+
tests/test_sofa_upgrade_conventions.py,sha256=HqIuvFFub1_HCH6qHTLPcPyBZ2ayDKzmZR7RwnTxo_o,3377
|
68
|
+
tests/test_sofa_verify.py,sha256=cleTkFJurJfDEZrnSTfBYe2VqsCZ1VxWr9efNGjWr88,15631
|
69
|
+
tests/test_utils.py,sha256=yT290xCaeDW4l3DHUeFGiVmLe9xMguETN1NAGRTJ-X4,8307
|
70
|
+
sofar-1.1.1.dist-info/AUTHORS.rst,sha256=JGcf9PJwl0w14PgTRjI3HXpcwDbkGbUN4mkYoZ8smL8,164
|
71
|
+
sofar-1.1.1.dist-info/LICENSE,sha256=qdGH_RUPveBBfGYShm6OIBd6bRJdwp3fDCpjMsowmqA,1068
|
72
|
+
sofar-1.1.1.dist-info/METADATA,sha256=dzyA462zt5gq3YjCarEozElPtpd-sTUpaE0D1xxb0L8,3168
|
73
|
+
sofar-1.1.1.dist-info/WHEEL,sha256=a-zpFRIJzOq5QfuhBzbhiA1eHTzNCJn8OdRvhdNX0Rk,110
|
74
|
+
sofar-1.1.1.dist-info/top_level.txt,sha256=2JSMmeQ5zeMumFynTJU1oODh09wxSiN3gFpsY-bNsSo,12
|
75
|
+
sofar-1.1.1.dist-info/RECORD,,
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import pytest
|
2
|
+
from packaging import version
|
3
|
+
import re
|
4
|
+
import sofar as sf
|
5
|
+
|
6
|
+
|
7
|
+
# deprecate in 1.3.0 ----------------------------------------------------------
|
8
|
+
def test_pad_zero_modi():
|
9
|
+
with pytest.warns(
|
10
|
+
UserWarning,
|
11
|
+
match=re.escape('Sofa.info() will be deprecated in sofar 1.3.0')):
|
12
|
+
sofa = sf.Sofa('GeneralTF')
|
13
|
+
sofa.info()
|
14
|
+
|
15
|
+
if version.parse(sf.__version__) >= version.parse('1.3.0'):
|
16
|
+
with pytest.raises(ValueError):
|
17
|
+
# remove Sofa.info() from pyfar 1.3.0!
|
18
|
+
sofa = sf.Sofa('GeneralTF')
|
19
|
+
sofa.info()
|
tests/test_io.py
CHANGED
@@ -6,6 +6,7 @@ from sofar.utils import (_get_conventions,
|
|
6
6
|
from sofar.io import (_format_value_for_netcdf,
|
7
7
|
_format_value_from_netcdf)
|
8
8
|
import os
|
9
|
+
import pathlib
|
9
10
|
from tempfile import TemporaryDirectory
|
10
11
|
import pytest
|
11
12
|
from pytest import raises
|
@@ -14,7 +15,7 @@ import numpy.testing as npt
|
|
14
15
|
from netCDF4 import Dataset
|
15
16
|
|
16
17
|
|
17
|
-
def
|
18
|
+
def test_read_write_sofa(capfd):
|
18
19
|
|
19
20
|
temp_dir = TemporaryDirectory()
|
20
21
|
filename = os.path.join(temp_dir.name, "test.sofa")
|
@@ -25,6 +26,11 @@ def test_read_sofa(capfd):
|
|
25
26
|
sofa = sf.read_sofa(filename)
|
26
27
|
assert hasattr(sofa, "_api")
|
27
28
|
|
29
|
+
# test with path object
|
30
|
+
sf.write_sofa(pathlib.Path(filename), sofa)
|
31
|
+
sofa = sf.read_sofa(pathlib.Path(filename))
|
32
|
+
assert hasattr(sofa, "_api")
|
33
|
+
|
28
34
|
# reading without updating API
|
29
35
|
sofa = sf.read_sofa(filename, verify=False)
|
30
36
|
assert not hasattr(sofa, "_api")
|
@@ -47,7 +53,7 @@ def test_read_sofa(capfd):
|
|
47
53
|
with Dataset(filename, "r+", format="NETCDF4") as file:
|
48
54
|
setattr(file, "SOFAConventionsVersion", "0.1")
|
49
55
|
# ValueError when version should be matched
|
50
|
-
with raises(ValueError, match="
|
56
|
+
with raises(ValueError, match="v0.1 is not a valid SOFA Convention"):
|
51
57
|
sf.read_sofa(filename)
|
52
58
|
|
53
59
|
# read file containing a variable with wrong shape
|
@@ -64,10 +70,6 @@ def test_read_sofa(capfd):
|
|
64
70
|
# data can be read without updating API
|
65
71
|
sf.read_sofa(filename, verify=False)
|
66
72
|
|
67
|
-
# test assertion for wrong filename
|
68
|
-
with raises(ValueError, match="Filename must end with .sofa"):
|
69
|
-
sf.read_sofa('sofa.exe')
|
70
|
-
|
71
73
|
|
72
74
|
def test_read_sofa_custom_data():
|
73
75
|
"""Test if sofa files with custom data are loaded correctly"""
|
@@ -83,12 +85,26 @@ def test_read_sofa_custom_data():
|
|
83
85
|
assert sofa.GLOBAL_Warming == 'critical'
|
84
86
|
|
85
87
|
|
86
|
-
def
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
88
|
+
def test_read_netcdf():
|
89
|
+
tmp = TemporaryDirectory()
|
90
|
+
files = [os.path.join(tmp.name, "invalid.sofa"),
|
91
|
+
os.path.join(tmp.name, "invalid.netcdf")]
|
92
|
+
|
93
|
+
# create data with invalid SOFA convention and version
|
94
|
+
sofa = sf.Sofa("GeneralTF")
|
95
|
+
sofa.protected = False
|
96
|
+
sofa.GLOBAL_SOFAConventions = "MadeUp"
|
97
|
+
sofa.protected = True
|
98
|
+
|
99
|
+
# test reading
|
100
|
+
for file in files:
|
101
|
+
# write data
|
102
|
+
sf.io._write_sofa(file, sofa, verify=False)
|
103
|
+
# can not be read with read_sofa
|
104
|
+
with raises(ValueError):
|
105
|
+
sf.read_sofa(file)
|
106
|
+
sofa_read = sf.read_sofa_as_netcdf(file)
|
107
|
+
sf.equals(sofa, sofa_read)
|
92
108
|
|
93
109
|
|
94
110
|
def test_write_sofa_outdated_version():
|
@@ -299,23 +315,15 @@ def test_format_value_from_netcdf():
|
|
299
315
|
|
300
316
|
def test_verify_convention_and_version():
|
301
317
|
|
302
|
-
# test
|
303
|
-
|
304
|
-
assert
|
305
|
-
|
306
|
-
version = _verify_convention_and_version("2.0", "1.0", "GeneralTF")
|
307
|
-
assert version == "2.0"
|
308
|
-
|
309
|
-
version = _verify_convention_and_version("match", "1.0", "GeneralTF")
|
310
|
-
assert version == "1.0"
|
318
|
+
# test with existing convention and version (no error returns None)
|
319
|
+
out = _verify_convention_and_version("1.0", "GeneralTF")
|
320
|
+
assert out is None
|
311
321
|
|
312
322
|
# test assertions
|
313
323
|
with raises(ValueError, match="Convention 'Funky' does not exist"):
|
314
|
-
_verify_convention_and_version("
|
315
|
-
with raises(ValueError, match="
|
316
|
-
_verify_convention_and_version("
|
317
|
-
with raises(ValueError, match="Version 1.2 does not exist"):
|
318
|
-
_verify_convention_and_version("1.2", "1.0", "GeneralTF")
|
324
|
+
_verify_convention_and_version("1.0", "Funky")
|
325
|
+
with raises(ValueError, match="v1.1 is not a valid SOFA Convention"):
|
326
|
+
_verify_convention_and_version("1.1", "GeneralTF")
|
319
327
|
|
320
328
|
|
321
329
|
def test_atleast_nd():
|
tests/test_sofa.py
CHANGED
@@ -231,9 +231,9 @@ def test_add_entry():
|
|
231
231
|
assert "Temperature_Units" not in sofa._custom
|
232
232
|
|
233
233
|
# test adding missing entry defined in convention
|
234
|
-
sofa.
|
234
|
+
sofa.protected = False
|
235
235
|
delattr(sofa, "ListenerPosition")
|
236
|
-
sofa.
|
236
|
+
sofa.protected = True
|
237
237
|
sofa.add_variable("ListenerPosition", [0, 0, 0], "double", "IC")
|
238
238
|
assert "ListenerPosition" not in sofa._custom
|
239
239
|
|
@@ -273,10 +273,10 @@ def test_add_missing(
|
|
273
273
|
opt = "GLOBAL_History"
|
274
274
|
|
275
275
|
# remove data before adding it again
|
276
|
-
sofa.
|
276
|
+
sofa.protected = False
|
277
277
|
sofa.delete(man)
|
278
278
|
sofa.delete(opt)
|
279
|
-
sofa.
|
279
|
+
sofa.protected = False
|
280
280
|
|
281
281
|
# add missing data
|
282
282
|
sofa.add_missing(mandatory, optional, verbose)
|
@@ -313,11 +313,6 @@ def test_delete_entry():
|
|
313
313
|
assert not hasattr(sofa, "SourceManufacturer")
|
314
314
|
|
315
315
|
|
316
|
-
def test__update_conventions():
|
317
|
-
"""Tested in test_sofa_verify.pi::test_version"""
|
318
|
-
pass
|
319
|
-
|
320
|
-
|
321
316
|
def test__get_size_and_shape_of_string_var():
|
322
317
|
|
323
318
|
# test with string
|
@@ -84,9 +84,18 @@ def test_upgrade_conventions(path, capfd):
|
|
84
84
|
targets = sofa.upgrade_convention()
|
85
85
|
out, _ = capfd.readouterr()
|
86
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
|
+
|
87
96
|
if targets:
|
88
97
|
for target in targets:
|
89
|
-
sofa.upgrade_convention(target)
|
98
|
+
sofa.upgrade_convention(target, verify=verify)
|
90
99
|
out, _ = capfd.readouterr()
|
91
100
|
assert "Upgrading" in out
|
92
101
|
else:
|
tests/test_sofa_verify.py
CHANGED
@@ -115,9 +115,9 @@ def test_case_insensitivity():
|
|
115
115
|
|
116
116
|
# data type (must be case sensitive) --------------------------------------
|
117
117
|
sofa = sf.Sofa("SimpleFreeFieldHRIR")
|
118
|
-
sofa.
|
118
|
+
sofa.protected = False
|
119
119
|
sofa.GLOBAL_DataType = "fir"
|
120
|
-
sofa.
|
120
|
+
sofa.protected = True
|
121
121
|
with raises(ValueError, match="GLOBAL_DataType is fir"):
|
122
122
|
sofa.verify()
|
123
123
|
|
@@ -153,9 +153,9 @@ def test_missing_default_attributes(capfd):
|
|
153
153
|
|
154
154
|
# test missing default attribute
|
155
155
|
sofa = sf.Sofa("GeneralTF")
|
156
|
-
sofa.
|
156
|
+
sofa.protected = False
|
157
157
|
delattr(sofa, "GLOBAL_Conventions")
|
158
|
-
sofa.
|
158
|
+
sofa.protected = True
|
159
159
|
|
160
160
|
# raises error
|
161
161
|
with raises(ValueError, match="Detected missing mandatory data"):
|
@@ -242,42 +242,42 @@ def test_wrong_name():
|
|
242
242
|
|
243
243
|
# attribute with missing variable
|
244
244
|
sofa = sf.Sofa("GeneralTF")
|
245
|
-
sofa.
|
245
|
+
sofa.protected = False
|
246
246
|
sofa.IR_Type = "pressure"
|
247
247
|
sofa._custom = {"IR_Type": {"default": None,
|
248
248
|
"flags": None,
|
249
249
|
"dimensions": None,
|
250
250
|
"type": "attribute",
|
251
251
|
"comment": ""}}
|
252
|
-
sofa.
|
252
|
+
sofa.protected = True
|
253
253
|
|
254
254
|
with raises(ValueError, match="Detected attributes with missing"):
|
255
255
|
sofa.verify()
|
256
256
|
|
257
257
|
# attribute with no underscore
|
258
258
|
sofa = sf.Sofa("GeneralTF")
|
259
|
-
sofa.
|
259
|
+
sofa.protected = False
|
260
260
|
sofa.IRType = "pressure"
|
261
261
|
sofa._custom = {"IRType": {"default": None,
|
262
262
|
"flags": None,
|
263
263
|
"dimensions": None,
|
264
264
|
"type": "attribute",
|
265
265
|
"comment": ""}}
|
266
|
-
sofa.
|
266
|
+
sofa.protected = True
|
267
267
|
|
268
268
|
with raises(ValueError, match="Detected attribute names with too many"):
|
269
269
|
sofa.verify()
|
270
270
|
|
271
271
|
# variable with underscore
|
272
272
|
sofa = sf.Sofa("GeneralTF")
|
273
|
-
sofa.
|
273
|
+
sofa.protected = False
|
274
274
|
sofa.IR_Data = 1
|
275
275
|
sofa._custom = {"IR_Data": {"default": None,
|
276
276
|
"flags": None,
|
277
277
|
"dimensions": "IM",
|
278
278
|
"type": "double",
|
279
279
|
"comment": ""}}
|
280
|
-
sofa.
|
280
|
+
sofa.protected = True
|
281
281
|
|
282
282
|
with raises(ValueError, match="Detected variable names with too many"):
|
283
283
|
sofa.verify()
|
tests/test_utils.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import shutil
|
2
2
|
import sofar as sf
|
3
3
|
from sofar.utils import _get_conventions
|
4
|
-
from sofar.update_conventions import _compile_conventions
|
4
|
+
from sofar.update_conventions import _compile_conventions, _check_congruency
|
5
5
|
import os
|
6
6
|
import json
|
7
7
|
from tempfile import TemporaryDirectory
|
@@ -9,6 +9,7 @@ import pytest
|
|
9
9
|
from pytest import raises
|
10
10
|
import numpy as np
|
11
11
|
from copy import deepcopy
|
12
|
+
import warnings
|
12
13
|
|
13
14
|
|
14
15
|
def test_list_conventions(capfd):
|
@@ -48,6 +49,19 @@ def test__get_conventions(capfd):
|
|
48
49
|
_get_conventions(return_type="None")
|
49
50
|
|
50
51
|
|
52
|
+
@pytest.mark.parametrize('branch', ['master', 'development'])
|
53
|
+
def test__congruency(capfd, branch):
|
54
|
+
"""
|
55
|
+
Check if conventions from SOFAToolbox and sofaconventions.org are
|
56
|
+
identical.
|
57
|
+
"""
|
58
|
+
out, _ = capfd.readouterr()
|
59
|
+
_check_congruency(branch=branch)
|
60
|
+
out, _ = capfd.readouterr()
|
61
|
+
if out != "":
|
62
|
+
warnings.warn(out, Warning)
|
63
|
+
|
64
|
+
|
51
65
|
def test_update_conventions(capfd):
|
52
66
|
|
53
67
|
# create temporary directory and copy existing conventions
|
@@ -146,53 +160,53 @@ def test_equals_global_parameters():
|
|
146
160
|
|
147
161
|
# check different number of keys
|
148
162
|
sofa_b = deepcopy(sofa_a)
|
149
|
-
sofa_b.
|
163
|
+
sofa_b.protected = False
|
150
164
|
delattr(sofa_b, "ReceiverPosition")
|
151
|
-
sofa_b.
|
165
|
+
sofa_b.protected = True
|
152
166
|
with pytest.warns(UserWarning, match="not identical: sofa_a has"):
|
153
167
|
is_identical = sf.equals(sofa_a, sofa_b)
|
154
168
|
assert not is_identical
|
155
169
|
|
156
170
|
# check different keys
|
157
171
|
sofa_b = deepcopy(sofa_a)
|
158
|
-
sofa_b.
|
172
|
+
sofa_b.protected = False
|
159
173
|
sofa_b.PositionReceiver = sofa_b.ReceiverPosition
|
160
174
|
delattr(sofa_b, "ReceiverPosition")
|
161
|
-
sofa_b.
|
175
|
+
sofa_b.protected = True
|
162
176
|
with pytest.warns(UserWarning, match="not identical: sofa_a and sofa_b"):
|
163
177
|
is_identical = sf.equals(sofa_a, sofa_b)
|
164
178
|
assert not is_identical
|
165
179
|
|
166
180
|
# check mismatching data types
|
167
181
|
sofa_b = deepcopy(sofa_a)
|
168
|
-
sofa_b.
|
182
|
+
sofa_b.protected = False
|
169
183
|
sofa_b._convention["ReceiverPosition"]["type"] = "int"
|
170
|
-
sofa_b.
|
184
|
+
sofa_b.protected = True
|
171
185
|
with pytest.warns(UserWarning, match="not identical: ReceiverPosition"):
|
172
186
|
is_identical = sf.equals(sofa_a, sofa_b)
|
173
187
|
assert not is_identical
|
174
188
|
|
175
189
|
# check exclude GLOBAL attributes
|
176
190
|
sofa_b = deepcopy(sofa_a)
|
177
|
-
sofa_b.
|
191
|
+
sofa_b.protected = False
|
178
192
|
delattr(sofa_b, "GLOBAL_Version")
|
179
|
-
sofa_b.
|
193
|
+
sofa_b.protected = True
|
180
194
|
is_identical = sf.equals(sofa_a, sofa_b, exclude="GLOBAL")
|
181
195
|
assert is_identical
|
182
196
|
|
183
197
|
# check exclude Date attributes
|
184
198
|
sofa_b = deepcopy(sofa_a)
|
185
|
-
sofa_b.
|
199
|
+
sofa_b.protected = False
|
186
200
|
delattr(sofa_b, "GLOBAL_DateModified")
|
187
|
-
sofa_b.
|
201
|
+
sofa_b.protected = True
|
188
202
|
is_identical = sf.equals(sofa_a, sofa_b, exclude="DATE")
|
189
203
|
assert is_identical
|
190
204
|
|
191
205
|
# check exclude Date attributes
|
192
206
|
sofa_b = deepcopy(sofa_a)
|
193
|
-
sofa_b.
|
207
|
+
sofa_b.protected = False
|
194
208
|
delattr(sofa_b, "GLOBAL_DateModified")
|
195
|
-
sofa_b.
|
209
|
+
sofa_b.protected = True
|
196
210
|
is_identical = sf.equals(sofa_a, sofa_b, exclude="ATTR")
|
197
211
|
assert is_identical
|
198
212
|
|
@@ -210,14 +224,14 @@ def test_equals_attribute_values(value_a, value_b, attribute, fails):
|
|
210
224
|
|
211
225
|
# generate SOFA objects (SimpleHeadphoneIR has string variables)
|
212
226
|
sofa_a = sf.Sofa("SimpleHeadphoneIR")
|
213
|
-
sofa_a.
|
227
|
+
sofa_a.protected = False
|
214
228
|
sofa_b = deepcopy(sofa_a)
|
215
229
|
|
216
230
|
# set parameters
|
217
231
|
setattr(sofa_a, attribute, value_a)
|
218
|
-
sofa_a.
|
232
|
+
sofa_a.protected = True
|
219
233
|
setattr(sofa_b, attribute, value_b)
|
220
|
-
sofa_b.
|
234
|
+
sofa_b.protected = True
|
221
235
|
|
222
236
|
# compare
|
223
237
|
if fails:
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|