Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Documentation
on:
push:
branches:
- master
- main
permissions:
contents: read
pages: write
id-token: write
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/configure-pages@v5
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: pip install zensical mkdocs-material mkdocstrings-python
- run: zensical build --clean
- uses: actions/upload-pages-artifact@v4
with:
path: site
- uses: actions/deploy-pages@v4
id: deployment
9 changes: 5 additions & 4 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest"]
python: ["3.10", "3.11", "3.12", "3.13"]
python: ["3.11", "3.12", "3.13"]
include:
- os: "macos-latest"
python: "3.10"
python: "3.11"
defaults:
run:
shell: "bash -eo pipefail {0}"

steps:
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v2"
- uses: "actions/checkout@v4"
- uses: "actions/setup-python@v5"
with:
python-version: "${{ matrix.python }}"
- uses: "actions/cache@v3"
Expand All @@ -48,6 +48,7 @@ jobs:
key: "test-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements/*') }}"
- run: "python --version"
- run: "python -mpip install -U pip"
- run: "python -mpip install -U pip setuptools>=72 wheel"
- run: "python -mpip --version"
- run: "python -mpip install -r requirements/requirements-dev.txt"
- run: "python -mpip install ./"
Expand Down
69 changes: 69 additions & 0 deletions docs/2d_bathymetry.html

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions docs/2d_free_surface.html

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions docs/2d_timeseries.html

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions docs/3d_bump_scatter.html

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions docs/3d_bump_z.html

Large diffs are not rendered by default.

119 changes: 119 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# API Reference

## Opening a SELAFIN file

```python
import xarray as xr

ds = xr.open_dataset(filename, engine="selafin", lang="en", lazy_loading=True)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `filename` | `str` or path | -- | Path to the `.slf` file |
| `engine` | `str` | -- | Must be `"selafin"` |
| `lang` | `str` | `"en"` | Variable language: `"en"` or `"fr"` |
| `lazy_loading` | `bool` | `True` | If `True`, data is read on demand |
| `disable_lock` | `bool` | `False` | Disable thread-safe file locking |
| `chunks` | `dict` or `None` | `None` | Dask chunk sizes (e.g. `{"time": -1, "node": 50}`) |

## Dataset structure

### Dimensions

| Dimension | Description | Present in |
|-----------|-------------|------------|
| `time` | Time steps | 2D and 3D |
| `node` | Mesh nodes | 2D and 3D |
| `plan` | Vertical layers (sigma levels) | 3D only |

### Coordinates

| Coordinate | Dimension | Type | Description |
|------------|-----------|------|-------------|
| `x` | `(node,)` | `float32` | Easting / x mesh coordinates |
| `y` | `(node,)` | `float32` | Northing / y mesh coordinates |
| `time` | `(time,)` | `datetime64` | Time stamps |

### Attributes

| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `title` | `str` | `"Converted with xarray-selafin"` | File title (up to 80 chars) |
| `language` | `str` | `"en"` | Language for variable detection |
| `float_size` | `int` | `4` | Precision: `4` (single) or `8` (double) |
| `endian` | `str` | `">"` | Byte order: `">"` (big) or `"<"` (little) |
| `params` | `tuple` | *(rebuilt)* | Integer parameter table (10 values) |
| `ipobo` | `ndarray` | *(rebuilt)* | Boundary node pointer array |
| `ikle2` | `ndarray` | **required** | 2D connectivity, shape `(n_elements, 3)`, **1-indexed** |
| `ikle3` | `ndarray` | *(rebuilt)* | 3D connectivity, shape `(n_prisms, 6)`, 1-indexed |
| `variables` | `dict` | *(auto-detected)* | Maps short names to `(long_name, unit)` tuples |
| `date_start` | `tuple` | *(from first time)* | `(year, month, day, hour, minute, second)` |

---

## Writing a SELAFIN file

```python
from xarray_selafin.xarray_backend import SelafinAccessor # registers .selafin accessor

ds.selafin.write("output.slf")
```

### Required attributes for export

!!! danger "Export will fail without these"
The two attributes below **must** be set correctly before calling `ds.selafin.write()`:

**`ikle2`** -- triangular connectivity table, **1-based indexing**:

```python
from scipy.spatial import Delaunay

ikle = Delaunay(np.vstack((x, y)).T).simplices # 0-based from scipy
ds.attrs["ikle2"] = ikle + 1 # SELAFIN requires 1-based
```

**`variables`** -- dictionary mapping each data variable name to a `(long_name, unit)` tuple:

```python
ds.attrs["variables"] = {
"WINDX": ("WINDX", "M/S"),
"WINDY": ("WINDY", "M/S"),
"PATM": ("PATM", "PASCAL"),
"TAIR": ("TAIR", "DEGREES C"),
}
```

Each key **must match** a variable name in the dataset. Without this dict, variable metadata in the output file will be empty or the write will crash.

---

## Variable definitions

Built-in TELEMAC variable definitions (French and English):

### 2D variables (partial list)

| ID | English name | Unit |
|----|-------------|------|
| `U` | VELOCITY U | M/S |
| `V` | VELOCITY V | M/S |
| `H` | WATER DEPTH | M |
| `S` | FREE SURFACE | M |
| `B` | BOTTOM | M |
| `M` | SCALAR VELOCITY | M/S |
| `F` | FROUDE NUMBER | -- |
| `TAU` | BOTTOM SHEAR STRESS | PASCAL |

### 3D variables (partial list)

| ID | English name | Unit |
|----|-------------|------|
| `Z` | ELEVATION Z | M |
| `U` | VELOCITY U | M/S |
| `V` | VELOCITY V | M/S |
| `W` | VELOCITY W | M/S |
| `NUX` | NUX FOR VELOCITY | M2/S |

Full tables: `xarray_selafin/data/Serafin_var2D.csv` and `Serafin_var3D.csv`.
190 changes: 190 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# Concrete examples with holoviews

## Plotting SELAFIN data

The unstructured triangular mesh is stored in `ds.attrs["ikle2"]`. We use `holoviews.TriMesh` to render filled triangulations directly:

```python
import holoviews as hv
import numpy as np
import pandas as pd
from bokeh.models import HoverTool

def plot_selafin(ds, var, geo=False, **kwargs):
if geo:
import geoviews as lib
else:
import holoviews as lib
simplices = pd.DataFrame(ds.attrs["ikle2"] - 1, columns=["v0", "v1", "v2"])
nodes = np.column_stack([ds.x.values, ds.y.values, ds[var].values])
nodes = lib.Points(nodes, vdims="z")
trimesh = lib.TriMesh((simplices, nodes), name=var)
hover = HoverTool(tooltips=[("x", "$x"), ("y", "$y"), (var, "@z")])
return trimesh.opts(
lib.opts.TriMesh(
filled=True, colorbar=True, edge_color="z",
inspection_policy="edges", node_size=0, tools=[hover],
**kwargs,
)
)
```

!!! warning "Use [Thalassa](https://github.com/ec-jrc/Thalassa) for big meshes"
The above function is just given as an example. We strongly recommend leveraging the use of `datashader` rendering.
For large datasets, consider using the [Thalassa](https://github.com/ec-jrc/Thalassa) library, which uses `datashader` for efficient big data visualisations.

---

## 2D: Tidal Flats

```python
import xarray as xr

ds = xr.open_dataset("tests/data/r2d_tidal_flats.slf", engine="selafin")
```

### Free surface

```python
plot_selafin(ds.isel(time=-1), "S", cmap="coolwarm", width=1000, height=200)
```

<iframe src="./2d_free_surface.html" width="100%" height="200" style="border:none;"></iframe>

### Bathymetry

```python
plot_selafin(ds.isel(time=-1), "B", cmap="viridis", width=1000, height=200)
```

<iframe src="./2d_bathymetry.html" width="100%" height="200" style="border:none;"></iframe>

### Time series at a node

```python
ds.isel(node=0).hvplot(x="time", y="S")
```

<iframe src="./2d_timeseries.html" width="100%" height="410" style="border:none;"></iframe>

---

## TOMAWAC: Waves (geo-referenced)

For geo-referenced data, pass `geo=True` to use `geoviews` with a tile background:

```python
ds = xr.open_dataset("tests/data/tom_manche.slf", engine="selafin", lang="fr")
plot_selafin(ds.isel(time=-1), "HAUTEUR_HM0", geo=True, cmap="rainbow4", width=800, height=800)
```

<iframe src="./tomawac_hm0.html" width="100%" height="710" style="border:none;"></iframe>

---

## 3D: Bump (hydraulic jump)

3D data is not natively rendered as a volume with hvplot. You can view **horizontal sections** by indexing on the `plan` dimension:

```python
ds = xr.open_dataset("tests/data/r3d_bump.slf", engine="selafin")
plot_selafin(ds.isel(time=-1, plan=-1), "Z", cmap="coolwarm", width=1000, height=200)
```

<iframe src="./3d_bump_z.html" width="100%" height="310" style="border:none;"></iframe>

You can also use `hvplot.scatter` for **vertical cross-sections**:

```python
ds.isel(time=-1).hvplot.scatter(x="x", y="Z", c="U", cmap="coolwarm")
```

<iframe src="./3d_bump_scatter.html" width="100%" height="510" style="border:none;"></iframe>

---

## Creating a SELAFIN file from scratch

To write a SELAFIN file you need two critical attributes on the dataset:

1. **`ikle2`** -- the triangular connectivity table, **1-indexed**
2. **`variables`** -- a dict mapping variable short names to `(long_name, unit)` tuples

```python
import numpy as np
import pandas as pd
import xarray as xr
from scipy.spatial import Delaunay

# Create a mesh
xx = np.arange(10)
yy = np.arange(10)
x, y = np.meshgrid(xx, yy)
x = x.ravel() + 0.3*np.random.rand(100)
y = y.ravel() + 0.3*np.random.rand(100)
ikle = Delaunay(np.vstack((x, y)).T).simplices # 0-indexed from scipy

# Create data
ds = xr.Dataset(
{
"WINDX": (("time", "node"), np.matlib.repmat(np.arange(0,10,0.1), 5, 1)),
"WINDY": (("time", "node"), np.matlib.repmat(np.arange(0,20,0.2), 5, 1)),
"PATM": (("time", "node"), np.full((5, 100), 101325.0)),
},
coords={
"x": ("node", x),
"y": ("node", y),
"time": pd.date_range("2024-01-01", periods=5, freq="6h"),
},
)

# --- CRITICAL: set attributes for SELAFIN export ---
ds.attrs["ikle2"] = ikle + 1 # convert to 1-based indexing

ds.attrs["variables"] = {
"WINDX": ("WINDX", "M/S"),
"WINDY": ("WINDY", "M/S"),
"PATM": ("PATM", "PASCAL"),
}
```

!!! warning "1-based connectivity"
SELAFIN uses **1-based indexing** for `ikle2`. If you get your triangulation from scipy or meshio (which are 0-based), you **must** add 1 before assigning.

!!! warning "Variables dictionary"
Without `ds.attrs["variables"]`, the export will either fail or produce a file with empty variable names. Each key must match a data variable name, and the value is a tuple of `(long_name, unit)`.

Now write:

```python
from xarray_selafin.xarray_backend import SelafinAccessor # registers .selafin accessor

ds.selafin.write("wind_forcing.slf")
```

## Visualize vector field

```python
import holoviews as hv
import numpy as np

x = ds.isel(time=-1).x.values
y = ds.isel(time=-1).y.values
u = ds.isel(time=-1)["WINDX"].values
v = ds.isel(time=-1)["WINDY"].values

angle = np.arctan2(v, u)
mag = np.sqrt(u**2 + v**2)

hv.VectorField((x, y, angle, mag)).opts(
magnitude="Magnitude",
color="Magnitude",
colorbar=True,
cmap="rainbow4",
tools=["hover"],
width = 1000,
height = 600
)
```

<iframe src="./scratch_wind.html" width="100%" height="610" style="border:none;"></iframe>
Loading
Loading