uda-xarray 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {uda_xarray-0.3.0/uda_xarray.egg-info → uda_xarray-0.3.1}/PKG-INFO +4 -2
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/pyproject.toml +5 -3
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/tests/test_backend.py +29 -4
- uda_xarray-0.3.1/tests/test_mastu_mappings_live.py +72 -0
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/uda_xarray/mappings.py +20 -2
- uda_xarray-0.3.1/uda_xarray/mastu.json +1451 -0
- {uda_xarray-0.3.0 → uda_xarray-0.3.1/uda_xarray.egg-info}/PKG-INFO +4 -2
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/uda_xarray.egg-info/SOURCES.txt +1 -0
- uda_xarray-0.3.0/uda_xarray/mastu.json +0 -618
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/LICENSE +0 -0
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/README.md +0 -0
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/setup.cfg +0 -0
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/uda_xarray/__init__.py +0 -0
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/uda_xarray/main.py +0 -0
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/uda_xarray/mast.json +0 -0
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/uda_xarray.egg-info/dependency_links.txt +0 -0
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/uda_xarray.egg-info/entry_points.txt +0 -0
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/uda_xarray.egg-info/requires.txt +0 -0
- {uda_xarray-0.3.0 → uda_xarray-0.3.1}/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.
|
|
3
|
+
Version: 0.3.1
|
|
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>
|
|
@@ -9,8 +9,10 @@ Classifier: Development Status :: 4 - Beta
|
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
-
Requires-Python:
|
|
15
|
+
Requires-Python: >=3.9
|
|
14
16
|
Description-Content-Type: text/markdown
|
|
15
17
|
License-File: LICENSE
|
|
16
18
|
Requires-Dist: uda>=2.9.2
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "uda-xarray"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "UDA backend for xarray"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.9
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
7
|
authors = [
|
|
8
8
|
{name = "Samuel Jackson", email = "samuel.jackson@ukaea.uk"},
|
|
9
9
|
]
|
|
@@ -16,6 +16,8 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python :: 3",
|
|
17
17
|
"Programming Language :: Python :: 3.11",
|
|
18
18
|
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
19
21
|
"License :: OSI Approved :: MIT License",
|
|
20
22
|
]
|
|
21
23
|
|
|
@@ -51,7 +53,7 @@ dev = [
|
|
|
51
53
|
uda = "uda_xarray.main:UDABackendEntrypoint"
|
|
52
54
|
|
|
53
55
|
[tool.bumpversion]
|
|
54
|
-
current_version = "0.3.
|
|
56
|
+
current_version = "0.3.1"
|
|
55
57
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
56
58
|
serialize = ["{major}.{minor}.{patch}"]
|
|
57
59
|
search = "{current_version}"
|
|
@@ -288,7 +288,7 @@ def test_open_datatree_video(mocker):
|
|
|
288
288
|
assert ds.sizes["time"] == 3
|
|
289
289
|
|
|
290
290
|
|
|
291
|
-
@pytest.mark.
|
|
291
|
+
@pytest.mark.live
|
|
292
292
|
def test_open_uda_dataset_video_frame():
|
|
293
293
|
"""Test loading a single video frame from a real UDA server."""
|
|
294
294
|
ds = xr.open_dataset(
|
|
@@ -300,14 +300,14 @@ def test_open_uda_dataset_video_frame():
|
|
|
300
300
|
assert ds.sizes["time"] == 1
|
|
301
301
|
|
|
302
302
|
|
|
303
|
-
@pytest.mark.
|
|
303
|
+
@pytest.mark.live
|
|
304
304
|
def test_real_data_2d():
|
|
305
305
|
"""Test loading real 2D data from a live UDA server."""
|
|
306
306
|
ds = xr.open_dataset("uda://AYE_TE:30421", engine="uda")
|
|
307
307
|
assert ds["data"].name == "data"
|
|
308
|
-
assert ds["data"].dims == ("time", "
|
|
308
|
+
assert ds["data"].dims == ("time", "radial index")
|
|
309
309
|
assert "time" in ds.coords
|
|
310
|
-
assert "
|
|
310
|
+
assert "radial index" in ds.coords
|
|
311
311
|
|
|
312
312
|
|
|
313
313
|
def test_remapped_signal_uses_legacy_name(mocker):
|
|
@@ -507,3 +507,28 @@ def test_unmapped_signal_passes_through(mocker):
|
|
|
507
507
|
|
|
508
508
|
mock_client.get.assert_called_once_with("SOME_UNKNOWN_SIGNAL", 30421)
|
|
509
509
|
assert ds["data"].attrs["uda_name"] == "SOME_UNKNOWN_SIGNAL"
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def test_resolve_ignores_case_and_leading_slash():
|
|
513
|
+
mappings = SignalMappings(
|
|
514
|
+
mappings={
|
|
515
|
+
"/ESM/VLOOP/DYNAMIC": [
|
|
516
|
+
SignalRange(shot_min=1, shot_max=10, name="/ESM/VLOOP_DYNAMIC")
|
|
517
|
+
]
|
|
518
|
+
}
|
|
519
|
+
)
|
|
520
|
+
for name in ("/ESM/VLOOP/DYNAMIC", "esm/vloop/dynamic", "/esm/vloop/dynamic"):
|
|
521
|
+
assert mappings.resolve(name, 5) == "/ESM/VLOOP_DYNAMIC"
|
|
522
|
+
assert mappings.resolve("esm/vloop/dynamic", 11) is None
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def test_bundled_mastu_renames():
|
|
526
|
+
mappings = SignalMappings.from_file()
|
|
527
|
+
# Shot with only the old name in the catalogue.
|
|
528
|
+
assert mappings.resolve("esm/vloop/dynamic", 43477) == "/ESM/VLOOP_DYNAMIC"
|
|
529
|
+
assert mappings.resolve("esm/density/nebar", 43482) == "/ESM/NEBAR"
|
|
530
|
+
assert mappings.resolve("xdc/ai/cpu1/rogext_TF", 40240) == "/XDC/AI/CPU1/TF_CURRENT"
|
|
531
|
+
# Shots with the new name are not remapped.
|
|
532
|
+
assert mappings.resolve("esm/vloop/dynamic", 43478) is None
|
|
533
|
+
assert mappings.resolve("esm/vloop/dynamic", 52000) is None
|
|
534
|
+
assert mappings.resolve("xdc/ai/cpu1/rogext_TF", 40300) is None
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Live tests for the MAST-U signal mappings in ``mastu.json``.
|
|
2
|
+
|
|
3
|
+
These tests read real data, so they need a UDA server. ``conftest.py`` skips them when the
|
|
4
|
+
server cannot be reached (for example on CI). Run them from a machine with UDA access.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import xarray as xr
|
|
9
|
+
|
|
10
|
+
from uda_xarray.mappings import SignalMappings
|
|
11
|
+
|
|
12
|
+
# Renamed signals. The key is the current name. A shot with the current name needs no mapping.
|
|
13
|
+
RENAMED = (
|
|
14
|
+
"/ESM/VLOOP/DYNAMIC",
|
|
15
|
+
"/ESM/VLOOP/STATIC",
|
|
16
|
+
"/ESM/DENSITY/NEBAR",
|
|
17
|
+
"/ESM/DENSITY/NG",
|
|
18
|
+
"/ESM/DENSITY/FG",
|
|
19
|
+
"/ESM/POWER/POHM",
|
|
20
|
+
"/XDC/AI/CPU1/ROGEXT_TF",
|
|
21
|
+
)
|
|
22
|
+
# Shots that have the current name of every signal in RENAMED, and are not in a mapped range.
|
|
23
|
+
# 47000 has both the old and the current names. 52002 is after the old names end (shot 51257).
|
|
24
|
+
CURRENT_NAME_SHOTS = (47000, 52002)
|
|
25
|
+
# Number of shots to read for each signal. Each read takes a few seconds.
|
|
26
|
+
SAMPLES_PER_SIGNAL = 5
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
pytestmark = pytest.mark.live
|
|
30
|
+
|
|
31
|
+
MAPPINGS = SignalMappings.from_file()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _sample_shots(signal: str) -> list[int]:
|
|
35
|
+
"""Return shots spread over the mapped ranges, including the first and the last."""
|
|
36
|
+
ranges = sorted(MAPPINGS.mappings[signal], key=lambda r: r.shot_min)
|
|
37
|
+
shots = [ranges[0].shot_min, ranges[-1].shot_max]
|
|
38
|
+
step = max(1, len(ranges) // (SAMPLES_PER_SIGNAL - 1))
|
|
39
|
+
shots += [r.shot_min for r in ranges[::step]]
|
|
40
|
+
return sorted(set(shots))[: SAMPLES_PER_SIGNAL + 1]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _open(name: str, shot: int) -> xr.Dataset:
|
|
44
|
+
return xr.open_dataset(f"uda://{name}:{shot}", engine="uda")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cases() -> list[tuple[str, int]]:
|
|
48
|
+
return [(signal, shot) for signal in RENAMED for shot in _sample_shots(signal)]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.parametrize(("signal", "shot"), _cases())
|
|
52
|
+
def test_mapped_shot_returns_data_under_old_name(signal, shot):
|
|
53
|
+
"""The current name is read from the old name in a mapped shot."""
|
|
54
|
+
old_name = MAPPINGS.resolve(signal, shot)
|
|
55
|
+
assert old_name is not None
|
|
56
|
+
|
|
57
|
+
dataset = _open(signal.lstrip("/").lower(), shot)
|
|
58
|
+
|
|
59
|
+
assert dataset["data"].size > 0
|
|
60
|
+
assert dataset["data"].attrs["uda_name"] == old_name
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.parametrize("shot", CURRENT_NAME_SHOTS)
|
|
64
|
+
@pytest.mark.parametrize("signal", RENAMED)
|
|
65
|
+
def test_current_name_shot_is_not_remapped(signal, shot):
|
|
66
|
+
"""A shot with the current name is not remapped, and still returns data."""
|
|
67
|
+
assert MAPPINGS.resolve(signal, shot) is None
|
|
68
|
+
|
|
69
|
+
dataset = _open(signal.lstrip("/").lower(), shot)
|
|
70
|
+
|
|
71
|
+
assert dataset["data"].size > 0
|
|
72
|
+
assert dataset["data"].attrs["uda_name"].lstrip("/").upper() == signal.lstrip("/")
|
|
@@ -6,7 +6,15 @@ import importlib.resources
|
|
|
6
6
|
import json
|
|
7
7
|
from typing import Iterator
|
|
8
8
|
|
|
9
|
-
from pydantic import BaseModel
|
|
9
|
+
from pydantic import BaseModel, PrivateAttr
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _normalise(name: str) -> str:
|
|
13
|
+
"""Return *name* without a leading slash and in upper case.
|
|
14
|
+
|
|
15
|
+
UDA matches signal names without regard to case, and with or without the leading slash.
|
|
16
|
+
"""
|
|
17
|
+
return name.lstrip("/").upper()
|
|
10
18
|
|
|
11
19
|
|
|
12
20
|
class SignalRange(BaseModel):
|
|
@@ -31,6 +39,7 @@ class SignalMappings(BaseModel):
|
|
|
31
39
|
"""
|
|
32
40
|
|
|
33
41
|
mappings: dict[str, list[SignalRange]]
|
|
42
|
+
_index: dict[str, list[SignalRange]] | None = PrivateAttr(default=None)
|
|
34
43
|
|
|
35
44
|
@classmethod
|
|
36
45
|
def from_file(cls, path: str | None = None) -> "SignalMappings":
|
|
@@ -63,7 +72,7 @@ class SignalMappings(BaseModel):
|
|
|
63
72
|
Returns *None* if *signal* is not in the mapping or no range covers
|
|
64
73
|
the shot.
|
|
65
74
|
"""
|
|
66
|
-
ranges = self.
|
|
75
|
+
ranges = self._normalised().get(_normalise(signal))
|
|
67
76
|
if ranges is None:
|
|
68
77
|
return None
|
|
69
78
|
for r in ranges:
|
|
@@ -71,6 +80,15 @@ class SignalMappings(BaseModel):
|
|
|
71
80
|
return r.name
|
|
72
81
|
return None
|
|
73
82
|
|
|
83
|
+
def _normalised(self) -> dict[str, list[SignalRange]]:
|
|
84
|
+
"""Return the mappings keyed by normalised signal name (built once)."""
|
|
85
|
+
if self._index is None:
|
|
86
|
+
index: dict[str, list[SignalRange]] = {}
|
|
87
|
+
for key, ranges in self.mappings.items():
|
|
88
|
+
index.setdefault(_normalise(key), []).extend(ranges)
|
|
89
|
+
self._index = index
|
|
90
|
+
return self._index
|
|
91
|
+
|
|
74
92
|
def __iter__(self) -> Iterator[tuple[str, list[SignalRange]]]:
|
|
75
93
|
return iter(self.mappings.items())
|
|
76
94
|
|