Skip to content
Open
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
6 changes: 6 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,9 @@ UxDataArray
UxDataArray.remap.nearest_neighbor
UxDataArray.remap.inverse_distance_weighted
UxDataArray.remap.bilinear
UxDataArray.remap.to_rectilinear
UxDataArray.remap.to_structured
UxDataArray.remap.to_lonlat


UxDataset
Expand All @@ -490,6 +493,9 @@ UxDataset
UxDataset.remap.nearest_neighbor
UxDataset.remap.inverse_distance_weighted
UxDataset.remap.bilinear
UxDataset.remap.to_rectilinear
UxDataset.remap.to_structured
UxDataset.remap.to_lonlat


Mathematical Operators
Expand Down
6 changes: 6 additions & 0 deletions docs/generated/uxarray.UxDataArray.remap.to_rectilinear.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
uxarray.UxDataArray.remap.to_rectilinear
========================================

.. currentmodule:: uxarray

.. autoaccessormethod:: UxDataArray.remap.to_rectilinear
6 changes: 6 additions & 0 deletions docs/generated/uxarray.UxDataset.remap.to_rectilinear.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
uxarray.UxDataset.remap.to_rectilinear
======================================

.. currentmodule:: uxarray

.. autoaccessormethod:: UxDataset.remap.to_rectilinear
98 changes: 75 additions & 23 deletions docs/user-guide/remapping.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,7 @@
"cell_type": "markdown",
"id": "7eec39631eeeb6f8",
"metadata": {},
"source": [
"# Remapping\n",
"\n",
"Remapping (also known as **regridding**) is the process of transferring data defined on one spatial discretization to another. Whether you’re aligning model output to a different grid or comparing datasets on distinct grids, remapping ensures that values are accurately assigned or interpolated between coordinate systems.\n",
"\n",
"For a comprehensive overview of common remapping techniques, see the [Climate Data Guide: Regridding Overview](https://climatedataguide.ucar.edu/climate-tools/regridding-overview).\n",
"\n",
"UXarray currently supports three remapping techniques:\n",
"\n",
"- **Nearest Neighbor** \n",
"- **Inverse Distance Weighted**\n",
"- **Bilinear**\n",
"\n",
"```{admonition} Optional YAC backend\n",
":class: tip\n",
"\n",
"UXarray uses its native remapping backend by default. If [YAC](https://dkrz-sw.gitlab-pages.dkrz.de/yac/) is installed with its `yac.core` Python bindings, `.remap(...)`, `.remap.nearest_neighbor(...)`, and `.remap.bilinear(...)` can use `backend=\"yac\"` to route remapping through YAC.\n",
"\n",
"When `backend=\"yac\"`, the `yac_method` parameter selects the YAC interpolation method. Supported values are `nnn`, `average`, and `conservative`. `inverse_distance_weighted()` remains UXarray-only, and `bilinear(..., backend=\"yac\")` is a convenience wrapper for `yac_method=\"average\"`. For conservative face-centered remapping, use the generic `.remap(..., backend=\"yac\", yac_method=\"conservative\")` entrypoint.\n",
"\n",
"See the [YAC documentation](https://dkrz-sw.gitlab-pages.dkrz.de/yac/) and [YAC installation guide](https://dkrz-sw.gitlab-pages.dkrz.de/yac/d1/d9f/installing_yac.html) for build instructions, including enabling the Python bindings.\n",
"```\n"
]
"source": "# Remapping\n\nRemapping (also known as **regridding**) is the process of transferring data defined on one spatial discretization to another. Whether you’re aligning model output to a different grid or comparing datasets on distinct grids, remapping ensures that values are accurately assigned or interpolated between coordinate systems.\n\nFor a comprehensive overview of common remapping techniques, see the [Climate Data Guide: Regridding Overview](https://climatedataguide.ucar.edu/climate-tools/regridding-overview).\n\nUXarray currently supports three remapping techniques:\n\n- **Nearest Neighbor** \n- **Inverse Distance Weighted**\n- **Bilinear**\n\n```{admonition} Optional YAC backend\n:class: tip\n\nUXarray uses its native remapping backend by default. If [YAC](https://dkrz-sw.gitlab-pages.dkrz.de/yac/) is installed with its `yac.core` Python bindings, supported remapping methods can use `backend=\"yac\"`.\n\nYAC can be useful for larger remapping targets or workflows that already rely on YAC interpolation methods.\n```\n"
},
{
"cell_type": "code",
Expand Down Expand Up @@ -125,6 +103,12 @@
"uxds_120.remap"
]
},
{
"cell_type": "markdown",
"id": "fc57c2ee",
"source": "## Backend Choice\n\nUXarray remapping methods use the native UXarray backend by default. If YAC is installed, supported methods can be routed through YAC with `backend=\"yac\"`.\n\n| Method | Native backend | YAC backend |\n| --- | --- | --- |\n| `nearest_neighbor` | Yes | Yes (`nnn`) |\n| `inverse_distance_weighted` | Yes | No |\n| `bilinear` | Yes | Yes (`average`) |\n| `to_rectilinear` | Yes | Yes |\n",
"metadata": {}
},
{
"cell_type": "markdown",
"id": "969b42fd-bcd8-4781-b1b3-5dfddc42153f",
Expand Down Expand Up @@ -599,6 +583,74 @@
").cols(2).opts(fig_size=200)"
]
},
{
"cell_type": "markdown",
"id": "bd187355",
"metadata": {},
"source": "## Remapping to a Rectilinear Grid\n\nThe `remap.to_rectilinear()` accessor remaps unstructured data onto 1D longitude and latitude coordinates and returns a plain `xarray.DataArray` or `xarray.Dataset` with regular `lat` and `lon` axes.\n\nThis is useful when you want to work with UXarray data in tools that expect a structured longitude-latitude grid."
},
{
"cell_type": "markdown",
"id": "38f21388",
"metadata": {},
"source": "### Define the target longitude-latitude grid\n\nHere, we define a coarse rectilinear grid using 1D longitude and latitude coordinates."
},
{
"cell_type": "code",
"execution_count": null,
"id": "e28d1ccf",
"metadata": {},
"outputs": [],
"source": "lon = [-180, -120, -60, 0, 60, 120, 180]\nlat = [-90, -60, -30, 0, 30, 60, 90]\n\nprint(f\"Target lon points: {len(lon)}\")\nprint(f\"Target lat points: {len(lat)}\")"
},
{
"cell_type": "markdown",
"id": "90fe01b1",
"metadata": {},
"source": "### Remap the unstructured field\n\nCalling `to_rectilinear()` replaces the unstructured spatial dimension with the new `lat` and `lon` dimensions."
},
{
"cell_type": "code",
"execution_count": null,
"id": "0720932d",
"metadata": {},
"outputs": [],
"source": [
"rectilinear_bottom_depth = uxds_120[\"bottomDepth\"].remap.to_rectilinear(\n",
" lon=lon,\n",
" lat=lat,\n",
")\n",
"rectilinear_bottom_depth"
]
},
{
"cell_type": "markdown",
"id": "e5ab4445",
"metadata": {},
"source": "### Plot the source and rectilinear output\n\nFirst, plot the original unstructured field. Then plot the rectilinear result with xarray directly."
},
{
"cell_type": "code",
"execution_count": null,
"id": "25c2780c",
"metadata": {},
"outputs": [],
"source": "uxds_120[\"bottomDepth\"].plot(title=\"Bottom Depth (120km)\", **common_kwargs)"
},
{
"cell_type": "code",
"id": "f3a1f228",
"metadata": {},
"source": "rectilinear_bottom_depth.plot(\n figsize=(10, 4),\n cmap=cmocean.cm.deep,\n cbar_kwargs={\"label\": \"Bottom Depth\"},\n)",
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"id": "4fe74092",
"metadata": {},
"source": "### Alias and YAC backend\n\n`to_structured()` and `to_lonlat()` are aliases for `to_rectilinear()`. If YAC is installed, pass `backend=\"yac\"` and choose a `yac_method`."
},
{
"cell_type": "markdown",
"id": "938d5532-d93d-4abe-83db-098879b2cd93",
Expand Down
69 changes: 69 additions & 0 deletions test/test_remap.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np
import numpy.testing as nt
import pytest
import xarray as xr

import uxarray as ux
from uxarray.core.dataarray import UxDataArray
Expand Down Expand Up @@ -196,6 +197,74 @@ def test_dataset_remap_preserves_coords(gridpath, datasetpath):
assert "time" in ds_out.coords


def test_to_rectilinear_native_backend():
"""Rectilinear remap returns plain xarray output on lat/lon axes."""
grid = ux.Grid.from_structured(
lon=np.asarray([0.0, 90.0]),
lat=np.asarray([0.0, 45.0]),
)
da = UxDataArray(
np.asarray([1.0, 2.0, 3.0, 4.0]),
dims=["n_face"],
coords={
"n_face": [0, 1, 2, 3],
"face_lon": (
"n_face",
grid.face_lon.values,
{"standard_name": "longitude", "units": "degrees_east"},
),
"face_lat": (
"n_face",
grid.face_lat.values,
{"standard_name": "latitude", "units": "degrees_north"},
),
},
uxgrid=grid,
)
lon = xr.DataArray(
[0.0, 90.0],
dims=["lon"],
attrs={"axis": "X", "units": "degrees_east"},
)
lat = xr.DataArray(
[0.0, 45.0],
dims=["lat"],
attrs={"axis": "Y", "units": "degrees_north"},
)

out = da.remap.to_rectilinear(lon=lon, lat=lat)
out_structured = da.remap.to_structured(lon=lon, lat=lat)
out_lonlat = da.remap.to_lonlat(lon=lon, lat=lat)

assert isinstance(out, xr.DataArray)
assert out.dims == ("lat", "lon")
assert out.shape == (2, 2)
nt.assert_array_equal(out.values, np.asarray([[1.0, 2.0], [3.0, 4.0]]))
nt.assert_array_equal(out_structured.values, out.values)
nt.assert_array_equal(out_lonlat.values, out.values)
assert out["lon"].attrs["units"] == "degrees_east"
assert out["lat"].attrs["units"] == "degrees_north"


def test_reshape_to_rectilinear_accepts_xarray_dataset():
"""Rectilinear reshaping accepts an already-open xarray Dataset."""
from uxarray.remap.structured import (
_normalize_rectilinear_target,
_reshape_to_rectilinear,
)

spec = _normalize_rectilinear_target(
lon=np.asarray([0.0, 90.0]), lat=np.asarray([0.0, 45.0])
)
ds = xr.Dataset({"var": ("n_face", np.asarray([1.0, 2.0, 3.0, 4.0]))})

out = _reshape_to_rectilinear(ds, spec)

assert isinstance(out, xr.Dataset)
assert out["var"].dims == ("lat", "lon")
nt.assert_array_equal(out["var"].values, np.asarray([[1.0, 2.0], [3.0, 4.0]]))


# ------------------------------------------------------------
# Bilinear tests
# ------------------------------------------------------------
Expand Down
84 changes: 84 additions & 0 deletions test/test_remap_yac.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,87 @@ def test_yac_batched_remap_with_fractional_mask():

assert out.shape == da.shape
np.testing.assert_array_equal(out.values, da.values)


def test_yac_to_rectilinear_node_remap():
verts = np.array([(0.0, 0.0), (90.0, 0.0), (0.0, 45.0)])
grid = ux.open_grid(verts)
da = ux.UxDataArray(
np.asarray([1.0, 2.0, 3.0]),
dims=["n_node"],
coords={"n_node": [0, 1, 2]},
uxgrid=grid,
)

out = da.remap.to_rectilinear(
lon=np.asarray([0.0, 90.0]),
lat=np.asarray([0.0, 45.0]),
backend="yac",
yac_method="nnn",
yac_options={"n": 1},
)

assert out.dims == ("lat", "lon")
assert out.shape == (2, 2)
np.testing.assert_array_equal(out.values, np.asarray([[1.0, 3.0], [2.0, 3.0]]))


def test_yac_to_rectilinear_preserves_extra_dimensions():
verts = np.array([(0.0, 0.0), (90.0, 0.0), (0.0, 45.0)])
grid = ux.open_grid(verts)
da = ux.UxDataArray(
np.asarray([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]),
dims=["time", "n_node"],
coords={"time": [0, 1], "n_node": [0, 1, 2]},
uxgrid=grid,
)

out = da.remap.to_rectilinear(
lon=np.asarray([0.0, 90.0]),
lat=np.asarray([0.0, 45.0]),
backend="yac",
yac_method="nnn",
yac_options={"n": 1},
)

assert out.dims == ("time", "lat", "lon")
assert out.shape == (2, 2, 2)
np.testing.assert_array_equal(
out.values,
np.asarray([[[1.0, 3.0], [2.0, 3.0]], [[10.0, 30.0], [20.0, 30.0]]]),
)


def test_yac_to_rectilinear_preserves_valid_nonspatial_coords():
verts = np.array([(0.0, 0.0), (90.0, 0.0), (0.0, 45.0)])
grid = ux.open_grid(verts)
da = ux.UxDataArray(
np.asarray([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]),
dims=["time", "n_node"],
coords={
"time": [0, 1],
"n_node": [0, 1, 2],
"run": ("time", np.asarray([100, 101])),
"experiment": "demo",
"node_lon": (
"n_node",
np.asarray([0.0, 90.0, 0.0]),
{"standard_name": "longitude", "units": "degrees_east"},
),
},
uxgrid=grid,
)

out = da.remap.to_rectilinear(
lon=np.asarray([0.0, 90.0]),
lat=np.asarray([0.0, 45.0]),
backend="yac",
yac_method="nnn",
yac_options={"n": 1},
)

assert "run" in out.coords
assert "experiment" in out.coords
assert "node_lon" not in out.coords
np.testing.assert_array_equal(out["run"].values, np.asarray([100, 101]))
assert out["experiment"].item() == "demo"
Loading
Loading