uda-xarray 0.2.3__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.2.3/uda_xarray.egg-info → uda_xarray-0.3.1}/PKG-INFO +5 -2
- {uda_xarray-0.2.3 → uda_xarray-0.3.1}/pyproject.toml +9 -3
- uda_xarray-0.3.1/tests/test_backend.py +534 -0
- uda_xarray-0.3.1/tests/test_mastu_mappings_live.py +72 -0
- uda_xarray-0.3.1/uda_xarray/__init__.py +1 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.1}/uda_xarray/main.py +6 -0
- uda_xarray-0.3.1/uda_xarray/mappings.py +96 -0
- uda_xarray-0.3.1/uda_xarray/mast.json +356 -0
- uda_xarray-0.3.1/uda_xarray/mastu.json +1451 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.1/uda_xarray.egg-info}/PKG-INFO +5 -2
- {uda_xarray-0.2.3 → uda_xarray-0.3.1}/uda_xarray.egg-info/SOURCES.txt +5 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.1}/uda_xarray.egg-info/requires.txt +1 -0
- uda_xarray-0.2.3/tests/test_backend.py +0 -295
- {uda_xarray-0.2.3 → uda_xarray-0.3.1}/LICENSE +0 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.1}/README.md +0 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.1}/setup.cfg +0 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.1}/uda_xarray.egg-info/dependency_links.txt +0 -0
- {uda_xarray-0.2.3 → uda_xarray-0.3.1}/uda_xarray.egg-info/entry_points.txt +0 -0
- {uda_xarray-0.2.3 → 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
|
+
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,13 +9,16 @@ 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
|
|
17
19
|
Requires-Dist: uda-mast
|
|
18
20
|
Requires-Dist: xarray
|
|
21
|
+
Requires-Dist: pydantic>=2.0
|
|
19
22
|
Dynamic: license-file
|
|
20
23
|
|
|
21
24
|
# uda-xarray
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "uda-xarray"
|
|
3
|
-
version = "0.
|
|
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
|
|
|
@@ -23,11 +25,15 @@ dependencies = [
|
|
|
23
25
|
"uda>=2.9.2",
|
|
24
26
|
"uda-mast",
|
|
25
27
|
"xarray",
|
|
28
|
+
"pydantic>=2.0",
|
|
26
29
|
]
|
|
27
30
|
|
|
28
31
|
[tool.setuptools]
|
|
29
32
|
packages = ["uda_xarray"]
|
|
30
33
|
|
|
34
|
+
[tool.setuptools.package-data]
|
|
35
|
+
uda_xarray = ["*.json"]
|
|
36
|
+
|
|
31
37
|
[project.urls]
|
|
32
38
|
repository = "https://github.com/samueljackson92/uda-xarray"
|
|
33
39
|
|
|
@@ -47,7 +53,7 @@ dev = [
|
|
|
47
53
|
uda = "uda_xarray.main:UDABackendEntrypoint"
|
|
48
54
|
|
|
49
55
|
[tool.bumpversion]
|
|
50
|
-
current_version = "0.
|
|
56
|
+
current_version = "0.3.1"
|
|
51
57
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
52
58
|
serialize = ["{major}.{minor}.{patch}"]
|
|
53
59
|
search = "{current_version}"
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""Tests for the UDA xarray backend."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pyuda
|
|
7
|
+
import pytest
|
|
8
|
+
import xarray as xr
|
|
9
|
+
|
|
10
|
+
from uda_xarray.mappings import SignalMappings, SignalRange
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_open_uda_dataset(mocker):
|
|
14
|
+
"""Test loading a 1D UDA signal as an xarray Dataset."""
|
|
15
|
+
# Create mock signal object
|
|
16
|
+
mock_signal = Mock()
|
|
17
|
+
mock_signal.data = np.array([1.0, 2.0, 3.0])
|
|
18
|
+
mock_signal.shape = (3,)
|
|
19
|
+
dim1 = Mock(label="time")
|
|
20
|
+
dim1.data = np.array([0.0, 1.0, 2.0])
|
|
21
|
+
mock_signal.dims = [dim1]
|
|
22
|
+
|
|
23
|
+
mock_signal.units = "A"
|
|
24
|
+
mock_signal.time = Mock()
|
|
25
|
+
mock_signal.time.data = np.array([0.0, 1.0, 2.0])
|
|
26
|
+
mock_signal.time.label = "time"
|
|
27
|
+
mock_signal.errors = Mock()
|
|
28
|
+
mock_signal.errors.data = np.array([0.1, 0.1, 0.1])
|
|
29
|
+
|
|
30
|
+
# Mock the pyuda Client
|
|
31
|
+
mock_client = Mock()
|
|
32
|
+
mock_client.get.return_value = mock_signal
|
|
33
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
34
|
+
mocker.patch(
|
|
35
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
36
|
+
return_value="Signal",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
ds = xr.open_dataset("uda://ip:30421", engine="uda")
|
|
40
|
+
|
|
41
|
+
# Verify the client was called correctly
|
|
42
|
+
mock_client.get.assert_called_once_with("ip", 30421)
|
|
43
|
+
|
|
44
|
+
assert ds["data"].name == "data"
|
|
45
|
+
assert ds["data"].dims == ("time",)
|
|
46
|
+
assert "time" in ds.coords
|
|
47
|
+
|
|
48
|
+
assert ds["data"].shape == ds["time"].shape
|
|
49
|
+
assert "units" in ds["data"].attrs
|
|
50
|
+
assert ds["data"].attrs["uda_name"] == "ip"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_open_uda_dataset_2d(mocker):
|
|
54
|
+
"""Test loading a 2D UDA signal as an xarray Dataset."""
|
|
55
|
+
# Create mock 2D signal object
|
|
56
|
+
mock_signal = Mock()
|
|
57
|
+
mock_signal.data = np.array([[1.0, 2.0], [3.0, 4.0]])
|
|
58
|
+
mock_signal.shape = (2, 2)
|
|
59
|
+
dim1 = Mock(label="time")
|
|
60
|
+
dim1.data = np.array([0.0, 1.0])
|
|
61
|
+
dim2 = Mock(label="channel")
|
|
62
|
+
dim2.data = np.array([0, 1])
|
|
63
|
+
mock_signal.dims = [dim1, dim2]
|
|
64
|
+
mock_signal.units = "eV"
|
|
65
|
+
mock_signal.errors = Mock()
|
|
66
|
+
mock_signal.errors.data = np.array([[0.1, 0.1], [0.1, 0.1]])
|
|
67
|
+
mock_signal.time = Mock()
|
|
68
|
+
mock_signal.time.data = np.array([0.0, 1.0])
|
|
69
|
+
mock_signal.time.label = "time"
|
|
70
|
+
|
|
71
|
+
# Mock the pyuda Client
|
|
72
|
+
mock_client = Mock()
|
|
73
|
+
mock_client.get.return_value = mock_signal
|
|
74
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
75
|
+
mocker.patch(
|
|
76
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
77
|
+
return_value="Signal",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
ds = xr.open_dataset("uda://AYE_TE:30421", engine="uda")
|
|
81
|
+
|
|
82
|
+
mock_client.get.assert_called_once_with("AYE_TE", 30421)
|
|
83
|
+
|
|
84
|
+
assert ds["data"].name == "data"
|
|
85
|
+
assert ds["data"].dims == ("time", "channel")
|
|
86
|
+
assert "time" in ds.coords
|
|
87
|
+
assert "channel" in ds.coords
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_open_uda_dataset_video(mocker):
|
|
91
|
+
"""Test loading a UDA video signal as an xarray Dataset."""
|
|
92
|
+
mock_signal = Mock()
|
|
93
|
+
mock_signal.is_color = False
|
|
94
|
+
mock_signal.frame_times = np.array([0.0, 0.033, 0.066])
|
|
95
|
+
frame1 = Mock()
|
|
96
|
+
frame1.k = np.array([[10, 20], [30, 40]])
|
|
97
|
+
frame2 = Mock()
|
|
98
|
+
frame2.k = np.array([[15, 25], [35, 45]])
|
|
99
|
+
frame3 = Mock()
|
|
100
|
+
frame3.k = np.array([[20, 30], [40, 50]])
|
|
101
|
+
mock_signal.frames = [frame1, frame2, frame3]
|
|
102
|
+
mock_signal.height = 2
|
|
103
|
+
mock_signal.width = 2
|
|
104
|
+
mock_signal.duration = 0.066
|
|
105
|
+
mock_signal.num_frames = 3
|
|
106
|
+
mock_signal.name = "rba"
|
|
107
|
+
mock_signal.description = "Mock video signal"
|
|
108
|
+
mock_signal.units = "counts"
|
|
109
|
+
|
|
110
|
+
mock_client = Mock()
|
|
111
|
+
mock_client.get_images.return_value = mock_signal
|
|
112
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
113
|
+
mocker.patch(
|
|
114
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
115
|
+
return_value="Image",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
ds = xr.open_dataset("uda://rba:30421", engine="uda")
|
|
119
|
+
assert ds["data"].name == "data"
|
|
120
|
+
assert ds["data"].dims == ("time", "height", "width")
|
|
121
|
+
assert "time" in ds.coords
|
|
122
|
+
assert ds.sizes["time"] == 3
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_open_uda_dataset_invalid_signal(mocker):
|
|
126
|
+
"""Test that an invalid UDA signal raises a RuntimeError."""
|
|
127
|
+
# Mock the pyuda Client to raise an exception
|
|
128
|
+
mock_client = Mock()
|
|
129
|
+
mock_client.get.side_effect = pyuda.ServerException("Signal not found")
|
|
130
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
131
|
+
mocker.patch(
|
|
132
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
133
|
+
return_value="Signal",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
xr.open_dataset("uda://invalid_signal:99999", engine="uda")
|
|
138
|
+
except RuntimeError as e:
|
|
139
|
+
assert "Could not open UDA dataset" in str(e)
|
|
140
|
+
else:
|
|
141
|
+
assert False, "Expected RuntimeError was not raised"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_open_uda_dataset_invalid_format():
|
|
145
|
+
"""Test that an invalid UDA URL format raises a ValueError."""
|
|
146
|
+
try:
|
|
147
|
+
xr.open_dataset("invalid_format", engine="uda")
|
|
148
|
+
except ValueError as e:
|
|
149
|
+
assert (
|
|
150
|
+
"UDA dataset must be specified as uda://<signal_name>:<shot_number>"
|
|
151
|
+
in str(e)
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
assert False, "Expected ValueError was not raised"
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
xr.open_dataset("http://invalid_scheme:12345", engine="uda")
|
|
158
|
+
except ValueError as e:
|
|
159
|
+
assert "UDA dataset must start with the uda:// scheme" in str(e)
|
|
160
|
+
else:
|
|
161
|
+
assert False, "Expected ValueError was not raised"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_open_datatree(mocker):
|
|
165
|
+
"""Test loading a 1D UDA signal as an xarray DataTree."""
|
|
166
|
+
# Create mock signal object
|
|
167
|
+
mock_signal = Mock()
|
|
168
|
+
mock_signal.data = np.array([1.0, 2.0, 3.0])
|
|
169
|
+
mock_signal.shape = (3,)
|
|
170
|
+
dim1 = Mock(label="time")
|
|
171
|
+
dim1.data = np.array([0.0, 1.0, 2.0])
|
|
172
|
+
mock_signal.dims = [dim1]
|
|
173
|
+
|
|
174
|
+
mock_signal.units = "A"
|
|
175
|
+
mock_signal.time = Mock()
|
|
176
|
+
mock_signal.time.data = np.array([0.0, 1.0, 2.0])
|
|
177
|
+
mock_signal.time.label = "time"
|
|
178
|
+
mock_signal.errors = Mock()
|
|
179
|
+
mock_signal.errors.data = np.array([0.1, 0.1, 0.1])
|
|
180
|
+
|
|
181
|
+
# Mock the pyuda Client
|
|
182
|
+
mock_client = Mock()
|
|
183
|
+
mock_client.get.return_value = mock_signal
|
|
184
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
185
|
+
mocker.patch(
|
|
186
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
187
|
+
return_value="Signal",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
dt = xr.open_datatree("uda://ip:30421", engine="uda")
|
|
191
|
+
|
|
192
|
+
# Verify the client was called correctly
|
|
193
|
+
mock_client.get.assert_called_once_with("ip", 30421)
|
|
194
|
+
|
|
195
|
+
# Verify it's a DataTree object
|
|
196
|
+
assert isinstance(dt, xr.DataTree)
|
|
197
|
+
|
|
198
|
+
# Access the dataset from the root node
|
|
199
|
+
ds = dt.to_dataset()
|
|
200
|
+
assert ds["data"].name == "data"
|
|
201
|
+
assert ds["data"].dims == ("time",)
|
|
202
|
+
assert "time" in ds.coords
|
|
203
|
+
assert ds["data"].shape == ds["time"].shape
|
|
204
|
+
assert "units" in ds["data"].attrs
|
|
205
|
+
assert ds["data"].attrs["uda_name"] == "ip"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_open_datatree_2d(mocker):
|
|
209
|
+
"""Test loading a 2D UDA signal as an xarray DataTree."""
|
|
210
|
+
# Create mock 2D signal object
|
|
211
|
+
mock_signal = Mock()
|
|
212
|
+
mock_signal.data = np.array([[1.0, 2.0], [3.0, 4.0]])
|
|
213
|
+
mock_signal.shape = (2, 2)
|
|
214
|
+
dim1 = Mock(label="time")
|
|
215
|
+
dim1.data = np.array([0.0, 1.0])
|
|
216
|
+
dim2 = Mock(label="channel")
|
|
217
|
+
dim2.data = np.array([0, 1])
|
|
218
|
+
mock_signal.dims = [dim1, dim2]
|
|
219
|
+
mock_signal.units = "eV"
|
|
220
|
+
mock_signal.errors = Mock()
|
|
221
|
+
mock_signal.errors.data = np.array([[0.1, 0.1], [0.1, 0.1]])
|
|
222
|
+
mock_signal.time = Mock()
|
|
223
|
+
mock_signal.time.data = np.array([0.0, 1.0])
|
|
224
|
+
mock_signal.time.label = "time"
|
|
225
|
+
|
|
226
|
+
# Mock the pyuda Client
|
|
227
|
+
mock_client = Mock()
|
|
228
|
+
mock_client.get.return_value = mock_signal
|
|
229
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
230
|
+
mocker.patch(
|
|
231
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
232
|
+
return_value="Signal",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
dt = xr.open_datatree("uda://AYE_TE:30421", engine="uda")
|
|
236
|
+
|
|
237
|
+
mock_client.get.assert_called_once_with("AYE_TE", 30421)
|
|
238
|
+
|
|
239
|
+
# Verify it's a DataTree object
|
|
240
|
+
assert isinstance(dt, xr.DataTree)
|
|
241
|
+
|
|
242
|
+
# Access the dataset from the root node
|
|
243
|
+
ds = dt.to_dataset()
|
|
244
|
+
assert ds["data"].name == "data"
|
|
245
|
+
assert ds["data"].dims == ("time", "channel")
|
|
246
|
+
assert "time" in ds.coords
|
|
247
|
+
assert "channel" in ds.coords
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_open_datatree_video(mocker):
|
|
251
|
+
"""Test loading a UDA video signal as an xarray DataTree."""
|
|
252
|
+
mock_signal = Mock()
|
|
253
|
+
mock_signal.is_color = False
|
|
254
|
+
mock_signal.frame_times = np.array([0.0, 0.033, 0.066])
|
|
255
|
+
frame1 = Mock()
|
|
256
|
+
frame1.k = np.array([[10, 20], [30, 40]])
|
|
257
|
+
frame2 = Mock()
|
|
258
|
+
frame2.k = np.array([[15, 25], [35, 45]])
|
|
259
|
+
frame3 = Mock()
|
|
260
|
+
frame3.k = np.array([[20, 30], [40, 50]])
|
|
261
|
+
mock_signal.frames = [frame1, frame2, frame3]
|
|
262
|
+
mock_signal.height = 2
|
|
263
|
+
mock_signal.width = 2
|
|
264
|
+
mock_signal.duration = 0.066
|
|
265
|
+
mock_signal.num_frames = 3
|
|
266
|
+
mock_signal.name = "rba"
|
|
267
|
+
mock_signal.description = "Mock video signal"
|
|
268
|
+
mock_signal.units = "counts"
|
|
269
|
+
|
|
270
|
+
mock_client = Mock()
|
|
271
|
+
mock_client.get_images.return_value = mock_signal
|
|
272
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
273
|
+
mocker.patch(
|
|
274
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
275
|
+
return_value="Image",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
dt = xr.open_datatree("uda://rba:30421", engine="uda")
|
|
279
|
+
|
|
280
|
+
# Verify it's a DataTree object
|
|
281
|
+
assert isinstance(dt, xr.DataTree)
|
|
282
|
+
|
|
283
|
+
# Access the dataset from the root node
|
|
284
|
+
ds = dt.to_dataset()
|
|
285
|
+
assert ds["data"].name == "data"
|
|
286
|
+
assert ds["data"].dims == ("time", "height", "width")
|
|
287
|
+
assert "time" in ds.coords
|
|
288
|
+
assert ds.sizes["time"] == 3
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@pytest.mark.live
|
|
292
|
+
def test_open_uda_dataset_video_frame():
|
|
293
|
+
"""Test loading a single video frame from a real UDA server."""
|
|
294
|
+
ds = xr.open_dataset(
|
|
295
|
+
"uda://rba:30421", engine="uda", drop_variables=None, frame_number=1
|
|
296
|
+
)
|
|
297
|
+
assert ds["data"].name == "data"
|
|
298
|
+
assert ds["data"].dims == ("time", "height", "width")
|
|
299
|
+
assert "time" in ds.coords
|
|
300
|
+
assert ds.sizes["time"] == 1
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@pytest.mark.live
|
|
304
|
+
def test_real_data_2d():
|
|
305
|
+
"""Test loading real 2D data from a live UDA server."""
|
|
306
|
+
ds = xr.open_dataset("uda://AYE_TE:30421", engine="uda")
|
|
307
|
+
assert ds["data"].name == "data"
|
|
308
|
+
assert ds["data"].dims == ("time", "radial index")
|
|
309
|
+
assert "time" in ds.coords
|
|
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"
|
|
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("/")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""UDA backend for xarray."""
|