sofar 1.2.1__py3-none-any.whl

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