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
@@ -0,0 +1,356 @@
1
+ """
2
+ Write rules for validating SOFA files to json files.
3
+
4
+ Writes the following json files:
5
+ rules.json
6
+ Rules for verifying SOFA conventions and files
7
+ unit_aliases.json
8
+ Allowed aliases for the standard units
9
+
10
+ For a more detailed information about the json files refer to _readme.txt
11
+ """
12
+ # %%
13
+ import json
14
+ import os
15
+ from sofar.utils import _get_conventions
16
+
17
+ # deprecations
18
+ # NOTE: no further specific tests are enforced for deprecated conventions,
19
+ # i.e., there are no entries in `rules` for them.
20
+ deprecations = {
21
+ "GLOBAL:SOFAConventions": {
22
+ "GeneralFIRE":"GenerelFIR-E",
23
+ "MultiSpeakerBRIR": "SingleRoomMIMOSRIR",
24
+ "MusicalInstrumentDirectivityTF": "FreeFieldDirectivityTF",
25
+ "SimpleBRIR": "SingleRoomSRIR",
26
+ "SingleRoomDRIR": "SingleRoomSRIR",
27
+ "SimpleFreeFieldDirectivityTF": "FreeFieldDirectivityTF",
28
+ "SimpleFreeFieldSOS": "SimpleFreeFieldHRSOS",
29
+ "SimpleFreeFieldTF": "SimpleFreeFieldHRTF",
30
+ "SingleTrackedAudio":
31
+ "AnnotatedEmitterAudio or AnnotatedReceiverAudio",
32
+ },
33
+ }
34
+
35
+ # definition of valid coordinate systems. Not defined explicitly in AES69 but
36
+ # dozens of mentions of the following:
37
+ coords_min = ["cartesian", "spherical"]
38
+ coords_full = coords_min + ["spherical harmonics"]
39
+
40
+ # definition of units acordings to AES69 Table 7
41
+ units_min = ["metre", "degree, degree, metre"]
42
+ units_full = units_min + [units_min[1]]
43
+
44
+ # allowed alternative versions of units
45
+ # NOTE: AES69 allows multi-unit strings to be separated by comma, comma plus
46
+ # space and space (AES69 Section 4.7.8). This means that the unit
47
+ # "cubic metre" will be tested as a multi unit string and is thus also
48
+ # split in the aliases. A separate test for verifying that "cubic" is
49
+ # followed be "metre" must be performed.
50
+ unit_aliases = {
51
+ "metre": "metre",
52
+ "metres": "metre",
53
+ "meter": "metre",
54
+ "meters": "metre",
55
+ "cubic": "cubic",
56
+ "degree": "degree",
57
+ "degrees": "degree",
58
+ "second": "second",
59
+ "seconds": "second",
60
+ }
61
+ # possible values for restricted dimensions in the API
62
+ # Dimensions for spherical harmonics (considering orders up to 100) according
63
+ # to AES69 Eq. (5) and (6)
64
+ sh_dimension = ([(N+1)**2 for N in range(100)],
65
+ "(N+1)**2 where N is the spherical harmonics order")
66
+ # Dimensions for SOS (considering up to 100 sections) according to AES69
67
+ # Appendix C.5.2
68
+ sos_dimension = ([6 * (N + 1) for N in range(100)],
69
+ "an integer multiple of 6 greater 0")
70
+
71
+ # verification rules
72
+ rules = {
73
+ # Global --------------------------------------------------------------
74
+ # Check value of GLOBAL_DataType (AES69 Annex C)
75
+ # (FIRE and TFE are legacy data types from SOFA version 1.0)
76
+ "GLOBAL:DataType": {
77
+ "value": ["Audio", "FIR", "FIR-E", "FIRE", "TF", "TF-E", "SOS"],
78
+ "specific": {
79
+ "Audio": {
80
+ "Data.SamplingRate": None,
81
+ "Data.SamplingRate:Units": ["hertz"]},
82
+ "FIR": {
83
+ "Data.IR": None,
84
+ "Data.Delay": None,
85
+ "Data.SamplingRate": None,
86
+ "Data.SamplingRate:Units": ["hertz"]},
87
+ "FIR-E": {
88
+ "Data.IR": None,
89
+ "Data.Delay": None,
90
+ "Data.SamplingRate": None,
91
+ "Data.SamplingRate:Units": ["hertz"]},
92
+ "FIRE": {
93
+ "Data.IR": None,
94
+ "Data.Delay": None,
95
+ "Data.SamplingRate": None,
96
+ "Data.SamplingRate:Units": ["hertz"]},
97
+ "TF": {
98
+ "Data.Real": None,
99
+ "Data.Imag": None,
100
+ "N": None,
101
+ "N:Units": ["hertz"]},
102
+ "TF-E": {
103
+ "Data.Real": None,
104
+ "Data.Imag": None,
105
+ "N": None,
106
+ "N:Units": ["hertz"]},
107
+ "TFE": {
108
+ "Data.Real": None,
109
+ "Data.Imag": None,
110
+ "N": None,
111
+ "N:Units": ["hertz"]},
112
+ "SOS": {
113
+ "Data.SOS": None,
114
+ "Data.Delay": None,
115
+ "Data.SamplingRate": None,
116
+ "Data.SamplingRate:Units": ["hertz"],
117
+ # Checking the dimension of N if having SOS data
118
+ # (assuming up to 100 second order sections)
119
+ "_dimensions": {
120
+ "N": {
121
+ "value": sos_dimension[0],
122
+ "value_str": sos_dimension[1]},
123
+ }}}},
124
+ # Specified in AES69 Section 4.7.7 and Table 6
125
+ "GLOBAL:RoomType": {
126
+ "value": ["free field", "reverberant", "shoebox", "dae"],
127
+ "specific": {
128
+ "reverberant": {
129
+ "GLOBAL:RoomDescription": None},
130
+ "shoebox": {
131
+ "RoomCornerA": None,
132
+ "RoomCornerB": None},
133
+ "dae": {
134
+ "GLOBAL:RoomGeometry": None}}},
135
+ # Specified in AES69 Annex D
136
+ "GLOBAL:SOFAConventions": {
137
+ "value": _get_conventions(return_type="name"),
138
+ "specific": {
139
+ "AnnotatedEmitterAudio": {
140
+ "Data.Emitter": None},
141
+ "AnnotatedReceiverAudio": {
142
+ "Data.Receiver": None},
143
+ "GeneralFIR": {
144
+ "GLOBAL:DataType": ["FIR"]},
145
+ "GeneralFIR-E": {
146
+ "GLOBAL:DataType": ["FIR-E"]},
147
+ "GeneralTF": {
148
+ "GLOBAL:DataType": ["TF"]},
149
+ "GeneralTF-E": {
150
+ "GLOBAL:DataType": ["TF-E"]},
151
+ "SimpleFreeFieldHRIR": {
152
+ "GLOBAL:DataType": ["FIR"],
153
+ "GLOBAL:RoomType": ["free field"],
154
+ "EmitterPosition:Type": coords_min,
155
+ "_dimensions": {
156
+ "E": {
157
+ "value": [1],
158
+ "value_str": "1"}}},
159
+ "SimpleFreeFieldHRTF": {
160
+ "GLOBAL:DataType": ["TF"],
161
+ "GLOBAL:RoomType": ["free field"],
162
+ "EmitterPosition:Type": coords_min,
163
+ "_dimensions": {
164
+ "E": {
165
+ "value": [1],
166
+ "value_str": "1"}}},
167
+ "SimpleFreeFieldHRSOS": {
168
+ "GLOBAL:DataType": ["SOS"],
169
+ "GLOBAL:RoomType": ["free field"],
170
+ "EmitterPosition:Type": coords_min,
171
+ "_dimensions": {
172
+ "E": {
173
+ "value": [1],
174
+ "value_str": "1"}}},
175
+ "FreeFieldHRIR": {
176
+ "GLOBAL:DataType": ["FIR-E"],
177
+ "GLOBAL:RoomType": ["free field"]},
178
+ "FreeFieldHRTF": {
179
+ "GLOBAL:DataType": ["TF-E"],
180
+ "GLOBAL:RoomType": ["free field"]},
181
+ "SimpleHeadphoneIR": {
182
+ "GLOBAL:DataType": ["FIR"]},
183
+ "SingleRoomSRIR": {
184
+ "GLOBAL:DataType": ["FIR"]},
185
+ "SingleRoomMIMOSRIR": {
186
+ "GLOBAL:DataType": ["FIR-E"]},
187
+ "FreeFieldDirectivityTF": {
188
+ "GLOBAL:DataType": ["TF"]}}},
189
+ # check NLongName (AES69 Tables C.3 and C.4)
190
+ "N:LongName": {
191
+ "value": ["frequency"]},
192
+ # Listener ------------------------------------------------------------
193
+ # Possible values and dependencies are specified in
194
+ # AES69 Section 4.7.3
195
+ # ---------------------------------------------------------------------
196
+ # Check values and consistency of if ListenerPosition Type and Unit
197
+ "ListenerPosition:Type": {
198
+ "value": coords_min,
199
+ "specific": {
200
+ coords_min[0]: {
201
+ "ListenerPosition:Units": [units_min[0]]},
202
+ coords_min[1]: {
203
+ "ListenerPosition:Units": [units_min[1]]},
204
+ }},
205
+ # Check if dependencies of ListenerView are contained
206
+ "ListenerView": {
207
+ "value": None,
208
+ "general": ["ListenerView:Type", "ListenerView:Units"]},
209
+ # Check values and consistency of if ListenerView Type and Unit
210
+ "ListenerView:Type": {
211
+ "value": coords_min,
212
+ "specific": {
213
+ coords_min[0]: {
214
+ "ListenerView:Units": [units_min[0]]},
215
+ coords_min[1]: {
216
+ "ListenerView:Units": [units_min[1]]},
217
+ }},
218
+ # Check if dependencies of ListenerUp are contained
219
+ "ListenerUp": {
220
+ "value": None,
221
+ "general": ["ListenerView"]},
222
+ # Receiver ------------------------------------------------------------
223
+ # Possible values and dependencies are specified in
224
+ # AES69 Section 4.7.4
225
+ # ---------------------------------------------------------------------
226
+ # Check values and consistency of if ReceiverPosition Type and Unit
227
+ "ReceiverPosition:Type": {
228
+ "value": coords_full,
229
+ "specific": {
230
+ coords_full[0]: {
231
+ "ReceiverPosition:Units": [units_full[0]]},
232
+ coords_full[1]: {
233
+ "ReceiverPosition:Units": [units_full[1]]},
234
+ coords_full[2]: {
235
+ "ReceiverPosition:Units": [units_full[2]],
236
+ "_dimensions": {
237
+ # Check dimension R if using spherical harmonics for the
238
+ # receiver (assuming SH orders < 100)
239
+ "R": {
240
+ "value": sh_dimension[0],
241
+ "value_str": sh_dimension[1]}}},
242
+ }},
243
+ # Check if dependencies of ReceiverView are contained
244
+ "ReceiverView": {
245
+ "value": None,
246
+ "general": ["ReceiverView:Type", "ReceiverView:Units"]},
247
+ # Check values and consistency of if ReceiverView Type and Unit
248
+ "ReceiverView:Type": {
249
+ "value": coords_min,
250
+ "specific": {
251
+ coords_min[0]: {
252
+ "ReceiverView:Units": [units_min[0]]},
253
+ coords_min[1]: {
254
+ "ReceiverView:Units": [units_min[1]]},
255
+ }},
256
+ # Check if dependencies of ReceiverUp are contained
257
+ "ReceiverUp": {
258
+ "value": None,
259
+ "general": ["ReceiverView"]},
260
+ # Source --------------------------------------------------------------
261
+ # Possible values and dependencies are specified in
262
+ # AES69 Section 4.7.5
263
+ # ---------------------------------------------------------------------
264
+ # Check values and consistency of if SourcePosition Type and Unit
265
+ "SourcePosition:Type": {
266
+ "value": coords_min,
267
+ "specific": {
268
+ coords_min[0]: {
269
+ "SourcePosition:Units": [units_min[0]]},
270
+ coords_min[1]: {
271
+ "SourcePosition:Units": [units_min[1]]},
272
+ }},
273
+ # Check if dependencies of SourceView are contained
274
+ "SourceView": {
275
+ "value": None,
276
+ "general": ["SourceView:Type", "SourceView:Units"]},
277
+ # Check values and consistency of if SourceView Type and Unit
278
+ "SourceView:Type": {
279
+ "value": coords_min,
280
+ "specific": {
281
+ coords_min[0]: {
282
+ "SourceView:Units": [units_min[0]]},
283
+ coords_min[1]: {
284
+ "SourceView:Units": [units_min[1]]},
285
+ }},
286
+ # Check if dependencies of SourceUp are contained
287
+ "SourceUp": {
288
+ "value": None,
289
+ "general": ["SourceView"]},
290
+ # Emitter -------------------------------------------------------------
291
+ # Possible values and dependencies are specified in
292
+ # AES69 Section 4.7.6
293
+ # ---------------------------------------------------------------------
294
+ # Check values and consistency of if EmitterPosition Type and Unit
295
+ "EmitterPosition:Type": {
296
+ "value": coords_full,
297
+ "specific": {
298
+ coords_full[0]: {
299
+ "EmitterPosition:Units": [units_full[0]]},
300
+ coords_full[1]: {
301
+ "EmitterPosition:Units": [units_full[1]]},
302
+ coords_full[2]: {
303
+ "EmitterPosition:Units": [units_full[2]],
304
+ "_dimensions": {
305
+ # Check dimension R if using spherical harmonics for the
306
+ # receiver (assuming SH orders < 100)
307
+ "E": {
308
+ "value": sh_dimension[0],
309
+ "value_str": sh_dimension[1]}}},
310
+ }},
311
+ # Check if dependencies of EmitterView are contained
312
+ "EmitterView": {
313
+ "value": None,
314
+ "general": ["EmitterView:Type", "EmitterView:Units"]},
315
+ # Check values and consistency of if EmitterView Type and Unit
316
+ "EmitterView:Type": {
317
+ "value": coords_min,
318
+ "specific": {
319
+ coords_min[0]: {
320
+ "EmitterView:Units": [units_min[0]]},
321
+ coords_min[1]: {
322
+ "EmitterView:Units": [units_min[1]]},
323
+ }},
324
+ # Check if dependencies of EmitterUp are contained
325
+ "EmitterUp": {
326
+ "value": None,
327
+ "general": ["EmitterView"]},
328
+ # Check if dependencies of EmitterDescriptions are contained (specified in
329
+ # 5.3, D.12.5, D.13.5, D.14.5, D15.5)
330
+ "EmitterDescriptions": {
331
+ "value": None,
332
+ "general": ["GLOBAL:EmitterDescription"]},
333
+ # Room ----------------------------------------------------------------
334
+ # Possible values and dependencies are specified in
335
+ # AES69 Section 4.7.7
336
+ # ---------------------------------------------------------------------
337
+ "RoomVolume": {
338
+ "value": None,
339
+ "general": ["RoomVolume:Units"]},
340
+ "RoomTemperature": {
341
+ "value": None,
342
+ "general": ["RoomTemperature:Units"]},
343
+ "RoomVolume:Units": {
344
+ "value": ["cubic metre"]},
345
+ "RoomTemperature:Units": {
346
+ "value": ["kelvin"]},
347
+ }
348
+
349
+ # write to json files
350
+ for content, name in zip([rules, unit_aliases, deprecations],
351
+ ["rules", "unit_aliases", "deprecations"]):
352
+
353
+ json_file = os.path.join(
354
+ os.path.dirname(__file__), "rules", name + '.json')
355
+ with open(json_file, 'w') as file:
356
+ json.dump(content, file, indent=4)
sofar/sofastream.py ADDED
@@ -0,0 +1,301 @@
1
+ """
2
+ Read desired data from SOFA-file directly from disk without loading entire
3
+ file into memory.
4
+ """
5
+ from netCDF4 import Dataset
6
+ import numpy as np
7
+
8
+
9
+ class SofaStream():
10
+ """
11
+ Read desired data from SOFA-file directly from disk without loading entire
12
+ file into memory.
13
+
14
+ :class:`SofaStream` opens a SOFA-file and retrieves only the requested
15
+ data.
16
+
17
+ If you want to use all the data from a SOFA-file use :class:`Sofa`
18
+ class and :func:`read_sofa` function instead.
19
+
20
+ Parameters
21
+ ----------
22
+ filename : str
23
+ Full path to a SOFA-file
24
+
25
+ Returns
26
+ -------
27
+ sofa_stream : SofaStream
28
+ A SofaStream object which reads directly from the file.
29
+
30
+ Examples
31
+ --------
32
+ Get an attribute from a SOFA-file:
33
+
34
+ >>> import sofar as sf
35
+ >>> filename = "path/to/file.sofa"
36
+ >>> with sf.SofaStream(filename) as file:
37
+ >>> data = file.GLOBAL_RoomType
38
+ >>> print(data)
39
+ free field
40
+
41
+ Get a variable from a SOFA-file:
42
+
43
+ >>> with SofaStream(filename) as file:
44
+ >>> data = file.Data_IR
45
+ >>> print(data)
46
+ <class 'netCDF4._netCDF4.Variable'>
47
+ float64 Data.IR(M, R, N)
48
+ unlimited dimensions:
49
+ current shape = (11950, 2, 256)
50
+ filling on, default _FillValue of 9.969209968386869e+36 used
51
+
52
+ What is returned is a `netCDF-variable`. To access the values (in this
53
+ example the IRs) the variable needs to be sliced:
54
+
55
+ >>> with SofaStream(filename) as file:
56
+ >>> data = file.Data_IR
57
+ >>> # get all values
58
+ >>> all_irs = data[:]
59
+ >>> print(all_irs.shape)
60
+ (11950, 2, 256)
61
+ >>> # get data from first channel
62
+ >>> specific_irs = data[:,0,:]
63
+ >>> print(specific_irs.shape)
64
+ (11950, 256)
65
+ """
66
+
67
+ def __init__(self, filename):
68
+ """Initialize a new SofaStream object (see documentation above)."""
69
+ self._filename = filename
70
+
71
+ def __enter__(self):
72
+ """
73
+ Executed when entering a ``with`` statement
74
+ (see documentation above).
75
+ """
76
+ self._file = Dataset(self._filename, mode="r")
77
+ return self
78
+
79
+ def __exit__(self, *args):
80
+ """
81
+ Executed when exiting a ``with`` statement
82
+ (see documentation above).
83
+ """
84
+ self._file.close()
85
+
86
+ def __getattr__(self, name):
87
+ """
88
+ Executed when accessing data within a with statement
89
+ (see documentation above).
90
+ """
91
+ # get netCDF4-attributes and -variable-keys from SOFA-file
92
+ dset_variables = np.array(list(self._file.variables.keys()))
93
+ dset_attributes = np.asarray(self._file.ncattrs())
94
+
95
+ # remove delimiter from passed sofar-attribute
96
+ name_netcdf = name.replace(
97
+ 'GLOBAL_', '').replace('Data_', 'Data.')
98
+
99
+ # Handle variable-attributes (e.g. '_Units' and '_Type')
100
+ var_attr = None
101
+ if "_" in name_netcdf:
102
+ name_netcdf, var_attr = name_netcdf.split('_')
103
+
104
+ # get value if passed attribute points to a netCDF4-variable
105
+ if name_netcdf in dset_variables:
106
+ # get variable from SOFA-file
107
+ self._data = self._file.variables[name_netcdf]
108
+ if var_attr is not None:
109
+ self._data = getattr(self._data, var_attr)
110
+
111
+ # get value if passed attribute points to a netCDF4-attribute
112
+ elif name_netcdf in dset_attributes:
113
+ # get attribute value from SOFA-file
114
+ self._data = self._file.getncattr(name_netcdf)
115
+
116
+ else:
117
+ raise AttributeError(f"{name} is not contained in SOFA-file")
118
+
119
+ return self._data
120
+
121
+ @property
122
+ def list_dimensions(self):
123
+ """
124
+ Print the dimensions of the SOFA-file.
125
+
126
+ See :py:func:`~SofaStream.inspect` to see the shapes of the data inside
127
+ the SOFA-file and :py:func:`~SofaStream.get_dimension` to get the
128
+ size/value of a specific dimensions as integer number.
129
+
130
+ The SOFA standard defines the following dimensions that are used
131
+ to define the shape of the data entries:
132
+
133
+ M
134
+ number of measurements
135
+ N
136
+ number of samples, frequencies, SOS coefficients
137
+ (depending on GLOBAL_DataType)
138
+ R
139
+ Number of receivers or SH coefficients
140
+ (depending on ReceiverPosition_Type)
141
+ E
142
+ Number of emitters or SH coefficients
143
+ (depending on EmitterPosition_Type)
144
+ S
145
+ Maximum length of a string in a string array
146
+ C
147
+ Size of the coordinate dimension. This is always three.
148
+ I
149
+ Single dimension. This is always one.
150
+
151
+ """
152
+ dim = self._file.dimensions
153
+
154
+ # get verbose description for dimesion N
155
+ if self._file.getncattr('DataType').startswith("FIR"):
156
+ N_verbose = "samples"
157
+ elif self._file.getncattr('DataType').startswith("TF"):
158
+ N_verbose = "frequencies"
159
+ elif self._file.getncattr('DataType').startswith("SOS"):
160
+ N_verbose = "SOS coefficients"
161
+
162
+ # get verbose description for dimensions R and E
163
+ R_verbose = (
164
+ "receiver spherical harmonics coefficients"
165
+ if 'harmonic'
166
+ in self._file.variables['ReceiverPosition'].getncattr('Type')
167
+ else "receiver"
168
+ )
169
+ E_verbose = (
170
+ "emitter spherical harmonics coefficients"
171
+ if 'harmonic'
172
+ in self._file.variables['EmitterPosition'].getncattr('Type')
173
+ else "emitter"
174
+ )
175
+
176
+ dimensions = {
177
+ "M": "measurements",
178
+ "N": N_verbose,
179
+ "R": R_verbose,
180
+ "E": E_verbose,
181
+ "S": "maximum string length",
182
+ "C": "coordinate dimensions, fixed",
183
+ "I": "single dimension, fixed"}
184
+
185
+ info_str = ""
186
+ for key, value in dim.items():
187
+ value = value.size
188
+ dim_info = dimensions[key] if key in dimensions \
189
+ else "custom dimension"
190
+
191
+ info_str += f"{key} = {value} {dim_info}" + '\n'
192
+
193
+ print(info_str)
194
+
195
+ def get_dimension(self, dimension):
196
+ """
197
+ Get size of a SOFA dimension.
198
+
199
+ SOFA dimensions specify the shape of the data contained in a SOFA-file.
200
+ For a list of all dimensions see :py:func:`~list_dimensions`, for more
201
+ information about the data contained in a SOFA-file use
202
+ :py:func:`~inspect`.
203
+
204
+ Parameters
205
+ ----------
206
+ dimension : str
207
+ The dimension as a string, e.g., ``'N'``.
208
+
209
+ Returns
210
+ -------
211
+ size : int
212
+ the size of the queried dimension.
213
+ """
214
+
215
+ # get dimensons from SOFA-file
216
+ dims = self._file.dimensions
217
+
218
+ if dimension not in dims.keys():
219
+ raise ValueError((
220
+ f"{dimension} is not a valid dimension. "
221
+ "See Sofa.list_dimensions for a list of valid dimensions."))
222
+
223
+ return dims[dimension].size
224
+
225
+ def inspect(self, file=None):
226
+ """
227
+ Get information about the data inside a SOFA-file.
228
+
229
+ Prints the values of all attributes and variables with six or less
230
+ entries and the shapes and type of all numeric and string variables.
231
+ When printing the values of arrays, single dimensions are discarded for
232
+ easy of display, i.e., an array of shape (1, 3, 2) will be displayed as
233
+ an array of shape (3, 2).
234
+
235
+ Parameters
236
+ ----------
237
+ file : str
238
+ Full path of a file under which the information is to be stored in
239
+ plain text. The default ``None`` only print the information to the
240
+ console.
241
+ """
242
+
243
+ # Header of inspect-print
244
+ info_str = (
245
+ f"{self._file.getncattr('SOFAConventions')} "
246
+ f"{self._file.getncattr('SOFAConventionsVersion')} "
247
+ f"(SOFA version {self._file.getncattr('Version')})\n")
248
+ info_str += "-" * len(info_str) + "\n"
249
+
250
+ # information for attributes
251
+ for attr in self._file.ncattrs():
252
+
253
+ value = self._file.getncattr(attr)
254
+ sofar_attr = f"GLOBAL_{attr}"
255
+ info_str += sofar_attr + ' : ' + str(value) + '\n'
256
+
257
+ # information for variables
258
+ for key in self._file.variables.keys():
259
+ # get values, shape and dimensions
260
+ data = self._file[key]
261
+ shape = data.shape
262
+ dimensions = data.dimensions
263
+
264
+ # add variable name to info-string
265
+ info_str += key.replace('.', '_') + ' : '
266
+
267
+ # pad shape if required (trailing single dimensions are
268
+ # discarded following the numpy default)
269
+ while len(shape) < len(dimensions):
270
+ shape += (1, )
271
+
272
+ # add value for scalars
273
+ if data.size == 1:
274
+ info_str += str(data[:][0]) + '\n'
275
+
276
+ # Handle multidimensional data
277
+ else:
278
+ # make verbose shape, e.g., '(M=100, R=2, N=128, '
279
+ shape_verbose = "("
280
+ for s, d in zip(shape, dimensions):
281
+ shape_verbose += f"{d}={s}, "
282
+
283
+ # add shape information
284
+ info_str += shape_verbose[:-2] + ")\n"
285
+ # add value information if not too much
286
+ if data.size < 7:
287
+ info_str += " " + \
288
+ str(np.squeeze(data[:])).replace("\n", "\n ") + "\n"
289
+
290
+ # Add variable-attributes to info string (e.g. 'Type' or 'Units)
291
+ for att_ in list(self._file[key].ncattrs()):
292
+ info_str += (key.replace('.', '_') + f'_{att_} : '
293
+ + getattr(data, att_) + '\n')
294
+
295
+ # write to text file
296
+ if file is not None:
297
+ with open(file, 'w') as f_id:
298
+ f_id.write(info_str + "\n")
299
+
300
+ # print to console
301
+ print(info_str)