uda-xarray 0.2.3__tar.gz → 0.3.0__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.
- {uda_xarray-0.2.3/uda_xarray.egg-info → uda_xarray-0.3.0}/PKG-INFO +2 -1
- {uda_xarray-0.2.3 → uda_xarray-0.3.0}/pyproject.toml +6 -2
- {uda_xarray-0.2.3 → uda_xarray-0.3.0}/tests/test_backend.py +218 -4
- uda_xarray-0.3.0/uda_xarray/__init__.py +1 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.0}/uda_xarray/main.py +6 -0
- uda_xarray-0.3.0/uda_xarray/mappings.py +78 -0
- uda_xarray-0.3.0/uda_xarray/mast.json +356 -0
- uda_xarray-0.3.0/uda_xarray/mastu.json +618 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.0/uda_xarray.egg-info}/PKG-INFO +2 -1
- {uda_xarray-0.2.3 → uda_xarray-0.3.0}/uda_xarray.egg-info/SOURCES.txt +4 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.0}/uda_xarray.egg-info/requires.txt +1 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.0}/LICENSE +0 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.0}/README.md +0 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.0}/setup.cfg +0 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.0}/uda_xarray.egg-info/dependency_links.txt +0 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.0}/uda_xarray.egg-info/entry_points.txt +0 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.0}/uda_xarray.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uda-xarray
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: UDA backend for xarray
|
|
5
5
|
Author-email: Samuel Jackson <samuel.jackson@ukaea.uk>
|
|
6
6
|
Maintainer-email: Samuel Jackson <samuel.jackson@ukaea.uk>
|
|
@@ -16,6 +16,7 @@ License-File: LICENSE
|
|
|
16
16
|
Requires-Dist: uda>=2.9.2
|
|
17
17
|
Requires-Dist: uda-mast
|
|
18
18
|
Requires-Dist: xarray
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
19
20
|
Dynamic: license-file
|
|
20
21
|
|
|
21
22
|
# uda-xarray
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "uda-xarray"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "UDA backend for xarray"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9, <3.13"
|
|
@@ -23,11 +23,15 @@ dependencies = [
|
|
|
23
23
|
"uda>=2.9.2",
|
|
24
24
|
"uda-mast",
|
|
25
25
|
"xarray",
|
|
26
|
+
"pydantic>=2.0",
|
|
26
27
|
]
|
|
27
28
|
|
|
28
29
|
[tool.setuptools]
|
|
29
30
|
packages = ["uda_xarray"]
|
|
30
31
|
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
uda_xarray = ["*.json"]
|
|
34
|
+
|
|
31
35
|
[project.urls]
|
|
32
36
|
repository = "https://github.com/samueljackson92/uda-xarray"
|
|
33
37
|
|
|
@@ -47,7 +51,7 @@ dev = [
|
|
|
47
51
|
uda = "uda_xarray.main:UDABackendEntrypoint"
|
|
48
52
|
|
|
49
53
|
[tool.bumpversion]
|
|
50
|
-
current_version = "0.
|
|
54
|
+
current_version = "0.3.0"
|
|
51
55
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
52
56
|
serialize = ["{major}.{minor}.{patch}"]
|
|
53
57
|
search = "{current_version}"
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""Tests for the UDA xarray backend."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
3
5
|
import numpy as np
|
|
4
6
|
import pyuda
|
|
5
|
-
|
|
7
|
+
import pytest
|
|
8
|
+
import xarray as xr
|
|
9
|
+
|
|
10
|
+
from uda_xarray.mappings import SignalMappings, SignalRange
|
|
6
11
|
|
|
7
12
|
|
|
8
13
|
def test_open_uda_dataset(mocker):
|
|
14
|
+
"""Test loading a 1D UDA signal as an xarray Dataset."""
|
|
9
15
|
# Create mock signal object
|
|
10
16
|
mock_signal = Mock()
|
|
11
17
|
mock_signal.data = np.array([1.0, 2.0, 3.0])
|
|
@@ -45,6 +51,7 @@ def test_open_uda_dataset(mocker):
|
|
|
45
51
|
|
|
46
52
|
|
|
47
53
|
def test_open_uda_dataset_2d(mocker):
|
|
54
|
+
"""Test loading a 2D UDA signal as an xarray Dataset."""
|
|
48
55
|
# Create mock 2D signal object
|
|
49
56
|
mock_signal = Mock()
|
|
50
57
|
mock_signal.data = np.array([[1.0, 2.0], [3.0, 4.0]])
|
|
@@ -81,6 +88,7 @@ def test_open_uda_dataset_2d(mocker):
|
|
|
81
88
|
|
|
82
89
|
|
|
83
90
|
def test_open_uda_dataset_video(mocker):
|
|
91
|
+
"""Test loading a UDA video signal as an xarray Dataset."""
|
|
84
92
|
mock_signal = Mock()
|
|
85
93
|
mock_signal.is_color = False
|
|
86
94
|
mock_signal.frame_times = np.array([0.0, 0.033, 0.066])
|
|
@@ -115,6 +123,7 @@ def test_open_uda_dataset_video(mocker):
|
|
|
115
123
|
|
|
116
124
|
|
|
117
125
|
def test_open_uda_dataset_invalid_signal(mocker):
|
|
126
|
+
"""Test that an invalid UDA signal raises a RuntimeError."""
|
|
118
127
|
# Mock the pyuda Client to raise an exception
|
|
119
128
|
mock_client = Mock()
|
|
120
129
|
mock_client.get.side_effect = pyuda.ServerException("Signal not found")
|
|
@@ -133,6 +142,7 @@ def test_open_uda_dataset_invalid_signal(mocker):
|
|
|
133
142
|
|
|
134
143
|
|
|
135
144
|
def test_open_uda_dataset_invalid_format():
|
|
145
|
+
"""Test that an invalid UDA URL format raises a ValueError."""
|
|
136
146
|
try:
|
|
137
147
|
xr.open_dataset("invalid_format", engine="uda")
|
|
138
148
|
except ValueError as e:
|
|
@@ -152,6 +162,7 @@ def test_open_uda_dataset_invalid_format():
|
|
|
152
162
|
|
|
153
163
|
|
|
154
164
|
def test_open_datatree(mocker):
|
|
165
|
+
"""Test loading a 1D UDA signal as an xarray DataTree."""
|
|
155
166
|
# Create mock signal object
|
|
156
167
|
mock_signal = Mock()
|
|
157
168
|
mock_signal.data = np.array([1.0, 2.0, 3.0])
|
|
@@ -195,6 +206,7 @@ def test_open_datatree(mocker):
|
|
|
195
206
|
|
|
196
207
|
|
|
197
208
|
def test_open_datatree_2d(mocker):
|
|
209
|
+
"""Test loading a 2D UDA signal as an xarray DataTree."""
|
|
198
210
|
# Create mock 2D signal object
|
|
199
211
|
mock_signal = Mock()
|
|
200
212
|
mock_signal.data = np.array([[1.0, 2.0], [3.0, 4.0]])
|
|
@@ -236,6 +248,7 @@ def test_open_datatree_2d(mocker):
|
|
|
236
248
|
|
|
237
249
|
|
|
238
250
|
def test_open_datatree_video(mocker):
|
|
251
|
+
"""Test loading a UDA video signal as an xarray DataTree."""
|
|
239
252
|
mock_signal = Mock()
|
|
240
253
|
mock_signal.is_color = False
|
|
241
254
|
mock_signal.frame_times = np.array([0.0, 0.033, 0.066])
|
|
@@ -276,7 +289,8 @@ def test_open_datatree_video(mocker):
|
|
|
276
289
|
|
|
277
290
|
|
|
278
291
|
@pytest.mark.skip(reason="Requires access to real UDA server with video data")
|
|
279
|
-
def test_open_uda_dataset_video_frame(
|
|
292
|
+
def test_open_uda_dataset_video_frame():
|
|
293
|
+
"""Test loading a single video frame from a real UDA server."""
|
|
280
294
|
ds = xr.open_dataset(
|
|
281
295
|
"uda://rba:30421", engine="uda", drop_variables=None, frame_number=1
|
|
282
296
|
)
|
|
@@ -288,8 +302,208 @@ def test_open_uda_dataset_video_frame(mocker):
|
|
|
288
302
|
|
|
289
303
|
@pytest.mark.skip(reason="Requires access to real UDA server with video data")
|
|
290
304
|
def test_real_data_2d():
|
|
305
|
+
"""Test loading real 2D data from a live UDA server."""
|
|
291
306
|
ds = xr.open_dataset("uda://AYE_TE:30421", engine="uda")
|
|
292
307
|
assert ds["data"].name == "data"
|
|
293
308
|
assert ds["data"].dims == ("time", "radial_index")
|
|
294
309
|
assert "time" in ds.coords
|
|
295
310
|
assert "radial_index" in ds.coords
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_remapped_signal_uses_legacy_name(mocker):
|
|
314
|
+
"""Requesting a canonical signal name for an old shot should transparently
|
|
315
|
+
remap it to the legacy stored name before calling the UDA client."""
|
|
316
|
+
|
|
317
|
+
# Build a minimal in-memory mapping:
|
|
318
|
+
# canonical "AYC_TE" -> "AYC_TE" for shots 22832-30471
|
|
319
|
+
# -> "ATM_TE" for shots 1-22831
|
|
320
|
+
fake_mappings = SignalMappings(
|
|
321
|
+
mappings={
|
|
322
|
+
"AYC_TE": [
|
|
323
|
+
SignalRange(shot_min=22832, shot_max=30471, name="AYC_TE"),
|
|
324
|
+
SignalRange(shot_min=1, shot_max=22831, name="ATM_TE"),
|
|
325
|
+
]
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
mocker.patch("uda_xarray.main._MAPPINGS", fake_mappings)
|
|
329
|
+
|
|
330
|
+
mock_signal = Mock()
|
|
331
|
+
mock_signal.data = np.array([1.0, 2.0, 3.0])
|
|
332
|
+
mock_signal.shape = (3,)
|
|
333
|
+
dim1 = Mock(label="time")
|
|
334
|
+
dim1.data = np.array([0.0, 1.0, 2.0])
|
|
335
|
+
mock_signal.dims = [dim1]
|
|
336
|
+
mock_signal.units = "eV"
|
|
337
|
+
mock_signal.time = Mock(label="time", data=np.array([0.0, 1.0, 2.0]))
|
|
338
|
+
mock_signal.errors = Mock(data=np.array([0.01, 0.01, 0.01]))
|
|
339
|
+
|
|
340
|
+
mock_client = Mock()
|
|
341
|
+
mock_client.get.return_value = mock_signal
|
|
342
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
343
|
+
mocker.patch(
|
|
344
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
345
|
+
return_value="Signal",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Shot 15000 is in the legacy range -> should call client with "ATM_TE"
|
|
349
|
+
ds = xr.open_dataset("uda://AYC_TE:15000", engine="uda")
|
|
350
|
+
|
|
351
|
+
mock_client.get.assert_called_once_with("ATM_TE", 15000)
|
|
352
|
+
assert ds["data"].attrs["uda_name"] == "ATM_TE"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_remapped_signal_uses_modern_name(mocker):
|
|
356
|
+
"""Requesting a canonical signal name for a recent shot should keep the
|
|
357
|
+
modern stored name."""
|
|
358
|
+
|
|
359
|
+
fake_mappings = SignalMappings(
|
|
360
|
+
mappings={
|
|
361
|
+
"AYC_TE": [
|
|
362
|
+
SignalRange(shot_min=22832, shot_max=30471, name="AYC_TE"),
|
|
363
|
+
SignalRange(shot_min=1, shot_max=22831, name="ATM_TE"),
|
|
364
|
+
]
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
mocker.patch("uda_xarray.main._MAPPINGS", fake_mappings)
|
|
368
|
+
|
|
369
|
+
mock_signal = Mock()
|
|
370
|
+
mock_signal.data = np.array([1.0, 2.0, 3.0])
|
|
371
|
+
mock_signal.shape = (3,)
|
|
372
|
+
dim1 = Mock(label="time")
|
|
373
|
+
dim1.data = np.array([0.0, 1.0, 2.0])
|
|
374
|
+
mock_signal.dims = [dim1]
|
|
375
|
+
mock_signal.units = "eV"
|
|
376
|
+
mock_signal.time = Mock(label="time", data=np.array([0.0, 1.0, 2.0]))
|
|
377
|
+
mock_signal.errors = Mock(data=np.array([0.01, 0.01, 0.01]))
|
|
378
|
+
|
|
379
|
+
mock_client = Mock()
|
|
380
|
+
mock_client.get.return_value = mock_signal
|
|
381
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
382
|
+
mocker.patch(
|
|
383
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
384
|
+
return_value="Signal",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Shot 30000 is in the modern range -> should call client with "AYC_TE"
|
|
388
|
+
ds = xr.open_dataset("uda://AYC_TE:30000", engine="uda")
|
|
389
|
+
|
|
390
|
+
mock_client.get.assert_called_once_with("AYC_TE", 30000)
|
|
391
|
+
assert ds["data"].attrs["uda_name"] == "AYC_TE"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_mastu_hcam_remapped_to_sanx_name(mocker):
|
|
395
|
+
"""For a MASTU shot in the SAnx05 range, the canonical HCAM channel name
|
|
396
|
+
should be remapped to the SAnx05 digitiser path."""
|
|
397
|
+
|
|
398
|
+
# /XSX/HCAM/L/CH01/DATA is the canonical name; for shots 46353-49476 the
|
|
399
|
+
# data was stored as /xsx/SAnx05-01/ch14.
|
|
400
|
+
fake_mappings = SignalMappings(
|
|
401
|
+
mappings={
|
|
402
|
+
"/XSX/HCAM/L/CH01/DATA": [
|
|
403
|
+
SignalRange(
|
|
404
|
+
shot_min=49904, shot_max=51056, name="/XSX/HCAM/L/CH01/DATA"
|
|
405
|
+
),
|
|
406
|
+
SignalRange(shot_min=49476, shot_max=49904, name="/XSX/HCAM/L/CH01"),
|
|
407
|
+
SignalRange(shot_min=46353, shot_max=49476, name="/xsx/SAnx05-01/ch14"),
|
|
408
|
+
SignalRange(shot_min=44395, shot_max=46353, name="/XSX/HCAM/L/CH01"),
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
mocker.patch("uda_xarray.main._MAPPINGS", fake_mappings)
|
|
413
|
+
|
|
414
|
+
mock_signal = Mock()
|
|
415
|
+
mock_signal.data = np.array([1.0, 2.0, 3.0])
|
|
416
|
+
mock_signal.shape = (3,)
|
|
417
|
+
dim1 = Mock(label="time")
|
|
418
|
+
dim1.data = np.array([0.0, 1.0, 2.0])
|
|
419
|
+
mock_signal.dims = [dim1]
|
|
420
|
+
mock_signal.units = "V"
|
|
421
|
+
mock_signal.time = Mock(label="time", data=np.array([0.0, 1.0, 2.0]))
|
|
422
|
+
mock_signal.errors = Mock(data=np.array([0.01, 0.01, 0.01]))
|
|
423
|
+
|
|
424
|
+
mock_client = Mock()
|
|
425
|
+
mock_client.get.return_value = mock_signal
|
|
426
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
427
|
+
mocker.patch(
|
|
428
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
429
|
+
return_value="Signal",
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Shot 47000 falls in the SAnx05 range -> should call client with ch14 path
|
|
433
|
+
ds = xr.open_dataset("uda:///XSX/HCAM/L/CH01/DATA:47000", engine="uda")
|
|
434
|
+
|
|
435
|
+
mock_client.get.assert_called_once_with("/xsx/SAnx05-01/ch14", 47000)
|
|
436
|
+
assert ds["data"].attrs["uda_name"] == "/xsx/SAnx05-01/ch14"
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_mastu_hcam_modern_name_passthrough(mocker):
|
|
440
|
+
"""For the most-recent MASTU shot range, the canonical HCAM channel name
|
|
441
|
+
should be used unchanged (it already matches the stored path)."""
|
|
442
|
+
|
|
443
|
+
fake_mappings = SignalMappings(
|
|
444
|
+
mappings={
|
|
445
|
+
"/XSX/HCAM/L/CH01/DATA": [
|
|
446
|
+
SignalRange(
|
|
447
|
+
shot_min=49904, shot_max=51056, name="/XSX/HCAM/L/CH01/DATA"
|
|
448
|
+
),
|
|
449
|
+
SignalRange(shot_min=49476, shot_max=49904, name="/XSX/HCAM/L/CH01"),
|
|
450
|
+
SignalRange(shot_min=46353, shot_max=49476, name="/xsx/SAnx05-01/ch14"),
|
|
451
|
+
SignalRange(shot_min=44395, shot_max=46353, name="/XSX/HCAM/L/CH01"),
|
|
452
|
+
]
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
mocker.patch("uda_xarray.main._MAPPINGS", fake_mappings)
|
|
456
|
+
|
|
457
|
+
mock_signal = Mock()
|
|
458
|
+
mock_signal.data = np.array([1.0, 2.0, 3.0])
|
|
459
|
+
mock_signal.shape = (3,)
|
|
460
|
+
dim1 = Mock(label="time")
|
|
461
|
+
dim1.data = np.array([0.0, 1.0, 2.0])
|
|
462
|
+
mock_signal.dims = [dim1]
|
|
463
|
+
mock_signal.units = "V"
|
|
464
|
+
mock_signal.time = Mock(label="time", data=np.array([0.0, 1.0, 2.0]))
|
|
465
|
+
mock_signal.errors = Mock(data=np.array([0.01, 0.01, 0.01]))
|
|
466
|
+
|
|
467
|
+
mock_client = Mock()
|
|
468
|
+
mock_client.get.return_value = mock_signal
|
|
469
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
470
|
+
mocker.patch(
|
|
471
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
472
|
+
return_value="Signal",
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Shot 50000 is in the most-recent range -> canonical name used as-is
|
|
476
|
+
ds = xr.open_dataset("uda:///XSX/HCAM/L/CH01/DATA:50000", engine="uda")
|
|
477
|
+
|
|
478
|
+
mock_client.get.assert_called_once_with("/XSX/HCAM/L/CH01/DATA", 50000)
|
|
479
|
+
assert ds["data"].attrs["uda_name"] == "/XSX/HCAM/L/CH01/DATA"
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def test_unmapped_signal_passes_through(mocker):
|
|
483
|
+
"""A signal not present in the mappings should be forwarded as-is."""
|
|
484
|
+
|
|
485
|
+
fake_mappings = SignalMappings(mappings={})
|
|
486
|
+
mocker.patch("uda_xarray.main._MAPPINGS", fake_mappings)
|
|
487
|
+
|
|
488
|
+
mock_signal = Mock()
|
|
489
|
+
mock_signal.data = np.array([1.0, 2.0])
|
|
490
|
+
mock_signal.shape = (2,)
|
|
491
|
+
dim1 = Mock(label="time")
|
|
492
|
+
dim1.data = np.array([0.0, 1.0])
|
|
493
|
+
mock_signal.dims = [dim1]
|
|
494
|
+
mock_signal.units = "A"
|
|
495
|
+
mock_signal.time = Mock(label="time", data=np.array([0.0, 1.0]))
|
|
496
|
+
mock_signal.errors = Mock(data=np.array([0.1, 0.1]))
|
|
497
|
+
|
|
498
|
+
mock_client = Mock()
|
|
499
|
+
mock_client.get.return_value = mock_signal
|
|
500
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
501
|
+
mocker.patch(
|
|
502
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
503
|
+
return_value="Signal",
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
ds = xr.open_dataset("uda://SOME_UNKNOWN_SIGNAL:30421", engine="uda")
|
|
507
|
+
|
|
508
|
+
mock_client.get.assert_called_once_with("SOME_UNKNOWN_SIGNAL", 30421)
|
|
509
|
+
assert ds["data"].attrs["uda_name"] == "SOME_UNKNOWN_SIGNAL"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""UDA backend for xarray."""
|
|
@@ -10,6 +10,10 @@ import xarray as xr
|
|
|
10
10
|
from mast.mast_client import ListType
|
|
11
11
|
from xarray.backends import BackendEntrypoint
|
|
12
12
|
|
|
13
|
+
from uda_xarray.mappings import SignalMappings
|
|
14
|
+
|
|
15
|
+
_MAPPINGS = SignalMappings.from_file()
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
# Set up UDA environment variables with defaults if not already set. This is required for
|
|
15
19
|
# the pyuda client to work correctly outside of Freia.
|
|
@@ -66,6 +70,8 @@ class UDABackendEntrypoint(BackendEntrypoint):
|
|
|
66
70
|
name = name.replace("uda://", "")
|
|
67
71
|
shot = int(shot)
|
|
68
72
|
|
|
73
|
+
name = _MAPPINGS.resolve(name, shot) or name
|
|
74
|
+
|
|
69
75
|
client = pyuda.Client()
|
|
70
76
|
signal_type = self._get_signal_type(client, name, shot)
|
|
71
77
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Signal name mappings for the UDA xarray backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.resources
|
|
6
|
+
import json
|
|
7
|
+
from typing import Iterator
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SignalRange(BaseModel):
|
|
13
|
+
"""A single shot-range entry mapping a canonical signal name to the name
|
|
14
|
+
used in the data store for that range of shots."""
|
|
15
|
+
|
|
16
|
+
shot_min: int
|
|
17
|
+
shot_max: int
|
|
18
|
+
name: str
|
|
19
|
+
|
|
20
|
+
def contains(self, shot: int) -> bool:
|
|
21
|
+
"""Return True if *shot* falls within this range (inclusive)."""
|
|
22
|
+
return self.shot_min <= shot <= self.shot_max
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SignalMappings(BaseModel):
|
|
26
|
+
"""Complete signal-alias mapping table loaded from mast.json.
|
|
27
|
+
|
|
28
|
+
The mapping is a dict whose keys are the canonical (most recent) signal
|
|
29
|
+
names and whose values are lists of :class:`SignalRange` entries ordered
|
|
30
|
+
from newest to oldest.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
mappings: dict[str, list[SignalRange]]
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_file(cls, path: str | None = None) -> "SignalMappings":
|
|
37
|
+
"""Load mappings from *path*.
|
|
38
|
+
|
|
39
|
+
When *path* is ``None`` all bundled ``*.json`` mapping files are
|
|
40
|
+
loaded and merged into a single :class:`SignalMappings` instance.
|
|
41
|
+
"""
|
|
42
|
+
if path is None:
|
|
43
|
+
merged: dict[str, list] = {}
|
|
44
|
+
pkg = importlib.resources.files("uda_xarray")
|
|
45
|
+
for resource in pkg.iterdir():
|
|
46
|
+
if resource.name.endswith(".json"):
|
|
47
|
+
with importlib.resources.as_file(resource) as p:
|
|
48
|
+
for k, v in json.loads(p.read_text(encoding="utf-8")).items():
|
|
49
|
+
if k in merged:
|
|
50
|
+
merged[k].extend(v)
|
|
51
|
+
else:
|
|
52
|
+
merged[k] = list(v)
|
|
53
|
+
raw = merged
|
|
54
|
+
else:
|
|
55
|
+
with open(path, encoding="utf-8") as f:
|
|
56
|
+
raw = json.load(f)
|
|
57
|
+
|
|
58
|
+
return cls(mappings={k: [SignalRange(**r) for r in v] for k, v in raw.items()})
|
|
59
|
+
|
|
60
|
+
def resolve(self, signal: str, shot: int) -> str | None:
|
|
61
|
+
"""Return the stored name for *signal* at the given *shot* number.
|
|
62
|
+
|
|
63
|
+
Returns *None* if *signal* is not in the mapping or no range covers
|
|
64
|
+
the shot.
|
|
65
|
+
"""
|
|
66
|
+
ranges = self.mappings.get(signal)
|
|
67
|
+
if ranges is None:
|
|
68
|
+
return None
|
|
69
|
+
for r in ranges:
|
|
70
|
+
if r.contains(shot):
|
|
71
|
+
return r.name
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def __iter__(self) -> Iterator[tuple[str, list[SignalRange]]]:
|
|
75
|
+
return iter(self.mappings.items())
|
|
76
|
+
|
|
77
|
+
def __len__(self) -> int:
|
|
78
|
+
return len(self.mappings)
|