sofar 1.0.0__tar.gz → 1.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {sofar-1.0.0 → sofar-1.1.1}/CONTRIBUTING.rst +5 -2
  2. {sofar-1.0.0 → sofar-1.1.1}/HISTORY.rst +12 -0
  3. {sofar-1.0.0 → sofar-1.1.1}/PKG-INFO +4 -6
  4. {sofar-1.0.0 → sofar-1.1.1}/README.rst +1 -1
  5. {sofar-1.0.0 → sofar-1.1.1}/setup.cfg +1 -1
  6. {sofar-1.0.0 → sofar-1.1.1}/setup.py +3 -2
  7. {sofar-1.0.0 → sofar-1.1.1}/sofar/__init__.py +3 -2
  8. {sofar-1.0.0 → sofar-1.1.1}/sofar/io.py +92 -39
  9. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa.py +93 -69
  10. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/rules/upgrade.json +34 -0
  11. {sofar-1.0.0 → sofar-1.1.1}/sofar/update_conventions.py +27 -14
  12. {sofar-1.0.0 → sofar-1.1.1}/sofar/utils.py +18 -38
  13. {sofar-1.0.0 → sofar-1.1.1}/sofar.egg-info/PKG-INFO +4 -6
  14. {sofar-1.0.0 → sofar-1.1.1}/sofar.egg-info/SOURCES.txt +3 -2
  15. sofar-1.1.1/tests/test_deprecations.py +19 -0
  16. {sofar-1.0.0 → sofar-1.1.1}/tests/test_io.py +34 -26
  17. {sofar-1.0.0 → sofar-1.1.1}/tests/test_sofa.py +4 -9
  18. {sofar-1.0.0 → sofar-1.1.1}/tests/test_sofa_upgrade_conventions.py +10 -1
  19. {sofar-1.0.0 → sofar-1.1.1}/tests/test_sofa_verify.py +10 -10
  20. {sofar-1.0.0 → sofar-1.1.1}/tests/test_utils.py +30 -16
  21. {sofar-1.0.0 → sofar-1.1.1}/AUTHORS.rst +0 -0
  22. {sofar-1.0.0 → sofar-1.1.1}/LICENSE +0 -0
  23. {sofar-1.0.0 → sofar-1.1.1}/MANIFEST.in +0 -0
  24. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/VERSION +0 -0
  25. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.csv +0 -0
  26. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.json +0 -0
  27. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/FreeFieldHRIR_1.0.csv +0 -0
  28. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/FreeFieldHRIR_1.0.json +0 -0
  29. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/FreeFieldHRTF_1.0.csv +0 -0
  30. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/FreeFieldHRTF_1.0.json +0 -0
  31. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralFIR-E_2.0.csv +0 -0
  32. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralFIR-E_2.0.json +0 -0
  33. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralFIR_1.0.csv +0 -0
  34. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralFIR_1.0.json +0 -0
  35. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralSOS_1.0.csv +0 -0
  36. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralSOS_1.0.json +0 -0
  37. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralTF-E_1.0.csv +0 -0
  38. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralTF-E_1.0.json +0 -0
  39. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralTF_1.0.csv +0 -0
  40. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralTF_1.0.json +0 -0
  41. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralTF_2.0.csv +0 -0
  42. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/GeneralTF_2.0.json +0 -0
  43. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SimpleFreeFieldHRIR_1.0.csv +0 -0
  44. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SimpleFreeFieldHRIR_1.0.json +0 -0
  45. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SimpleFreeFieldHRSOS_1.0.csv +0 -0
  46. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SimpleFreeFieldHRSOS_1.0.json +0 -0
  47. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.csv +0 -0
  48. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.json +0 -0
  49. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SimpleFreeFieldSOS_1.0.csv +0 -0
  50. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SimpleFreeFieldSOS_1.0.json +0 -0
  51. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SimpleHeadphoneIR_1.0.csv +0 -0
  52. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SimpleHeadphoneIR_1.0.json +0 -0
  53. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SingleRoomMIMOSRIR_1.0.csv +0 -0
  54. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SingleRoomMIMOSRIR_1.0.json +0 -0
  55. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SingleRoomSRIR_1.0.csv +0 -0
  56. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/SingleRoomSRIR_1.0.json +0 -0
  57. {sofar-1.0.0/sofar/sofa_conventions/conventions → sofar-1.1.1/sofar/sofa_conventions/conventions/deprecated}/FreeFieldDirectivityTF_1.0.csv +0 -0
  58. {sofar-1.0.0/sofar/sofa_conventions/conventions → sofar-1.1.1/sofar/sofa_conventions/conventions/deprecated}/FreeFieldDirectivityTF_1.0.json +0 -0
  59. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/GeneralFIRE_1.0.csv +0 -0
  60. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/GeneralFIRE_1.0.json +0 -0
  61. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.csv +0 -0
  62. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.json +0 -0
  63. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.csv +0 -0
  64. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.json +0 -0
  65. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.csv +0 -0
  66. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.json +0 -0
  67. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_1.0.csv +0 -0
  68. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_1.0.json +0 -0
  69. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.1.csv +0 -0
  70. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.1.json +0 -0
  71. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.csv +0 -0
  72. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.json +0 -0
  73. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.2.csv +0 -0
  74. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.2.json +0 -0
  75. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.3.csv +0 -0
  76. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.3.json +0 -0
  77. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/rules/deprecations.json +0 -0
  78. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/rules/rules.json +0 -0
  79. {sofar-1.0.0 → sofar-1.1.1}/sofar/sofa_conventions/rules/unit_aliases.json +0 -0
  80. {sofar-1.0.0 → sofar-1.1.1}/sofar.egg-info/dependency_links.txt +0 -0
  81. {sofar-1.0.0 → sofar-1.1.1}/sofar.egg-info/not-zip-safe +0 -0
  82. {sofar-1.0.0 → sofar-1.1.1}/sofar.egg-info/requires.txt +0 -0
  83. {sofar-1.0.0 → sofar-1.1.1}/sofar.egg-info/top_level.txt +0 -0
  84. {sofar-1.0.0 → sofar-1.1.1}/tests/__init__.py +0 -0
@@ -178,9 +178,12 @@ before building the documentation.
178
178
  Submodules
179
179
  ~~~~~~~~~~
180
180
 
181
- To update the submodules containing the conventions and verification rules run
181
+ To update the submodule containing the conventions and verification rules run
182
182
 
183
- $ git submodule update --remote sofar/sofa_conventions
183
+ $ git submodule update --init --recursive
184
+ $ git submodule update --recursive --remote
185
+
186
+ and then commit the changes
184
187
 
185
188
 
186
189
  Deploying
@@ -1,6 +1,18 @@
1
1
  History
2
2
  =======
3
3
 
4
+ 1.1.1 (2023-7-7)
5
+ ----------------
6
+ * Fix deploying to PyPi.org
7
+
8
+ 1.1.0 (2023-7-7)
9
+ ----------------
10
+ * Deprecate FreeFieldDirectivityTV 1.0 in favor of FreeFieldDirectivityTV 1.1 (according to sofaconventoins.org and AES69-2022)
11
+ * Add `sofar.read_sofa_as_netcdf` for reading SOFA files with erroneous data
12
+ * Document SOFA conventions on https://sofar.readthedocs.io/en/stable/resources/conventions.html. `Sofa.info()` will this be deprecated in sofar v1.3.0
13
+ * `sofar.read_sofa` and `sofar.write_sofa` now accept filenames and path objects
14
+ * Add testing for Python 3.11
15
+
4
16
  1.0.0 (2022-12-16)
5
17
  ------------------
6
18
  * Use SOFA conventions of version 2.1 from https://github.com/pyfar/sofa_conventions
@@ -1,17 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sofar
3
- Version: 1.0.0
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
@@ -81,7 +81,5 @@ format*, Audio Engineering Society, Inc., New York, NY, USA.
81
81
 
82
82
  P. Majdak, F. Zotter, F. Brinkmann, J. De Muynke, M. Mihocic, and M.
83
83
  Noisternig, "Spatially Oriented Format for Acoustics 2.1: Introduction and
84
- Recent Advances,” *J. Audio Eng. Soc.*, vol. 70, no. 7/8, pp. 565584,
84
+ Recent Advances", *J. Audio Eng. Soc.*, vol. 70, no. 7/8, pp. 565-584,
85
85
  Jul. 2022. DOI: https://doi.org/10.17743/jaes.2022.0026
86
-
87
-
@@ -55,5 +55,5 @@ format*, Audio Engineering Society, Inc., New York, NY, USA.
55
55
 
56
56
  P. Majdak, F. Zotter, F. Brinkmann, J. De Muynke, M. Mihocic, and M.
57
57
  Noisternig, "Spatially Oriented Format for Acoustics 2.1: Introduction and
58
- Recent Advances,” *J. Audio Eng. Soc.*, vol. 70, no. 7/8, pp. 565584,
58
+ Recent Advances", *J. Audio Eng. Soc.*, vol. 70, no. 7/8, pp. 565-584,
59
59
  Jul. 2022. DOI: https://doi.org/10.17743/jaes.2022.0026
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 1.0.0
2
+ current_version = 1.1.1
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -44,7 +44,8 @@ setup(
44
44
  'Programming Language :: Python :: 3',
45
45
  'Programming Language :: Python :: 3.8',
46
46
  'Programming Language :: Python :: 3.9',
47
- 'Programming Language :: Python :: 3.10'
47
+ 'Programming Language :: Python :: 3.10',
48
+ 'Programming Language :: Python :: 3.11'
48
49
  ],
49
50
  description="Maybe the most complete python package for SOFA files so far",
50
51
  install_requires=requirements,
@@ -64,7 +65,7 @@ setup(
64
65
  "Documentation": "https://sofar.readthedocs.io/",
65
66
  "Source Code": "https://github.com/pyfar/sofar",
66
67
  },
67
- version='1.0.0',
68
+ version='1.1.1',
68
69
  zip_safe=False,
69
70
  python_requires='>=3.8'
70
71
  )
@@ -4,11 +4,11 @@
4
4
 
5
5
  __author__ = """The pyfar developers"""
6
6
  __email__ = 'info@pyfar.org'
7
- __version__ = '1.0.0'
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']
@@ -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 packaging
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 filename. '.sofa' is appended to the filename, if it is not
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
- The SOFA object filled with the default values of the convention.
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
- if not filename.endswith('.sofa'):
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
- # get convention name and version
72
- convention = getattr(file, "SOFAConventions")
73
- all_attr.append("GLOBAL_SOFAConventions")
74
- version_in = getattr(file, "SOFAConventionsVersion")
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
- # check if convention and version exist
78
- version_out = _verify_convention_and_version(
79
- version_in, version_in, convention)
130
+ # check if convention and version exist
131
+ _verify_convention_and_version(version, convention)
80
132
 
81
- # get SOFA object with default values
82
- sofa = sf.Sofa(convention, version=version_out, verify=verify)
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._protected = False
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._protected = False
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._protected = False
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._protected = False
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._protected = True
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
- if not filename.endswith('.sofa'):
210
- raise ValueError("Filename must end with .sofa")
211
-
212
- # check if the latest version is used for writing and warn otherwise
213
- # if case required for writing SOFA test data that violates the conventions
214
- if sofa.GLOBAL_SOFAConventions != "invalid-value":
215
- latest = sf.Sofa(sofa.GLOBAL_SOFAConventions)
216
- latest = latest.GLOBAL_SOFAConventionsVersion
217
- current = sofa.GLOBAL_SOFAConventionsVersion
218
-
219
- if packaging.version.parse(current) < packaging.version.parse(latest):
220
- warnings.warn(("Writing SOFA object with outdated Convention "
221
- f"version {current}. Use version='latest' to write "
222
- f"data with version {latest}."))
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
@@ -89,35 +89,43 @@ class Sofa():
89
89
  """See class docstring"""
90
90
 
91
91
  # get convention
92
- self._convention = self._load_convention(convention, version)
92
+ if convention is not None:
93
+ self._convention = self._load_convention(convention, version)
93
94
 
94
- # update read only attributes
95
- self._read_only_attr = [
96
- key for key in self._convention.keys()
97
- if self._read_only(self._convention[key]["flags"])]
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
- # add attributes with default values
100
- self._convention_to_sofa(mandatory)
100
+ # add attributes with default values
101
+ self._convention_to_sofa(mandatory)
101
102
 
102
- # add and update the API
103
- # (mandatory=False can not be verified because some conventions have
104
- # default values that have optional variables as dependencies)
105
- if verify and not mandatory:
106
- self.verify(mode="read")
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
- self._protected = True
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._protected and not hasattr(self, name):
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._protected:
117
- raise TypeError(f"{name} is a read only attribute")
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._protected or \
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._update_convention(version="match")
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._update_convention(version="match")
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._protected = False
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._protected = True
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
- ``self.info('dimensions')``.
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. Call
569
- :py:func:`Sofa.info("optional") <sofar.sofar.Sofa.info>` to list all
570
- optional variables and attributes.
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._protected = False
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
- self._update_convention(version="match")
692
+ self._convention[key] = self._custom[key]
656
693
 
657
694
  # add attribute to object
658
695
  setattr(self, key, value)
659
- self._protected = True
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._update_convention(version="match")
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._protected = False
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._protected = True
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._update_convention("match")
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._protected = False
961
+ self.protected = False
925
962
  setattr(self, key, self._convention[key]["default"])
926
- self._protected = True
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._protected = False
1144
+ self.protected = False
1108
1145
  self._dimensions = {}
1109
1146
  self._api = {}
1110
- self._protected = True
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 _update_convention(self, version):
1588
+ def _reset_convention(self):
1552
1589
  """
1553
- Add SOFA convention to SOFA object in private attribute `_convention`.
1554
- If The object already contains a convention, it will be overwritten.
1555
-
1556
- Parameters
1557
- ----------
1558
- version : str
1559
- ``'latest'``
1560
- Use the latest API and upgrade the SOFA file if required.
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
- v_new = _verify_convention_and_version(
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, v_new)
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._protected = False
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._protected = True
1721
+ self.protected = True
1698
1722
 
1699
1723
  @staticmethod
1700
1724
  def _get_size_and_shape_of_string_var(value, key):