sal-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.
- sal_xarray-0.1.0/LICENSE +21 -0
- sal_xarray-0.1.0/PKG-INFO +123 -0
- sal_xarray-0.1.0/README.md +103 -0
- sal_xarray-0.1.0/pyproject.toml +71 -0
- sal_xarray-0.1.0/sal_xarray/main.py +116 -0
- sal_xarray-0.1.0/sal_xarray.egg-info/PKG-INFO +123 -0
- sal_xarray-0.1.0/sal_xarray.egg-info/SOURCES.txt +11 -0
- sal_xarray-0.1.0/sal_xarray.egg-info/dependency_links.txt +1 -0
- sal_xarray-0.1.0/sal_xarray.egg-info/entry_points.txt +2 -0
- sal_xarray-0.1.0/sal_xarray.egg-info/requires.txt +3 -0
- sal_xarray-0.1.0/sal_xarray.egg-info/top_level.txt +1 -0
- sal_xarray-0.1.0/setup.cfg +4 -0
- sal_xarray-0.1.0/tests/test_backend.py +196 -0
sal_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,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sal-xarray
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SAL 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/sal-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: numpy<2
|
|
17
|
+
Requires-Dist: sal>=1.2.5
|
|
18
|
+
Requires-Dist: xarray>=2025.12.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# sal-xarray
|
|
22
|
+
|
|
23
|
+
An xarray backend for SAL (Simple Access Layer) that enables seamless integration of SAL data sources with the xarray ecosystem.
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
|
|
27
|
+
sal-xarray provides a backend plugin for xarray that allows you to access SAL data sources using xarray's familiar API. It automatically handles the conversion of SAL signals to xarray DataArrays and Datasets, making it easy to work with SAL data in scientific Python workflows.
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- **xarray Integration**: Access SAL data using xarray's `open_dataset` function
|
|
31
|
+
- **Automatic Conversion**: Converts SAL signals to xarray DataArrays with proper coordinates and metadata
|
|
32
|
+
- **Status Mask Handling**: Includes status mask data alongside signal data
|
|
33
|
+
- **URI-based Access**: Simple URI scheme (`sal://pulse/<shot_number>/<signal_name>`) for accessing data
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install sal-xarray
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or using `uv`:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv pip install sal-xarray
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
Open a SAL dataset using xarray:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import xarray as xr
|
|
53
|
+
|
|
54
|
+
# Open a SAL signal by name and shot number
|
|
55
|
+
ds = xr.open_dataset("sal://pulse/87737/ppf/signal/jetppf/magn/ipla", engine="sal")
|
|
56
|
+
|
|
57
|
+
# Access the data
|
|
58
|
+
data = ds["data"]
|
|
59
|
+
status = ds["status"]
|
|
60
|
+
|
|
61
|
+
# The dataset includes time coordinates
|
|
62
|
+
time = ds["time"]
|
|
63
|
+
|
|
64
|
+
# Access metadata
|
|
65
|
+
units = ds["data"].attrs["units"]
|
|
66
|
+
signal_name = ds["data"].attrs["sal_name"]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The URI format is: `sal://pulse/<shot_number>/<signal_name>`
|
|
70
|
+
|
|
71
|
+
## Data Structure
|
|
72
|
+
|
|
73
|
+
When you open a SAL dataset, sal-xarray creates an xarray Dataset with:
|
|
74
|
+
- **data**: The signal data as a DataArray
|
|
75
|
+
- **status**: The status data as a DataArray
|
|
76
|
+
- **time**: Time coordinates (dimension)
|
|
77
|
+
- **attrs**: Metadata including units and SAL signal name
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
### Setup
|
|
82
|
+
|
|
83
|
+
Clone the repository and install development dependencies:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
git clone https://github.com/samueljackson92/sal-xarray.git
|
|
87
|
+
cd sal-xarray
|
|
88
|
+
uv sync
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Running Tests
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uv run pytest tests/
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Code Quality
|
|
98
|
+
|
|
99
|
+
The project uses ruff for linting and formatting, and pylint for additional checks:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Run ruff
|
|
103
|
+
uv run ruff check sal_xarray tests
|
|
104
|
+
uv run ruff format sal_xarray tests
|
|
105
|
+
|
|
106
|
+
# Run pylint
|
|
107
|
+
uv run pylint sal_xarray
|
|
108
|
+
|
|
109
|
+
# Run ty
|
|
110
|
+
uv run ty check sal_xarray
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Contributing
|
|
114
|
+
|
|
115
|
+
Contributions are welcome! Please ensure:
|
|
116
|
+
|
|
117
|
+
1. Tests pass for any new functionality
|
|
118
|
+
2. Code follows the project's style guidelines (ruff and pylint)
|
|
119
|
+
3. Documentation is updated as needed
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT License
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# sal-xarray
|
|
2
|
+
|
|
3
|
+
An xarray backend for SAL (Simple Access Layer) that enables seamless integration of SAL data sources with the xarray ecosystem.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
sal-xarray provides a backend plugin for xarray that allows you to access SAL data sources using xarray's familiar API. It automatically handles the conversion of SAL signals to xarray DataArrays and Datasets, making it easy to work with SAL data in scientific Python workflows.
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **xarray Integration**: Access SAL data using xarray's `open_dataset` function
|
|
11
|
+
- **Automatic Conversion**: Converts SAL signals to xarray DataArrays with proper coordinates and metadata
|
|
12
|
+
- **Status Mask Handling**: Includes status mask data alongside signal data
|
|
13
|
+
- **URI-based Access**: Simple URI scheme (`sal://pulse/<shot_number>/<signal_name>`) for accessing data
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install sal-xarray
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or using `uv`:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv pip install sal-xarray
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Open a SAL dataset using xarray:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import xarray as xr
|
|
33
|
+
|
|
34
|
+
# Open a SAL signal by name and shot number
|
|
35
|
+
ds = xr.open_dataset("sal://pulse/87737/ppf/signal/jetppf/magn/ipla", engine="sal")
|
|
36
|
+
|
|
37
|
+
# Access the data
|
|
38
|
+
data = ds["data"]
|
|
39
|
+
status = ds["status"]
|
|
40
|
+
|
|
41
|
+
# The dataset includes time coordinates
|
|
42
|
+
time = ds["time"]
|
|
43
|
+
|
|
44
|
+
# Access metadata
|
|
45
|
+
units = ds["data"].attrs["units"]
|
|
46
|
+
signal_name = ds["data"].attrs["sal_name"]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The URI format is: `sal://pulse/<shot_number>/<signal_name>`
|
|
50
|
+
|
|
51
|
+
## Data Structure
|
|
52
|
+
|
|
53
|
+
When you open a SAL dataset, sal-xarray creates an xarray Dataset with:
|
|
54
|
+
- **data**: The signal data as a DataArray
|
|
55
|
+
- **status**: The status data as a DataArray
|
|
56
|
+
- **time**: Time coordinates (dimension)
|
|
57
|
+
- **attrs**: Metadata including units and SAL signal name
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
### Setup
|
|
62
|
+
|
|
63
|
+
Clone the repository and install development dependencies:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git clone https://github.com/samueljackson92/sal-xarray.git
|
|
67
|
+
cd sal-xarray
|
|
68
|
+
uv sync
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Running Tests
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv run pytest tests/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Code Quality
|
|
78
|
+
|
|
79
|
+
The project uses ruff for linting and formatting, and pylint for additional checks:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Run ruff
|
|
83
|
+
uv run ruff check sal_xarray tests
|
|
84
|
+
uv run ruff format sal_xarray tests
|
|
85
|
+
|
|
86
|
+
# Run pylint
|
|
87
|
+
uv run pylint sal_xarray
|
|
88
|
+
|
|
89
|
+
# Run ty
|
|
90
|
+
uv run ty check sal_xarray
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Contributing
|
|
94
|
+
|
|
95
|
+
Contributions are welcome! Please ensure:
|
|
96
|
+
|
|
97
|
+
1. Tests pass for any new functionality
|
|
98
|
+
2. Code follows the project's style guidelines (ruff and pylint)
|
|
99
|
+
3. Documentation is updated as needed
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT License
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sal-xarray"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "SAL 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
|
+
"numpy<2",
|
|
24
|
+
"sal>=1.2.5",
|
|
25
|
+
"xarray>=2025.12.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = ["sal_xarray"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
repository = "https://github.com/samueljackson92/sal-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
|
+
"ty>=0.0.12",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.entry-points."xarray.backends"]
|
|
48
|
+
sal = "sal_xarray.main:SALBackendEntrypoint"
|
|
49
|
+
|
|
50
|
+
[tool.bumpversion]
|
|
51
|
+
current_version = "0.1.0"
|
|
52
|
+
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
53
|
+
serialize = ["{major}.{minor}.{patch}"]
|
|
54
|
+
search = "{current_version}"
|
|
55
|
+
replace = "{new_version}"
|
|
56
|
+
regex = false
|
|
57
|
+
ignore_missing_version = false
|
|
58
|
+
ignore_missing_files = false
|
|
59
|
+
tag = true
|
|
60
|
+
sign_tags = false
|
|
61
|
+
tag_name = "v{new_version}"
|
|
62
|
+
tag_message = "Bump version: {current_version} → {new_version}"
|
|
63
|
+
allow_dirty = false
|
|
64
|
+
commit = true
|
|
65
|
+
message = "Bump version: {current_version} → {new_version}"
|
|
66
|
+
commit_args = ""
|
|
67
|
+
|
|
68
|
+
[[tool.bumpversion.files]]
|
|
69
|
+
filename = "pyproject.toml"
|
|
70
|
+
search = 'version = "{current_version}"'
|
|
71
|
+
replace = 'version = "{new_version}"'
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Xarray SAL backend entrypoint."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Iterable
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import xarray as xr
|
|
9
|
+
from sal.client import SALClient
|
|
10
|
+
|
|
11
|
+
from xarray.backends import BackendEntrypoint
|
|
12
|
+
from xarray.backends.common import ReadBuffer
|
|
13
|
+
from xarray.backends.common import AbstractDataStore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SALBackendEntrypoint(BackendEntrypoint):
|
|
17
|
+
"""Xarray SAL backend entrypoint."""
|
|
18
|
+
|
|
19
|
+
def open_dataset(
|
|
20
|
+
self,
|
|
21
|
+
filename_or_obj: str
|
|
22
|
+
| os.PathLike[Any]
|
|
23
|
+
| ReadBuffer
|
|
24
|
+
| bytes
|
|
25
|
+
| memoryview
|
|
26
|
+
| AbstractDataStore,
|
|
27
|
+
*,
|
|
28
|
+
drop_variables: str | Iterable[str] | None = None,
|
|
29
|
+
host: str = "https://sal.jetdata.eu",
|
|
30
|
+
) -> xr.Dataset:
|
|
31
|
+
"""Open a SAL dataset given a signal name and shot number.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
filename_or_obj : str
|
|
36
|
+
SAL dataset specified as sal://pulse/<shot_number>/<signal_name>
|
|
37
|
+
drop_variables : str or list of str, optional
|
|
38
|
+
Variables to drop from the dataset (not used).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
if not isinstance(filename_or_obj, str):
|
|
42
|
+
raise ValueError("SAL backend only supports string filenames.")
|
|
43
|
+
|
|
44
|
+
if "sal://" not in filename_or_obj:
|
|
45
|
+
raise ValueError("SAL dataset must start with the sal:// scheme")
|
|
46
|
+
|
|
47
|
+
name = filename_or_obj.replace("sal://", "")
|
|
48
|
+
match = re.search(r"sal://pulse/(\d+)/", filename_or_obj)
|
|
49
|
+
|
|
50
|
+
if not match:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"Invalid SAL URL format. Expected sal://pulse/<shot_number>/<signal_name>"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
shot = int(match.group(1))
|
|
56
|
+
name = "/" + name
|
|
57
|
+
|
|
58
|
+
client = SALClient(host=host)
|
|
59
|
+
try:
|
|
60
|
+
signal = client.get(name)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
raise RuntimeError(f"Failed to retrieve SAL signal '{name}': {e}") from e
|
|
63
|
+
|
|
64
|
+
data = self.parse_data(signal)
|
|
65
|
+
data.attrs["shot_id"] = shot
|
|
66
|
+
|
|
67
|
+
status = xr.DataArray(signal.mask.status, dims=data.dims, coords=data.coords)
|
|
68
|
+
status.attrs["description"] = signal.mask.description
|
|
69
|
+
status.attrs["flag_values"] = np.unique(signal.mask.status)
|
|
70
|
+
status.attrs["flag_meanings"] = signal.mask.key.tolist()
|
|
71
|
+
|
|
72
|
+
dataset = data.to_dataset(name="data")
|
|
73
|
+
dataset["status"] = status
|
|
74
|
+
return dataset
|
|
75
|
+
|
|
76
|
+
def parse_data(self, signal) -> xr.DataArray:
|
|
77
|
+
"""Parse a SAL LeafReport into an xarray DataArray."""
|
|
78
|
+
|
|
79
|
+
dims = {}
|
|
80
|
+
for index, item in enumerate(signal.dimensions):
|
|
81
|
+
name = f"dim_{index}"
|
|
82
|
+
if item.temporal:
|
|
83
|
+
name = "time"
|
|
84
|
+
dim = xr.DataArray(item.data, dims=[name], name=name)
|
|
85
|
+
dim.attrs["description"] = item.description
|
|
86
|
+
dim.attrs["units"] = item.units
|
|
87
|
+
|
|
88
|
+
dims[dim.name] = dim
|
|
89
|
+
|
|
90
|
+
data = xr.DataArray(
|
|
91
|
+
signal.data,
|
|
92
|
+
dims=[d.name for d in dims.values()],
|
|
93
|
+
coords={d.name: d for d in dims.values()},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
data.attrs["description"] = signal.description
|
|
97
|
+
data.attrs["units"] = signal.units
|
|
98
|
+
data.attrs["sal_name"] = name
|
|
99
|
+
|
|
100
|
+
return data
|
|
101
|
+
|
|
102
|
+
def open_datatree(self, filename_or_obj, *, drop_variables=None):
|
|
103
|
+
self.open_dataset(filename_or_obj, drop_variables=drop_variables)
|
|
104
|
+
|
|
105
|
+
def open_groups_as_dict(self, filename_or_obj, **kwargs):
|
|
106
|
+
"""Open groups as a dictionary (not supported for SAL backend)."""
|
|
107
|
+
raise NotImplementedError("SAL backend does not support opening groups")
|
|
108
|
+
|
|
109
|
+
open_dataset_parameters = ["filename_or_obj", "drop_variables"]
|
|
110
|
+
|
|
111
|
+
def guess_can_open(self, filename_or_obj):
|
|
112
|
+
return filename_or_obj.startswith("sal://")
|
|
113
|
+
|
|
114
|
+
description = "Use SAL data in Xarray"
|
|
115
|
+
|
|
116
|
+
url = "https://github.com/samueljackson92/sal-xarray"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sal-xarray
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SAL 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/sal-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: numpy<2
|
|
17
|
+
Requires-Dist: sal>=1.2.5
|
|
18
|
+
Requires-Dist: xarray>=2025.12.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# sal-xarray
|
|
22
|
+
|
|
23
|
+
An xarray backend for SAL (Simple Access Layer) that enables seamless integration of SAL data sources with the xarray ecosystem.
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
|
|
27
|
+
sal-xarray provides a backend plugin for xarray that allows you to access SAL data sources using xarray's familiar API. It automatically handles the conversion of SAL signals to xarray DataArrays and Datasets, making it easy to work with SAL data in scientific Python workflows.
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- **xarray Integration**: Access SAL data using xarray's `open_dataset` function
|
|
31
|
+
- **Automatic Conversion**: Converts SAL signals to xarray DataArrays with proper coordinates and metadata
|
|
32
|
+
- **Status Mask Handling**: Includes status mask data alongside signal data
|
|
33
|
+
- **URI-based Access**: Simple URI scheme (`sal://pulse/<shot_number>/<signal_name>`) for accessing data
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install sal-xarray
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or using `uv`:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv pip install sal-xarray
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
Open a SAL dataset using xarray:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import xarray as xr
|
|
53
|
+
|
|
54
|
+
# Open a SAL signal by name and shot number
|
|
55
|
+
ds = xr.open_dataset("sal://pulse/87737/ppf/signal/jetppf/magn/ipla", engine="sal")
|
|
56
|
+
|
|
57
|
+
# Access the data
|
|
58
|
+
data = ds["data"]
|
|
59
|
+
status = ds["status"]
|
|
60
|
+
|
|
61
|
+
# The dataset includes time coordinates
|
|
62
|
+
time = ds["time"]
|
|
63
|
+
|
|
64
|
+
# Access metadata
|
|
65
|
+
units = ds["data"].attrs["units"]
|
|
66
|
+
signal_name = ds["data"].attrs["sal_name"]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The URI format is: `sal://pulse/<shot_number>/<signal_name>`
|
|
70
|
+
|
|
71
|
+
## Data Structure
|
|
72
|
+
|
|
73
|
+
When you open a SAL dataset, sal-xarray creates an xarray Dataset with:
|
|
74
|
+
- **data**: The signal data as a DataArray
|
|
75
|
+
- **status**: The status data as a DataArray
|
|
76
|
+
- **time**: Time coordinates (dimension)
|
|
77
|
+
- **attrs**: Metadata including units and SAL signal name
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
### Setup
|
|
82
|
+
|
|
83
|
+
Clone the repository and install development dependencies:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
git clone https://github.com/samueljackson92/sal-xarray.git
|
|
87
|
+
cd sal-xarray
|
|
88
|
+
uv sync
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Running Tests
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uv run pytest tests/
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Code Quality
|
|
98
|
+
|
|
99
|
+
The project uses ruff for linting and formatting, and pylint for additional checks:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Run ruff
|
|
103
|
+
uv run ruff check sal_xarray tests
|
|
104
|
+
uv run ruff format sal_xarray tests
|
|
105
|
+
|
|
106
|
+
# Run pylint
|
|
107
|
+
uv run pylint sal_xarray
|
|
108
|
+
|
|
109
|
+
# Run ty
|
|
110
|
+
uv run ty check sal_xarray
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Contributing
|
|
114
|
+
|
|
115
|
+
Contributions are welcome! Please ensure:
|
|
116
|
+
|
|
117
|
+
1. Tests pass for any new functionality
|
|
118
|
+
2. Code follows the project's style guidelines (ruff and pylint)
|
|
119
|
+
3. Documentation is updated as needed
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT License
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
sal_xarray/main.py
|
|
5
|
+
sal_xarray.egg-info/PKG-INFO
|
|
6
|
+
sal_xarray.egg-info/SOURCES.txt
|
|
7
|
+
sal_xarray.egg-info/dependency_links.txt
|
|
8
|
+
sal_xarray.egg-info/entry_points.txt
|
|
9
|
+
sal_xarray.egg-info/requires.txt
|
|
10
|
+
sal_xarray.egg-info/top_level.txt
|
|
11
|
+
tests/test_backend.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sal_xarray
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Tests for SAL xarray backend."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock, patch
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
import xarray as xr
|
|
8
|
+
|
|
9
|
+
from sal_xarray.main import SALBackendEntrypoint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_signal():
|
|
14
|
+
"""Create a mock SAL signal object."""
|
|
15
|
+
signal = Mock()
|
|
16
|
+
|
|
17
|
+
# Mock dimension
|
|
18
|
+
dim = Mock()
|
|
19
|
+
dim.data = np.array([0.0, 1.0, 2.0, 3.0, 4.0])
|
|
20
|
+
dim.temporal = True
|
|
21
|
+
dim.description = "Time dimension"
|
|
22
|
+
dim.units = "s"
|
|
23
|
+
|
|
24
|
+
signal.dimensions = [dim]
|
|
25
|
+
signal.data = np.array([10.0, 20.0, 30.0, 40.0, 50.0])
|
|
26
|
+
signal.description = "Test signal"
|
|
27
|
+
signal.units = "V"
|
|
28
|
+
|
|
29
|
+
# Mock mask
|
|
30
|
+
mask = Mock()
|
|
31
|
+
mask.status = np.array([0, 0, 1, 0, 0])
|
|
32
|
+
mask.description = "Data quality mask"
|
|
33
|
+
mask.key = np.array(["good", "bad"])
|
|
34
|
+
signal.mask = mask
|
|
35
|
+
|
|
36
|
+
return signal
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def mock_sal_client(mock_signal):
|
|
41
|
+
"""Create a mock SAL client."""
|
|
42
|
+
with patch("sal_xarray.main.SALClient") as MockClient:
|
|
43
|
+
client_instance = Mock()
|
|
44
|
+
client_instance.get.return_value = mock_signal
|
|
45
|
+
MockClient.return_value = client_instance
|
|
46
|
+
yield MockClient
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestSALBackendEntrypoint:
|
|
50
|
+
"""Tests for SALBackendEntrypoint."""
|
|
51
|
+
|
|
52
|
+
def test_guess_can_open_valid(self):
|
|
53
|
+
"""Test that guess_can_open returns True for sal:// URLs."""
|
|
54
|
+
backend = SALBackendEntrypoint()
|
|
55
|
+
assert backend.guess_can_open("sal://pulse/12345/signal_name")
|
|
56
|
+
|
|
57
|
+
def test_guess_can_open_invalid(self):
|
|
58
|
+
"""Test that guess_can_open returns False for non-sal URLs."""
|
|
59
|
+
backend = SALBackendEntrypoint()
|
|
60
|
+
assert not backend.guess_can_open("http://example.com")
|
|
61
|
+
assert not backend.guess_can_open("file://data.nc")
|
|
62
|
+
|
|
63
|
+
def test_open_dataset_invalid_type(self):
|
|
64
|
+
"""Test that open_dataset raises error for non-string input."""
|
|
65
|
+
backend = SALBackendEntrypoint()
|
|
66
|
+
with pytest.raises(
|
|
67
|
+
ValueError, match="SAL backend only supports string filenames"
|
|
68
|
+
):
|
|
69
|
+
backend.open_dataset(123)
|
|
70
|
+
|
|
71
|
+
def test_open_dataset_missing_scheme(self):
|
|
72
|
+
"""Test that open_dataset raises error when sal:// scheme is missing."""
|
|
73
|
+
backend = SALBackendEntrypoint()
|
|
74
|
+
with pytest.raises(
|
|
75
|
+
ValueError, match="SAL dataset must start with the sal:// scheme"
|
|
76
|
+
):
|
|
77
|
+
backend.open_dataset("pulse/12345/signal_name")
|
|
78
|
+
|
|
79
|
+
def test_open_dataset_invalid_format(self):
|
|
80
|
+
"""Test that open_dataset raises error for invalid URL format."""
|
|
81
|
+
backend = SALBackendEntrypoint()
|
|
82
|
+
with pytest.raises(ValueError, match="Invalid SAL URL format"):
|
|
83
|
+
backend.open_dataset("sal://invalid/format")
|
|
84
|
+
|
|
85
|
+
def test_open_dataset_success(self, mock_sal_client, mock_signal):
|
|
86
|
+
"""Test successful dataset opening with mocked SAL client."""
|
|
87
|
+
backend = SALBackendEntrypoint()
|
|
88
|
+
ds = backend.open_dataset("sal://pulse/12345/signal_name")
|
|
89
|
+
|
|
90
|
+
# Check that dataset was created
|
|
91
|
+
assert isinstance(ds, xr.Dataset)
|
|
92
|
+
|
|
93
|
+
# Check that data variable exists
|
|
94
|
+
assert "data" in ds.data_vars
|
|
95
|
+
assert "status" in ds.data_vars
|
|
96
|
+
|
|
97
|
+
# Check dimensions
|
|
98
|
+
assert "time" in ds.dims
|
|
99
|
+
assert ds.sizes["time"] == 5
|
|
100
|
+
|
|
101
|
+
# Check data values
|
|
102
|
+
np.testing.assert_array_equal(ds["data"].values, mock_signal.data)
|
|
103
|
+
|
|
104
|
+
# Check attributes
|
|
105
|
+
assert ds["data"].attrs["description"] == "Test signal"
|
|
106
|
+
assert ds["data"].attrs["units"] == "V"
|
|
107
|
+
assert ds["data"].attrs["shot_id"] == 12345
|
|
108
|
+
|
|
109
|
+
# Check status variable
|
|
110
|
+
np.testing.assert_array_equal(ds["status"].values, mock_signal.mask.status)
|
|
111
|
+
assert ds["status"].attrs["description"] == "Data quality mask"
|
|
112
|
+
|
|
113
|
+
# Check that SALClient was called correctly
|
|
114
|
+
mock_sal_client.assert_called_once_with(host="https://sal.jetdata.eu")
|
|
115
|
+
mock_sal_client.return_value.get.assert_called_once()
|
|
116
|
+
|
|
117
|
+
def test_open_dataset_custom_host(self, mock_sal_client):
|
|
118
|
+
"""Test opening dataset with custom host."""
|
|
119
|
+
backend = SALBackendEntrypoint()
|
|
120
|
+
custom_host = "https://custom.sal.server"
|
|
121
|
+
backend.open_dataset("sal://pulse/12345/signal_name", host=custom_host)
|
|
122
|
+
|
|
123
|
+
mock_sal_client.assert_called_once_with(host=custom_host)
|
|
124
|
+
|
|
125
|
+
def test_open_dataset_with_multiple_dimensions(self, mock_sal_client):
|
|
126
|
+
"""Test opening dataset with multiple dimensions."""
|
|
127
|
+
# Create mock signal with two dimensions
|
|
128
|
+
signal = Mock()
|
|
129
|
+
|
|
130
|
+
dim0 = Mock()
|
|
131
|
+
dim0.data = np.array([0.0, 1.0, 2.0])
|
|
132
|
+
dim0.temporal = False
|
|
133
|
+
dim0.description = "Spatial dimension"
|
|
134
|
+
dim0.units = "m"
|
|
135
|
+
|
|
136
|
+
dim1 = Mock()
|
|
137
|
+
dim1.data = np.array([0.0, 1.0, 2.0, 3.0])
|
|
138
|
+
dim1.temporal = True
|
|
139
|
+
dim1.description = "Time dimension"
|
|
140
|
+
dim1.units = "s"
|
|
141
|
+
|
|
142
|
+
signal.dimensions = [dim0, dim1]
|
|
143
|
+
signal.data = np.random.rand(3, 4)
|
|
144
|
+
signal.description = "2D signal"
|
|
145
|
+
signal.units = "A"
|
|
146
|
+
|
|
147
|
+
mask = Mock()
|
|
148
|
+
mask.status = np.zeros((3, 4), dtype=int)
|
|
149
|
+
mask.description = "Mask"
|
|
150
|
+
mask.key = np.array(["good", "bad"])
|
|
151
|
+
signal.mask = mask
|
|
152
|
+
|
|
153
|
+
mock_sal_client.return_value.get.return_value = signal
|
|
154
|
+
|
|
155
|
+
backend = SALBackendEntrypoint()
|
|
156
|
+
ds = backend.open_dataset("sal://pulse/12345/signal_name")
|
|
157
|
+
|
|
158
|
+
# Check dimensions
|
|
159
|
+
assert "dim_0" in ds.dims
|
|
160
|
+
assert "time" in ds.dims
|
|
161
|
+
assert ds.sizes["dim_0"] == 3
|
|
162
|
+
assert ds.sizes["time"] == 4
|
|
163
|
+
|
|
164
|
+
# Check data shape
|
|
165
|
+
assert ds["data"].shape == (3, 4)
|
|
166
|
+
|
|
167
|
+
def test_open_dataset_client_error(self, mock_sal_client):
|
|
168
|
+
"""Test that open_dataset handles SAL client errors."""
|
|
169
|
+
mock_sal_client.return_value.get.side_effect = Exception("Connection failed")
|
|
170
|
+
|
|
171
|
+
backend = SALBackendEntrypoint()
|
|
172
|
+
with pytest.raises(RuntimeError, match="Failed to retrieve SAL signal"):
|
|
173
|
+
backend.open_dataset("sal://pulse/12345/signal_name")
|
|
174
|
+
|
|
175
|
+
def test_open_dataset_different_shot_numbers(self, mock_sal_client):
|
|
176
|
+
"""Test opening datasets with different shot numbers."""
|
|
177
|
+
backend = SALBackendEntrypoint()
|
|
178
|
+
|
|
179
|
+
# Test with different shot numbers
|
|
180
|
+
for shot_num in [1, 100, 99999]:
|
|
181
|
+
ds = backend.open_dataset(f"sal://pulse/{shot_num}/signal_name")
|
|
182
|
+
assert ds["data"].attrs["shot_id"] == shot_num
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class TestXarrayIntegration:
|
|
186
|
+
"""Test integration with xarray's open_dataset function."""
|
|
187
|
+
|
|
188
|
+
def test_xr_open_dataset_with_engine(self, mock_sal_client):
|
|
189
|
+
"""Test using xr.open_dataset with engine='sal'."""
|
|
190
|
+
# This test requires the backend to be registered
|
|
191
|
+
# In practice, this happens via entry_points in pyproject.toml
|
|
192
|
+
ds = xr.open_dataset("sal://pulse/12345/signal_name", engine="sal")
|
|
193
|
+
|
|
194
|
+
assert isinstance(ds, xr.Dataset)
|
|
195
|
+
assert "data" in ds.data_vars
|
|
196
|
+
assert "status" in ds.data_vars
|