uda-xarray 0.1.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.1.0/LICENSE +21 -0
- uda_xarray-0.1.0/PKG-INFO +133 -0
- uda_xarray-0.1.0/README.md +113 -0
- uda_xarray-0.1.0/pyproject.toml +70 -0
- uda_xarray-0.1.0/setup.cfg +4 -0
- uda_xarray-0.1.0/tests/test_backend.py +150 -0
- uda_xarray-0.1.0/uda_xarray/main.py +164 -0
- uda_xarray-0.1.0/uda_xarray.egg-info/PKG-INFO +133 -0
- uda_xarray-0.1.0/uda_xarray.egg-info/SOURCES.txt +11 -0
- uda_xarray-0.1.0/uda_xarray.egg-info/dependency_links.txt +1 -0
- uda_xarray-0.1.0/uda_xarray.egg-info/entry_points.txt +2 -0
- uda_xarray-0.1.0/uda_xarray.egg-info/requires.txt +3 -0
- uda_xarray-0.1.0/uda_xarray.egg-info/top_level.txt +1 -0
uda_xarray-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Samuel Jackson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uda-xarray
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: UDA backend for xarray
|
|
5
|
+
Author-email: Samuel Jackson <samuel.jackson@ukaea.uk>
|
|
6
|
+
Maintainer-email: Samuel Jackson <samuel.jackson@ukaea.uk>
|
|
7
|
+
Project-URL: repository, https://github.com/samueljackson92/uda-xarray
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Requires-Python: <3.13,>=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: uda>=2.9.2
|
|
17
|
+
Requires-Dist: uda-mast
|
|
18
|
+
Requires-Dist: xarray>=2025.12.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# uda-xarray
|
|
22
|
+
|
|
23
|
+
An xarray backend for UDA (Universal Data Access) that enables seamless integration of UDA data sources with the xarray ecosystem.
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
|
|
27
|
+
uda-xarray provides a backend plugin for xarray that allows you to access UDA data sources using xarray's familiar API. It automatically handles the conversion of UDA signals to xarray DataArrays and Datasets, making it easy to work with UDA data in scientific Python workflows.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **xarray Integration**: Access UDA data using xarray's `open_dataset` function
|
|
32
|
+
- **Automatic Conversion**: Converts UDA signals to xarray DataArrays with proper coordinates and metadata
|
|
33
|
+
- **Error Handling**: Includes error data alongside signal data
|
|
34
|
+
- **URI-based Access**: Simple URI scheme (`uda://signal_name:shot`) for accessing data
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install uda-xarray
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or using `uv`:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv pip install uda-xarray
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
Open a UDA dataset using xarray:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import xarray as xr
|
|
54
|
+
|
|
55
|
+
# Open a UDA signal by name and shot number
|
|
56
|
+
ds = xr.open_dataset("uda://ip:30421", engine="uda")
|
|
57
|
+
|
|
58
|
+
# Access the data
|
|
59
|
+
data = ds["data"]
|
|
60
|
+
errors = ds["error"]
|
|
61
|
+
|
|
62
|
+
# The dataset includes time coordinates
|
|
63
|
+
time = ds["time"]
|
|
64
|
+
|
|
65
|
+
# Access metadata
|
|
66
|
+
units = ds["data"].attrs["units"]
|
|
67
|
+
signal_name = ds["data"].attrs["uda_name"]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The URI format is: `uda://<signal_name>:<shot_number>`
|
|
71
|
+
|
|
72
|
+
## Data Structure
|
|
73
|
+
|
|
74
|
+
When you open a UDA dataset, uda-xarray creates an xarray Dataset with:
|
|
75
|
+
|
|
76
|
+
- **data**: The signal data as a DataArray
|
|
77
|
+
- **error**: The error data as a DataArray
|
|
78
|
+
- **time**: Time coordinates (dimension)
|
|
79
|
+
- **attrs**: Metadata including units and UDA signal name
|
|
80
|
+
|
|
81
|
+
## Limitations
|
|
82
|
+
|
|
83
|
+
- Currently only supports 1D signals (2D signals will raise `NotImplementedError`)
|
|
84
|
+
- Requires a working UDA client connection
|
|
85
|
+
|
|
86
|
+
## Requirements
|
|
87
|
+
|
|
88
|
+
- Python >= 3.11, < 3.13
|
|
89
|
+
- uda >= 2.9.2
|
|
90
|
+
- xarray >= 2025.12.0
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
### Setup
|
|
95
|
+
|
|
96
|
+
Clone the repository and install development dependencies:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
git clone https://github.com/samueljackson92/uda-xarray.git
|
|
100
|
+
cd uda-xarray
|
|
101
|
+
uv sync
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Running Tests
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pytest tests/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Code Quality
|
|
111
|
+
|
|
112
|
+
The project uses ruff for linting and formatting, and pylint for additional checks:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Run ruff
|
|
116
|
+
uv run ruff check uda_xarray tests
|
|
117
|
+
uv run ruff format uda_xarray tests
|
|
118
|
+
|
|
119
|
+
# Run pylint
|
|
120
|
+
uv run pylint uda_xarray
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
|
|
125
|
+
Contributions are welcome! Please ensure:
|
|
126
|
+
|
|
127
|
+
1. Tests pass for any new functionality
|
|
128
|
+
2. Code follows the project's style guidelines (ruff and pylint)
|
|
129
|
+
3. Documentation is updated as needed
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT License
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# uda-xarray
|
|
2
|
+
|
|
3
|
+
An xarray backend for UDA (Universal Data Access) that enables seamless integration of UDA data sources with the xarray ecosystem.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
uda-xarray provides a backend plugin for xarray that allows you to access UDA data sources using xarray's familiar API. It automatically handles the conversion of UDA signals to xarray DataArrays and Datasets, making it easy to work with UDA data in scientific Python workflows.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **xarray Integration**: Access UDA data using xarray's `open_dataset` function
|
|
12
|
+
- **Automatic Conversion**: Converts UDA signals to xarray DataArrays with proper coordinates and metadata
|
|
13
|
+
- **Error Handling**: Includes error data alongside signal data
|
|
14
|
+
- **URI-based Access**: Simple URI scheme (`uda://signal_name:shot`) for accessing data
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install uda-xarray
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or using `uv`:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv pip install uda-xarray
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
Open a UDA dataset using xarray:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import xarray as xr
|
|
34
|
+
|
|
35
|
+
# Open a UDA signal by name and shot number
|
|
36
|
+
ds = xr.open_dataset("uda://ip:30421", engine="uda")
|
|
37
|
+
|
|
38
|
+
# Access the data
|
|
39
|
+
data = ds["data"]
|
|
40
|
+
errors = ds["error"]
|
|
41
|
+
|
|
42
|
+
# The dataset includes time coordinates
|
|
43
|
+
time = ds["time"]
|
|
44
|
+
|
|
45
|
+
# Access metadata
|
|
46
|
+
units = ds["data"].attrs["units"]
|
|
47
|
+
signal_name = ds["data"].attrs["uda_name"]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The URI format is: `uda://<signal_name>:<shot_number>`
|
|
51
|
+
|
|
52
|
+
## Data Structure
|
|
53
|
+
|
|
54
|
+
When you open a UDA dataset, uda-xarray creates an xarray Dataset with:
|
|
55
|
+
|
|
56
|
+
- **data**: The signal data as a DataArray
|
|
57
|
+
- **error**: The error data as a DataArray
|
|
58
|
+
- **time**: Time coordinates (dimension)
|
|
59
|
+
- **attrs**: Metadata including units and UDA signal name
|
|
60
|
+
|
|
61
|
+
## Limitations
|
|
62
|
+
|
|
63
|
+
- Currently only supports 1D signals (2D signals will raise `NotImplementedError`)
|
|
64
|
+
- Requires a working UDA client connection
|
|
65
|
+
|
|
66
|
+
## Requirements
|
|
67
|
+
|
|
68
|
+
- Python >= 3.11, < 3.13
|
|
69
|
+
- uda >= 2.9.2
|
|
70
|
+
- xarray >= 2025.12.0
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
### Setup
|
|
75
|
+
|
|
76
|
+
Clone the repository and install development dependencies:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git clone https://github.com/samueljackson92/uda-xarray.git
|
|
80
|
+
cd uda-xarray
|
|
81
|
+
uv sync
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Running Tests
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pytest tests/
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Code Quality
|
|
91
|
+
|
|
92
|
+
The project uses ruff for linting and formatting, and pylint for additional checks:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Run ruff
|
|
96
|
+
uv run ruff check uda_xarray tests
|
|
97
|
+
uv run ruff format uda_xarray tests
|
|
98
|
+
|
|
99
|
+
# Run pylint
|
|
100
|
+
uv run pylint uda_xarray
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Contributing
|
|
104
|
+
|
|
105
|
+
Contributions are welcome! Please ensure:
|
|
106
|
+
|
|
107
|
+
1. Tests pass for any new functionality
|
|
108
|
+
2. Code follows the project's style guidelines (ruff and pylint)
|
|
109
|
+
3. Documentation is updated as needed
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT License
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "uda-xarray"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "UDA backend for xarray"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11, <3.13"
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "Samuel Jackson", email = "samuel.jackson@ukaea.uk"},
|
|
9
|
+
]
|
|
10
|
+
maintainers = [
|
|
11
|
+
{name = "Samuel Jackson", email = "samuel.jackson@ukaea.uk"},
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
"uda>=2.9.2",
|
|
24
|
+
"uda-mast",
|
|
25
|
+
"xarray>=2025.12.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = ["uda_xarray"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
repository = "https://github.com/samueljackson92/uda-xarray"
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
exclude = []
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"bump-my-version>=1.2.5",
|
|
40
|
+
"pylint>=4.0.4",
|
|
41
|
+
"pytest>=8.4.1",
|
|
42
|
+
"pytest-mock>=3.15.1",
|
|
43
|
+
"ruff>=0.14.10",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.entry-points."xarray.backends"]
|
|
47
|
+
uda = "uda_xarray.main:UDABackendEntrypoint"
|
|
48
|
+
|
|
49
|
+
[tool.bumpversion]
|
|
50
|
+
current_version = "0.1.0"
|
|
51
|
+
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
52
|
+
serialize = ["{major}.{minor}.{patch}"]
|
|
53
|
+
search = "{current_version}"
|
|
54
|
+
replace = "{new_version}"
|
|
55
|
+
regex = false
|
|
56
|
+
ignore_missing_version = false
|
|
57
|
+
ignore_missing_files = false
|
|
58
|
+
tag = true
|
|
59
|
+
sign_tags = false
|
|
60
|
+
tag_name = "v{new_version}"
|
|
61
|
+
tag_message = "Bump version: {current_version} → {new_version}"
|
|
62
|
+
allow_dirty = false
|
|
63
|
+
commit = true
|
|
64
|
+
message = "Bump version: {current_version} → {new_version}"
|
|
65
|
+
commit_args = ""
|
|
66
|
+
|
|
67
|
+
[[tool.bumpversion.files]]
|
|
68
|
+
filename = "pyproject.toml"
|
|
69
|
+
search = 'version = "{current_version}"'
|
|
70
|
+
replace = 'version = "{new_version}"'
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import xarray as xr
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pyuda
|
|
4
|
+
from unittest.mock import Mock
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_open_uda_dataset(mocker):
|
|
8
|
+
# Create mock signal object
|
|
9
|
+
mock_signal = Mock()
|
|
10
|
+
mock_signal.data = np.array([1.0, 2.0, 3.0])
|
|
11
|
+
mock_signal.shape = (3,)
|
|
12
|
+
dim1 = Mock(label="time")
|
|
13
|
+
dim1.data = np.array([0.0, 1.0, 2.0])
|
|
14
|
+
mock_signal.dims = [dim1]
|
|
15
|
+
|
|
16
|
+
mock_signal.units = "A"
|
|
17
|
+
mock_signal.time = Mock()
|
|
18
|
+
mock_signal.time.data = np.array([0.0, 1.0, 2.0])
|
|
19
|
+
mock_signal.time.label = "time"
|
|
20
|
+
mock_signal.errors = Mock()
|
|
21
|
+
mock_signal.errors.data = np.array([0.1, 0.1, 0.1])
|
|
22
|
+
|
|
23
|
+
# Mock the pyuda Client
|
|
24
|
+
mock_client = Mock()
|
|
25
|
+
mock_client.get.return_value = mock_signal
|
|
26
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
27
|
+
mocker.patch(
|
|
28
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
29
|
+
return_value="Signal",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
ds = xr.open_dataset("uda://ip:30421", engine="uda")
|
|
33
|
+
|
|
34
|
+
# Verify the client was called correctly
|
|
35
|
+
mock_client.get.assert_called_once_with("ip", 30421)
|
|
36
|
+
|
|
37
|
+
assert ds["data"].name == "data"
|
|
38
|
+
assert ds["data"].dims == ("time",)
|
|
39
|
+
assert "time" in ds.coords
|
|
40
|
+
|
|
41
|
+
assert ds["data"].shape == ds["time"].shape
|
|
42
|
+
assert "units" in ds["data"].attrs
|
|
43
|
+
assert ds["data"].attrs["uda_name"] == "ip"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_open_uda_dataset_2d(mocker):
|
|
47
|
+
# Create mock 2D signal object
|
|
48
|
+
mock_signal = Mock()
|
|
49
|
+
mock_signal.data = np.array([[1.0, 2.0], [3.0, 4.0]])
|
|
50
|
+
mock_signal.shape = (2, 2)
|
|
51
|
+
dim1 = Mock(label="time")
|
|
52
|
+
dim1.data = np.array([0.0, 1.0])
|
|
53
|
+
dim2 = Mock(label="channel")
|
|
54
|
+
dim2.data = np.array([0, 1])
|
|
55
|
+
mock_signal.dims = [dim1, dim2]
|
|
56
|
+
mock_signal.units = "eV"
|
|
57
|
+
mock_signal.errors = Mock()
|
|
58
|
+
mock_signal.errors.data = np.array([[0.1, 0.1], [0.1, 0.1]])
|
|
59
|
+
mock_signal.time = Mock()
|
|
60
|
+
mock_signal.time.data = np.array([0.0, 1.0])
|
|
61
|
+
mock_signal.time.label = "time"
|
|
62
|
+
|
|
63
|
+
# Mock the pyuda Client
|
|
64
|
+
mock_client = Mock()
|
|
65
|
+
mock_client.get.return_value = mock_signal
|
|
66
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
67
|
+
mocker.patch(
|
|
68
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
69
|
+
return_value="Signal",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
ds = xr.open_dataset("uda://AYE_TE:30421", engine="uda")
|
|
73
|
+
|
|
74
|
+
mock_client.get.assert_called_once_with("AYE_TE", 30421)
|
|
75
|
+
|
|
76
|
+
assert ds["data"].name == "data"
|
|
77
|
+
assert ds["data"].dims == ("channel", "time")
|
|
78
|
+
assert "time" in ds.coords
|
|
79
|
+
assert "channel" in ds.coords
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_open_uda_dataset_video(mocker):
|
|
83
|
+
mock_signal = Mock()
|
|
84
|
+
mock_signal.is_color = False
|
|
85
|
+
mock_signal.frame_times = np.array([0.0, 0.033, 0.066])
|
|
86
|
+
frame1 = Mock()
|
|
87
|
+
frame1.k = np.array([[10, 20], [30, 40]])
|
|
88
|
+
frame2 = Mock()
|
|
89
|
+
frame2.k = np.array([[15, 25], [35, 45]])
|
|
90
|
+
frame3 = Mock()
|
|
91
|
+
frame3.k = np.array([[20, 30], [40, 50]])
|
|
92
|
+
mock_signal.frames = [frame1, frame2, frame3]
|
|
93
|
+
mock_signal.height = 2
|
|
94
|
+
mock_signal.width = 2
|
|
95
|
+
mock_signal.duration = 0.066
|
|
96
|
+
mock_signal.num_frames = 3
|
|
97
|
+
mock_signal.name = "rba"
|
|
98
|
+
mock_signal.description = "Mock video signal"
|
|
99
|
+
mock_signal.units = "counts"
|
|
100
|
+
|
|
101
|
+
mock_client = Mock()
|
|
102
|
+
mock_client.get_images.return_value = mock_signal
|
|
103
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
104
|
+
mocker.patch(
|
|
105
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
106
|
+
return_value="Image",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
ds = xr.open_dataset("uda://rba:30421", engine="uda")
|
|
110
|
+
assert ds["data"].name == "data"
|
|
111
|
+
assert ds["data"].dims == ("time", "height", "width")
|
|
112
|
+
assert "time" in ds.coords
|
|
113
|
+
assert ds.sizes["time"] == 3
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_open_uda_dataset_invalid_signal(mocker):
|
|
117
|
+
# Mock the pyuda Client to raise an exception
|
|
118
|
+
mock_client = Mock()
|
|
119
|
+
mock_client.get.side_effect = pyuda.ServerException("Signal not found")
|
|
120
|
+
mocker.patch("pyuda.Client", return_value=mock_client)
|
|
121
|
+
mocker.patch(
|
|
122
|
+
"uda_xarray.main.UDABackendEntrypoint._get_signal_type",
|
|
123
|
+
return_value="Signal",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
xr.open_dataset("uda://invalid_signal:99999", engine="uda")
|
|
128
|
+
except RuntimeError as e:
|
|
129
|
+
assert "Could not open UDA dataset" in str(e)
|
|
130
|
+
else:
|
|
131
|
+
assert False, "Expected RuntimeError was not raised"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_open_uda_dataset_invalid_format():
|
|
135
|
+
try:
|
|
136
|
+
xr.open_dataset("invalid_format", engine="uda")
|
|
137
|
+
except ValueError as e:
|
|
138
|
+
assert (
|
|
139
|
+
"UDA dataset must be specified as uda://<signal_name>:<shot_number>"
|
|
140
|
+
in str(e)
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
assert False, "Expected ValueError was not raised"
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
xr.open_dataset("http://invalid_scheme:12345", engine="uda")
|
|
147
|
+
except ValueError as e:
|
|
148
|
+
assert "UDA dataset must start with the uda:// scheme" in str(e)
|
|
149
|
+
else:
|
|
150
|
+
assert False, "Expected ValueError was not raised"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Xarray UDA backend entrypoint."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pyuda
|
|
8
|
+
import xarray as xr
|
|
9
|
+
from mast.mast_client import ListType
|
|
10
|
+
from xarray.backends import BackendEntrypoint
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SignalType(str, Enum):
|
|
14
|
+
"""Enum for UDA signal types."""
|
|
15
|
+
|
|
16
|
+
SIGNAL = "Signal"
|
|
17
|
+
IMAGE = "Image"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Signal = pyuda.Signal
|
|
21
|
+
Video = pyuda.Video
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UDABackendEntrypoint(BackendEntrypoint):
|
|
25
|
+
"""Xarray UDA backend entrypoint."""
|
|
26
|
+
|
|
27
|
+
def open_dataset(
|
|
28
|
+
self,
|
|
29
|
+
filename_or_obj,
|
|
30
|
+
*,
|
|
31
|
+
drop_variables=None,
|
|
32
|
+
frame_number: Optional[int] = None, # noqa: F821
|
|
33
|
+
) -> xr.Dataset:
|
|
34
|
+
"""Open a UDA dataset given a signal name and shot number.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
filename_or_obj : str
|
|
39
|
+
UDA dataset specified as uda://<signal_name>:<shot_number>
|
|
40
|
+
drop_variables : list, optional
|
|
41
|
+
Variables to drop from the dataset (not used).
|
|
42
|
+
frame_number : int, optional
|
|
43
|
+
Frame number to extract from an image signal (if applicable).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if ":" not in filename_or_obj:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"UDA dataset must be specified as uda://<signal_name>:<shot_number>"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if "uda://" not in filename_or_obj:
|
|
52
|
+
raise ValueError("UDA dataset must start with the uda:// scheme")
|
|
53
|
+
|
|
54
|
+
name, shot = filename_or_obj.rsplit(":", maxsplit=1)
|
|
55
|
+
name = name.replace("uda://", "")
|
|
56
|
+
shot = int(shot)
|
|
57
|
+
|
|
58
|
+
client = pyuda.Client()
|
|
59
|
+
signal_type = self._get_signal_type(client, name, shot)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
if signal_type == SignalType.SIGNAL:
|
|
63
|
+
signal = client.get(name, shot)
|
|
64
|
+
dataset = self._handle_signal(name, signal)
|
|
65
|
+
elif signal_type == SignalType.IMAGE:
|
|
66
|
+
kwargs = {}
|
|
67
|
+
if frame_number is not None:
|
|
68
|
+
kwargs["frame_number"] = frame_number
|
|
69
|
+
signal = client.get_images(name, shot, **kwargs)
|
|
70
|
+
dataset = self._handle_images(name, signal, frame_number=frame_number)
|
|
71
|
+
else:
|
|
72
|
+
raise NotImplementedError(f"Signal type {signal_type} not supported")
|
|
73
|
+
# pylint: disable=c-extension-no-member
|
|
74
|
+
except (pyuda.ServerException, pyuda.cpyuda.ClientException) as e:
|
|
75
|
+
raise RuntimeError(f"Could not open UDA dataset {filename_or_obj}") from e
|
|
76
|
+
|
|
77
|
+
return dataset
|
|
78
|
+
|
|
79
|
+
def _handle_signal(self, name: str, signal: Signal) -> xr.Dataset:
|
|
80
|
+
dim_data = {dim.label: dim.data for dim in signal.dims}
|
|
81
|
+
|
|
82
|
+
# Rename time dimension to just "time" if we can.
|
|
83
|
+
if signal.time.label in dim_data:
|
|
84
|
+
dim_data["time"] = dim_data.pop(signal.time.label)
|
|
85
|
+
|
|
86
|
+
item = xr.DataArray(
|
|
87
|
+
signal.data,
|
|
88
|
+
coords=dim_data,
|
|
89
|
+
attrs={"units": signal.units, "uda_name": name},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
error = xr.DataArray(
|
|
93
|
+
signal.errors.data,
|
|
94
|
+
coords=dim_data,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
dataset = xr.Dataset(data_vars={"data": item, "error": error})
|
|
98
|
+
return dataset
|
|
99
|
+
|
|
100
|
+
def _handle_images(
|
|
101
|
+
self, name: str, video: Video, frame_number: Optional[int] = None
|
|
102
|
+
) -> xr.Dataset:
|
|
103
|
+
attrs = {
|
|
104
|
+
name: getattr(video, name)
|
|
105
|
+
for name in dir(video)
|
|
106
|
+
if not name.startswith("_") and not callable(getattr(video, name))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
attrs.pop("frame_times")
|
|
110
|
+
attrs.pop("frames")
|
|
111
|
+
|
|
112
|
+
attrs["CLASS"] = "IMAGE"
|
|
113
|
+
attrs["IMAGE_VERSION"] = "1.2"
|
|
114
|
+
|
|
115
|
+
time = np.atleast_1d(video.frame_times)
|
|
116
|
+
if frame_number is not None:
|
|
117
|
+
time = time[frame_number : frame_number + 1]
|
|
118
|
+
coords = {"time": xr.DataArray(time, dims=["time"], attrs={"units": "s"})}
|
|
119
|
+
|
|
120
|
+
if video.is_color:
|
|
121
|
+
frames = [np.dstack((frame.r, frame.g, frame.b)) for frame in video.frames]
|
|
122
|
+
frames = np.stack(frames)
|
|
123
|
+
if frames.shape[1] != video.height:
|
|
124
|
+
frames = np.swapaxes(frames, 1, 2)
|
|
125
|
+
dim_names = ["time", "height", "width", "channel"]
|
|
126
|
+
|
|
127
|
+
attrs["IMAGE_SUBCLASS"] = "IMAGE_TRUECOLOR"
|
|
128
|
+
attrs["INTERLACE_MODE"] = "INTERLACE_PIXEL"
|
|
129
|
+
else:
|
|
130
|
+
frames = [frame.k for frame in video.frames]
|
|
131
|
+
frames = np.stack(frames)
|
|
132
|
+
frames = np.atleast_3d(frames)
|
|
133
|
+
if frames.shape[1] != video.height:
|
|
134
|
+
frames = np.swapaxes(frames, 1, 2)
|
|
135
|
+
dim_names = ["time", "height", "width"]
|
|
136
|
+
|
|
137
|
+
attrs["IMAGE_SUBCLASS"] = "IMAGE_INDEXED"
|
|
138
|
+
|
|
139
|
+
dataset = xr.DataArray(frames, dims=dim_names, coords=coords, attrs=attrs)
|
|
140
|
+
dataset = dataset.to_dataset(name="data")
|
|
141
|
+
dataset.attrs["uda_name"] = name
|
|
142
|
+
return dataset
|
|
143
|
+
|
|
144
|
+
def _get_signal_type(
|
|
145
|
+
self, client: pyuda.Client, name: str, shot: int
|
|
146
|
+
) -> SignalType:
|
|
147
|
+
sources = client.list(ListType.SOURCES, shot=shot)
|
|
148
|
+
source_types = {s.source_alias: s.type for s in sources}
|
|
149
|
+
|
|
150
|
+
if name in source_types and source_types[name] == "Image":
|
|
151
|
+
return SignalType.IMAGE
|
|
152
|
+
return SignalType.SIGNAL
|
|
153
|
+
|
|
154
|
+
def open_datatree(self, filename_or_obj, *, drop_variables=None):
|
|
155
|
+
raise NotImplementedError("UDA backend does not support open_datatree")
|
|
156
|
+
|
|
157
|
+
open_dataset_parameters = ["filename_or_obj", "drop_variables"]
|
|
158
|
+
|
|
159
|
+
def guess_can_open(self, filename_or_obj):
|
|
160
|
+
return filename_or_obj.startswith("uda://")
|
|
161
|
+
|
|
162
|
+
description = "Use UDA data in Xarray"
|
|
163
|
+
|
|
164
|
+
url = "https://github.com/samueljackson92/uda-xarray"
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uda-xarray
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: UDA backend for xarray
|
|
5
|
+
Author-email: Samuel Jackson <samuel.jackson@ukaea.uk>
|
|
6
|
+
Maintainer-email: Samuel Jackson <samuel.jackson@ukaea.uk>
|
|
7
|
+
Project-URL: repository, https://github.com/samueljackson92/uda-xarray
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Requires-Python: <3.13,>=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: uda>=2.9.2
|
|
17
|
+
Requires-Dist: uda-mast
|
|
18
|
+
Requires-Dist: xarray>=2025.12.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# uda-xarray
|
|
22
|
+
|
|
23
|
+
An xarray backend for UDA (Universal Data Access) that enables seamless integration of UDA data sources with the xarray ecosystem.
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
|
|
27
|
+
uda-xarray provides a backend plugin for xarray that allows you to access UDA data sources using xarray's familiar API. It automatically handles the conversion of UDA signals to xarray DataArrays and Datasets, making it easy to work with UDA data in scientific Python workflows.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **xarray Integration**: Access UDA data using xarray's `open_dataset` function
|
|
32
|
+
- **Automatic Conversion**: Converts UDA signals to xarray DataArrays with proper coordinates and metadata
|
|
33
|
+
- **Error Handling**: Includes error data alongside signal data
|
|
34
|
+
- **URI-based Access**: Simple URI scheme (`uda://signal_name:shot`) for accessing data
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install uda-xarray
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or using `uv`:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv pip install uda-xarray
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
Open a UDA dataset using xarray:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import xarray as xr
|
|
54
|
+
|
|
55
|
+
# Open a UDA signal by name and shot number
|
|
56
|
+
ds = xr.open_dataset("uda://ip:30421", engine="uda")
|
|
57
|
+
|
|
58
|
+
# Access the data
|
|
59
|
+
data = ds["data"]
|
|
60
|
+
errors = ds["error"]
|
|
61
|
+
|
|
62
|
+
# The dataset includes time coordinates
|
|
63
|
+
time = ds["time"]
|
|
64
|
+
|
|
65
|
+
# Access metadata
|
|
66
|
+
units = ds["data"].attrs["units"]
|
|
67
|
+
signal_name = ds["data"].attrs["uda_name"]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The URI format is: `uda://<signal_name>:<shot_number>`
|
|
71
|
+
|
|
72
|
+
## Data Structure
|
|
73
|
+
|
|
74
|
+
When you open a UDA dataset, uda-xarray creates an xarray Dataset with:
|
|
75
|
+
|
|
76
|
+
- **data**: The signal data as a DataArray
|
|
77
|
+
- **error**: The error data as a DataArray
|
|
78
|
+
- **time**: Time coordinates (dimension)
|
|
79
|
+
- **attrs**: Metadata including units and UDA signal name
|
|
80
|
+
|
|
81
|
+
## Limitations
|
|
82
|
+
|
|
83
|
+
- Currently only supports 1D signals (2D signals will raise `NotImplementedError`)
|
|
84
|
+
- Requires a working UDA client connection
|
|
85
|
+
|
|
86
|
+
## Requirements
|
|
87
|
+
|
|
88
|
+
- Python >= 3.11, < 3.13
|
|
89
|
+
- uda >= 2.9.2
|
|
90
|
+
- xarray >= 2025.12.0
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
### Setup
|
|
95
|
+
|
|
96
|
+
Clone the repository and install development dependencies:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
git clone https://github.com/samueljackson92/uda-xarray.git
|
|
100
|
+
cd uda-xarray
|
|
101
|
+
uv sync
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Running Tests
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pytest tests/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Code Quality
|
|
111
|
+
|
|
112
|
+
The project uses ruff for linting and formatting, and pylint for additional checks:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Run ruff
|
|
116
|
+
uv run ruff check uda_xarray tests
|
|
117
|
+
uv run ruff format uda_xarray tests
|
|
118
|
+
|
|
119
|
+
# Run pylint
|
|
120
|
+
uv run pylint uda_xarray
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
|
|
125
|
+
Contributions are welcome! Please ensure:
|
|
126
|
+
|
|
127
|
+
1. Tests pass for any new functionality
|
|
128
|
+
2. Code follows the project's style guidelines (ruff and pylint)
|
|
129
|
+
3. Documentation is updated as needed
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT License
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tests/test_backend.py
|
|
5
|
+
uda_xarray/main.py
|
|
6
|
+
uda_xarray.egg-info/PKG-INFO
|
|
7
|
+
uda_xarray.egg-info/SOURCES.txt
|
|
8
|
+
uda_xarray.egg-info/dependency_links.txt
|
|
9
|
+
uda_xarray.egg-info/entry_points.txt
|
|
10
|
+
uda_xarray.egg-info/requires.txt
|
|
11
|
+
uda_xarray.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uda_xarray
|