sofar 0.3.1__py2.py3-none-any.whl → 1.1.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. sofar/__init__.py +13 -7
  2. sofar/io.py +423 -0
  3. sofar/sofa.py +1795 -0
  4. sofar/sofa_conventions/VERSION +1 -0
  5. sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.csv +59 -0
  6. sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.json +444 -0
  7. sofar/{conventions/source → sofa_conventions/conventions}/FreeFieldHRIR_1.0.csv +3 -3
  8. sofar/{conventions → sofa_conventions/conventions}/FreeFieldHRIR_1.0.json +3 -3
  9. sofar/{conventions/source → sofa_conventions/conventions}/FreeFieldHRTF_1.0.csv +2 -2
  10. sofar/{conventions → sofa_conventions/conventions}/FreeFieldHRTF_1.0.json +3 -3
  11. sofar/{conventions/source → sofa_conventions/conventions}/GeneralFIR-E_2.0.csv +2 -2
  12. sofar/{conventions → sofa_conventions/conventions}/GeneralFIR-E_2.0.json +2 -2
  13. sofar/{conventions/source/GeneralFIR_2.0.csv → sofa_conventions/conventions/GeneralFIR_1.0.csv} +2 -2
  14. sofar/{conventions/GeneralFIR_2.0.json → sofa_conventions/conventions/GeneralFIR_1.0.json} +2 -2
  15. sofar/{conventions/source/GeneralFIR_1.0.csv → sofa_conventions/conventions/GeneralSOS_1.0.csv} +11 -11
  16. sofar/{conventions/GeneralFIR_1.0.json → sofa_conventions/conventions/GeneralSOS_1.0.json} +48 -37
  17. sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF-E_1.0.csv +3 -3
  18. sofar/{conventions → sofa_conventions/conventions}/GeneralTF-E_1.0.json +4 -4
  19. sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF_1.0.csv +1 -1
  20. sofar/{conventions → sofa_conventions/conventions}/GeneralTF_1.0.json +1 -1
  21. sofar/{conventions/source → sofa_conventions/conventions}/GeneralTF_2.0.csv +4 -4
  22. sofar/{conventions → sofa_conventions/conventions}/GeneralTF_2.0.json +4 -4
  23. sofar/sofa_conventions/conventions/SimpleFreeFieldHRIR_1.0.csv +47 -0
  24. sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldHRIR_1.0.json +1 -1
  25. sofar/{conventions/source → sofa_conventions/conventions}/SimpleFreeFieldHRSOS_1.0.csv +1 -1
  26. sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldHRSOS_1.0.json +1 -1
  27. sofar/{conventions/source/SimpleFreeFieldHRTF_2.0.csv → sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.csv} +3 -3
  28. sofar/{conventions/SimpleFreeFieldHRTF_2.0.json → sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.json} +4 -4
  29. sofar/{conventions/source → sofa_conventions/conventions}/SimpleHeadphoneIR_1.0.csv +9 -9
  30. sofar/sofa_conventions/conventions/SimpleHeadphoneIR_1.0.json +396 -0
  31. sofar/{conventions/source → sofa_conventions/conventions}/SingleRoomMIMOSRIR_1.0.csv +18 -8
  32. sofar/{conventions → sofa_conventions/conventions}/SingleRoomMIMOSRIR_1.0.json +124 -50
  33. sofar/{conventions/source → sofa_conventions/conventions}/SingleRoomSRIR_1.0.csv +18 -8
  34. sofar/{conventions → sofa_conventions/conventions}/SingleRoomSRIR_1.0.json +124 -50
  35. sofar/{conventions/source → sofa_conventions/conventions/deprecated}/FreeFieldDirectivityTF_1.0.csv +2 -2
  36. sofar/{conventions → sofa_conventions/conventions/deprecated}/FreeFieldDirectivityTF_1.0.json +2 -2
  37. sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.csv +48 -0
  38. sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.csv +43 -0
  39. sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.json +333 -0
  40. sofar/{conventions/source/SimpleFreeFieldHRIR_1.0.csv → sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.csv} +15 -18
  41. sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.json +340 -0
  42. sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_1.0.csv +44 -0
  43. sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_1.0.json +340 -0
  44. sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.1.csv +51 -0
  45. sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.1.json +396 -0
  46. sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.csv +51 -0
  47. sofar/{conventions/SimpleHeadphoneIR_1.0.json → sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.json} +3 -3
  48. sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.2.csv +47 -0
  49. sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.2.json +360 -0
  50. sofar/sofa_conventions/rules/deprecations.json +12 -0
  51. sofar/sofa_conventions/rules/rules.json +800 -0
  52. sofar/sofa_conventions/rules/unit_aliases.json +11 -0
  53. sofar/sofa_conventions/rules/upgrade.json +190 -0
  54. sofar/update_conventions.py +427 -0
  55. sofar/utils.py +315 -0
  56. {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/AUTHORS.rst +1 -0
  57. sofar-1.1.0.dist-info/METADATA +89 -0
  58. sofar-1.1.0.dist-info/RECORD +75 -0
  59. {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/WHEEL +1 -1
  60. {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/top_level.txt +1 -0
  61. tests/__init__.py +0 -0
  62. tests/test_deprecations.py +19 -0
  63. tests/test_io.py +344 -0
  64. tests/test_sofa.py +354 -0
  65. tests/test_sofa_upgrade_conventions.py +102 -0
  66. tests/test_sofa_verify.py +472 -0
  67. tests/test_utils.py +241 -0
  68. sofar/conventions/source/MultiSpeakerBRIR_0.3.csv +0 -48
  69. sofar/sofar.py +0 -2531
  70. sofar-0.3.1.dist-info/METADATA +0 -69
  71. sofar-0.3.1.dist-info/RECORD +0 -46
  72. /sofar/{conventions/source → sofa_conventions/conventions}/SimpleFreeFieldSOS_1.0.csv +0 -0
  73. /sofar/{conventions → sofa_conventions/conventions}/SimpleFreeFieldSOS_1.0.json +0 -0
  74. /sofar/{conventions/source → sofa_conventions/conventions/deprecated}/GeneralFIRE_1.0.csv +0 -0
  75. /sofar/{conventions → sofa_conventions/conventions/deprecated}/GeneralFIRE_1.0.json +0 -0
  76. /sofar/{conventions → sofa_conventions/conventions/deprecated}/MultiSpeakerBRIR_0.3.json +0 -0
  77. /sofar/{conventions/source → sofa_conventions/conventions/deprecated}/SingleRoomDRIR_0.3.csv +0 -0
  78. /sofar/{conventions → sofa_conventions/conventions/deprecated}/SingleRoomDRIR_0.3.json +0 -0
  79. {sofar-0.3.1.dist-info → sofar-1.1.0.dist-info}/LICENSE +0 -0
sofar/__init__.py CHANGED
@@ -4,18 +4,24 @@
4
4
 
5
5
  __author__ = """The pyfar developers"""
6
6
  __email__ = 'info@pyfar.org'
7
- __version__ = '0.3.1'
7
+ __version__ = '1.1.0'
8
8
 
9
+ from .sofa import Sofa
9
10
 
10
- from .sofar import (Sofa,
11
- list_conventions,
12
- read_sofa,
13
- write_sofa,
14
- equals)
11
+ from .io import read_sofa, read_sofa_as_netcdf, write_sofa
12
+
13
+ from .utils import (list_conventions,
14
+ equals,
15
+ version)
16
+
17
+ from .update_conventions import update_conventions
15
18
 
16
19
 
17
20
  __all__ = ['Sofa',
21
+ 'update_conventions',
18
22
  'list_conventions',
19
23
  'read_sofa',
24
+ 'read_sofa_as_netcdf',
20
25
  'write_sofa',
21
- 'equals']
26
+ 'equals',
27
+ 'version']
sofar/io.py ADDED
@@ -0,0 +1,423 @@
1
+ import contextlib
2
+ import os
3
+ import numpy as np
4
+ from netCDF4 import Dataset, chartostring, stringtochar
5
+ import warnings
6
+ import pathlib
7
+ from packaging.version import parse
8
+ import sofar as sf
9
+ from .utils import _verify_convention_and_version, _atleast_nd
10
+
11
+
12
+ def read_sofa(filename, verify=True, verbose=True):
13
+ """
14
+ Read SOFA file from disk and convert it to SOFA object.
15
+
16
+ Numeric data is returned as floats or numpy float arrays unless they have
17
+ missing data, in which case they are returned as numpy masked arrays.
18
+
19
+ Parameters
20
+ ----------
21
+ filename : str
22
+ The full path to the sofa data.
23
+ verify : bool, optional
24
+ Verify and update the SOFA object by calling :py:func:`~Sofa.verify`.
25
+ This helps to find potential errors in the default values and is thus
26
+ recommended. If reading a file does not work, try to call `Sofa` with
27
+ ``verify=False``. The default is ``True``.
28
+ verbose : bool, optional
29
+ Print the names of detected custom variables and attributes. The
30
+ default is ``True``
31
+
32
+ Returns
33
+ -------
34
+ sofa : Sofa
35
+ Object containing the data from `filename`.
36
+
37
+ Notes
38
+ -----
39
+
40
+ 1. Missing dimensions are appended when writing the SOFA object to disk.
41
+ E.g., if ``sofa.Data_IR`` is of shape (1, 2) it is written as an array
42
+ of shape (1, 2, 1) because the SOFA standard AES69-2020 defines it as a
43
+ three dimensional array with the dimensions (`M: measurements`,
44
+ `R: receivers`, `N: samples`)
45
+ 2. When reading data from a SOFA file, array data is always returned as
46
+ numpy arrays and singleton trailing dimensions are discarded (numpy
47
+ default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
48
+ after writing and reading to and from disk.
49
+ 3. One dimensional arrays with only one element will be converted to scalar
50
+ values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
51
+ (1, ) inside SOFA files (according to the SOFA standard AES69-2020) but
52
+ will be a scalar inside SOFA objects after reading from disk.
53
+ """
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
+
110
+ # check the filename
111
+ filename = pathlib.Path(filename).with_suffix('.sofa')
112
+ if not os.path.isfile(filename):
113
+ raise ValueError(f"{filename} does not exist")
114
+
115
+ # attributes that are skipped
116
+ skip = ["_Encoding"]
117
+
118
+ # init list of all and custom attributes
119
+ all_attr = []
120
+ custom = []
121
+
122
+ # open new NETCDF4 file for reading
123
+ with Dataset(filename, "r", format="NETCDF4") as file:
124
+
125
+ if mode == "sofa":
126
+ # get convention name and version
127
+ convention = getattr(file, "SOFAConventions")
128
+ version = getattr(file, "SOFAConventionsVersion")
129
+
130
+ # check if convention and version exist
131
+ _verify_convention_and_version(version, convention)
132
+
133
+ # get SOFA object with default values
134
+ sofa = sf.Sofa(convention, version=version, verify=verify)
135
+ else:
136
+ sofa = sf.Sofa(None)
137
+
138
+ # allow writing read only attributes
139
+ sofa.protected = False
140
+
141
+ # load global attributes
142
+ for attr in file.ncattrs():
143
+
144
+ value = getattr(file, attr)
145
+ all_attr.append(f"GLOBAL_{attr}")
146
+
147
+ if not hasattr(sofa, f"GLOBAL_{attr}"):
148
+ sofa._add_custom_api_entry(
149
+ f"GLOBAL_{attr}", value, None, None, "attribute")
150
+ custom.append(f"GLOBAL_{attr}")
151
+ sofa.protected = False
152
+ else:
153
+ setattr(sofa, f"GLOBAL_{attr}", value)
154
+
155
+ # load data
156
+ for var in file.variables.keys():
157
+
158
+ value = _format_value_from_netcdf(file[var][:], var)
159
+ all_attr.append(var.replace(".", "_"))
160
+
161
+ if hasattr(sofa, var.replace(".", "_")):
162
+ setattr(sofa, var.replace(".", "_"), value)
163
+ else:
164
+ dimensions = "".join(list(file[var].dimensions))
165
+ # SOFA only uses dtypes 'double' and 'S1' but netCDF has more
166
+ dtype = "string" if file[var].datatype == "S1" else "double"
167
+ sofa._add_custom_api_entry(var.replace(".", "_"), value, None,
168
+ dimensions, dtype)
169
+ custom.append(var.replace(".", "_"))
170
+ sofa.protected = False
171
+
172
+ # load variable attributes
173
+ for attr in [a for a in file[var].ncattrs() if a not in skip]:
174
+
175
+ value = getattr(file[var], attr)
176
+ all_attr.append(var.replace(".", "_") + "_" + attr)
177
+
178
+ if not hasattr(sofa, var.replace(".", "_") + "_" + attr):
179
+ sofa._add_custom_api_entry(
180
+ var.replace(".", "_") + "_" + attr, value, None,
181
+ None, "attribute")
182
+ custom.append(var.replace(".", "_") + "_" + attr)
183
+ sofa.protected = False
184
+ else:
185
+ setattr(sofa, var.replace(".", "_") + "_" + attr, value)
186
+
187
+ # remove fields from initial Sofa object that were not contained in NetCDF
188
+ # file (initial Sofa object contained mandatory and optional fields)
189
+ attrs = [attr for attr in sofa.__dict__.keys() if not attr.startswith("_")]
190
+ for attr in attrs:
191
+ if attr not in all_attr:
192
+ delattr(sofa, attr)
193
+
194
+ # do not allow writing read only attributes any more
195
+ sofa.protected = True
196
+
197
+ # notice about custom entries
198
+ if custom and verbose:
199
+ print(("SOFA file contained custom entries\n"
200
+ "----------------------------------\n"
201
+ f"{', '.join(custom)}"))
202
+
203
+ # update api
204
+ if verify:
205
+ try:
206
+ sofa.verify(mode="read")
207
+ except: # noqa (No error handling - just improved verbosity)
208
+ raise ValueError((
209
+ "The SOFA object could not be verified, maybe due to erroneous"
210
+ " data. Call sofa=sofar.read_sofa(filename, verify=False) and "
211
+ "then sofa.verify() to get more information"))
212
+
213
+ return sofa
214
+
215
+
216
+ def write_sofa(filename: str, sofa: sf.Sofa, compression=4):
217
+ """
218
+ Write a SOFA object to disk as a SOFA file.
219
+
220
+ Parameters
221
+ ----------
222
+ filename : str
223
+ The filename. '.sofa' is appended to the filename, if it is not
224
+ explicitly given.
225
+ sofa : object
226
+ The SOFA object that is written to disk
227
+ compression : int
228
+ The level of compression with ``0`` being no compression and ``9``
229
+ being the best compression. The default of ``9`` optimizes the file
230
+ size but increases the time for writing files to disk.
231
+
232
+ Notes
233
+ -----
234
+
235
+ 1. Missing dimensions are appended when writing the SOFA object to disk.
236
+ E.g., if ``sofa.Data_IR`` is of shape (1, 2) it is written as an array
237
+ of shape (1, 2, 1) because the SOFA standard AES69-2020 defines it as a
238
+ three dimensional array with the dimensions (`M: measurements`,
239
+ `R: receivers`, `N: samples`)
240
+ 2. When reading data from a SOFA file, array data is always returned as
241
+ numpy arrays and singleton trailing dimensions are discarded (numpy
242
+ default). I.e., ``sofa.Data_IR`` will again be an array of shape (1, 2)
243
+ after writing and reading to and from disk.
244
+ 3. One dimensional arrays with only one element will be converted to scalar
245
+ values. E.g. ``sofa.Data_SamplingRate`` is stored as an array of shape
246
+ (1, ) inside SOFA files (according to the SOFA standard AES69-2020) but
247
+ will be a scalar inside SOFA objects after reading from disk.
248
+ """
249
+ _write_sofa(filename, sofa, compression, verify=True)
250
+
251
+
252
+ def _write_sofa(filename: str, sofa: sf.Sofa, compression=4, verify=True):
253
+ """
254
+ Private write function for writing invalid SOFA files for testing. See
255
+ write_sofa for documentation.
256
+ """
257
+
258
+ # check the filename
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."))
276
+
277
+ # setting the netCDF compression parameter
278
+ use_zlib = compression != 0
279
+
280
+ # update the dimensions
281
+ if verify:
282
+ sofa.verify(mode="write")
283
+
284
+ # list of all attribute names
285
+ all_keys = [key for key in sofa.__dict__.keys() if not key.startswith("_")]
286
+
287
+ # open new NETCDF4 file for writing
288
+ with Dataset(filename, "w", format="NETCDF4") as file:
289
+
290
+ # write dimensions
291
+ for dim in sofa._api:
292
+ file.createDimension(dim, sofa._api[dim])
293
+
294
+ # write global attributes
295
+ keys = [key for key in all_keys if key.startswith("GLOBAL_")]
296
+ for key in keys:
297
+ setattr(file, key[7:], str(getattr(sofa, key)))
298
+
299
+ # write data
300
+ for key in all_keys:
301
+
302
+ # skip attributes
303
+ # Note: This definition of attribute is blurry:
304
+ # lax definition:
305
+ # sofa._convention[key]["type"] == "attribute":
306
+ # strict definition:
307
+ # ("_" in key and not key.startswith("Data_")) or \
308
+ # key.count("_") > 1
309
+ #
310
+ # The strict definition is implicitly included in the SOFA standard
311
+ # since underscores only occur for variables starting with Data_
312
+ if sofa._convention[key]["type"] == "attribute":
313
+ continue
314
+
315
+ # get the data and type and shape
316
+ value, dtype = _format_value_for_netcdf(
317
+ getattr(sofa, key), key, sofa._convention[key]["type"],
318
+ sofa._dimensions[key], sofa._api["S"])
319
+
320
+ # create variable and write data
321
+ shape = tuple(list(sofa._dimensions[key]))
322
+ tmp_var = file.createVariable(
323
+ key.replace("Data_", "Data."), dtype, shape,
324
+ zlib=use_zlib, complevel=compression)
325
+ if dtype == "f8":
326
+ tmp_var[:] = value
327
+ else:
328
+ tmp_var[:] = stringtochar(value)
329
+ tmp_var._Encoding = "ascii"
330
+
331
+ # write variable attributes
332
+ sub_keys = [k for k in all_keys if k.startswith(f"{key}_")]
333
+ for sub_key in sub_keys:
334
+ setattr(tmp_var, sub_key[len(key)+1:],
335
+ str(getattr(sofa, sub_key)))
336
+
337
+
338
+ def _format_value_for_netcdf(value, key, dtype, dimensions, S):
339
+ """
340
+ Format value from SOFA object for saving in a NETCDF4 file.
341
+
342
+ Parameters
343
+ ----------
344
+ value : str, array like
345
+ The value to be formatted
346
+ key : str
347
+ The name of the current attribute. Needed for verbose errors.
348
+ dtype : str
349
+ The the data type of value
350
+ dimensions : str
351
+ The intended dimensions from ``sofa.dimensions``
352
+ S : int
353
+ Length of the string array.
354
+
355
+ Returns
356
+ -------
357
+ value : str, numpy array
358
+ The formatted value.
359
+ netcdf_dtype : str
360
+ The data type as a string for writing to a NETCDF4 file ('attribute',
361
+ 'f8', or 'S1').
362
+ """
363
+ # copy value
364
+ with contextlib.suppress(AttributeError):
365
+ value = value.copy()
366
+
367
+ # parse data
368
+ if dtype == "attribute":
369
+ value = str(value)
370
+ netcdf_dtype = "attribute"
371
+ elif dtype == "double":
372
+ value = _atleast_nd(value, len(dimensions))
373
+ netcdf_dtype = "f8"
374
+ elif dtype == "string":
375
+ value = np.array(value, dtype=f"S{str(S)}")
376
+ value = _atleast_nd(value, len(dimensions))
377
+ netcdf_dtype = 'S1'
378
+ else:
379
+ raise ValueError(f"Unknown type {dtype} for {key}")
380
+
381
+ return value, netcdf_dtype
382
+
383
+
384
+ def _format_value_from_netcdf(value, key):
385
+ """
386
+ Format value from NETCDF4 file for saving in a SOFA object
387
+
388
+ Parameters
389
+ ----------
390
+ value : np.array of dtype float or S
391
+ The value to be formatted
392
+ key : str
393
+ The variable name of the current value. Needed for verbose errors.
394
+
395
+ Returns
396
+ -------
397
+ value : str, number, numpy array
398
+ The formatted value.
399
+ """
400
+
401
+ if "float" in str(value.dtype) or "int" in str(value.dtype):
402
+ if np.ma.is_masked(value):
403
+ warnings.warn(f"Entry {key} contains missing data")
404
+ else:
405
+ # Convert to numpy array or scalar
406
+ value = np.asarray(value)
407
+ elif str(value.dtype)[1] in ["S", "U"]:
408
+ # string arrays are stored in masked arrays with empty strings '' being
409
+ # masked. Convert to regular arrays with unmasked empty strings
410
+ if str(value.dtype)[1] == "S":
411
+ value = chartostring(value, encoding="ascii")
412
+ value = np.atleast_1d(value).astype("U")
413
+ else:
414
+ raise TypeError(
415
+ f"{key}: value.dtype is {value.dtype} but must be float, S or, U")
416
+
417
+ # convert arrays to scalars if they do not store data that is usually used
418
+ # as scalar metadata, e.g., the SamplingRate
419
+ data_keys = ["Data_IR", "Data_Real", "Data_Imag", "Data_SOS" "Data_Delay"]
420
+ if value.size == 1 and key not in data_keys:
421
+ value = value[0]
422
+
423
+ return value