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
2 changes: 2 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ UxDataArray
UxDataArray.remap.nearest_neighbor
UxDataArray.remap.inverse_distance_weighted
UxDataArray.remap.bilinear
UxDataArray.remap.to_rectilinear


UxDataset
Expand All @@ -477,6 +478,7 @@ UxDataset
UxDataset.remap.nearest_neighbor
UxDataset.remap.inverse_distance_weighted
UxDataset.remap.bilinear
UxDataset.remap.to_rectilinear


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
2 changes: 1 addition & 1 deletion docs/user-guide/remapping.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"\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",
"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. To remap onto a rectilinear latitude-longitude target and get regular xarray output, use `.remap.to_rectilinear(lon=..., lat=..., backend=\"yac\")`.\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"
Expand Down
65 changes: 65 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,70 @@ 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, backend="uxarray")

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]]))
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"
59 changes: 59 additions & 0 deletions uxarray/remap/accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __repr__(self) -> str:
+ "Supported methods:\n"
+ " • nearest_neighbor(destination_grid, remap_to='faces')\n"
+ " • inverse_distance_weighted(destination_grid, remap_to='faces', power=2, k=8)\n"
+ " • to_rectilinear(lon, lat, backend='yac')\n"
)

def __call__(
Expand Down Expand Up @@ -175,6 +176,64 @@ def inverse_distance_weighted(
self.ux_obj, destination_grid, remap_to, power, k
)

def to_rectilinear(
self,
lon,
lat,
backend: str = "yac",
yac_method: str | None = "nnn",
yac_options: dict | None = None,
**kwargs,
Comment thread
rajeeja marked this conversation as resolved.
):
"""
Remap onto a rectilinear longitude/latitude grid.

This convenience method targets 1-D longitude and latitude coordinate
arrays and returns a plain xarray object with ``lat`` and ``lon`` axes,
making the output suitable for downstream structured-grid workflows.

Parameters
----------
lon : array-like or xarray.DataArray
1-D target longitude cell-center coordinate in degrees.
lat : array-like or xarray.DataArray
1-D target latitude cell-center coordinate in degrees.
backend : {'uxarray', 'yac'}, default='yac'
Remapping backend to use. The YAC backend uses YAC's rectilinear
grid support directly. The UXarray backend builds a temporary
structured destination grid and applies native nearest-neighbor
remapping before reshaping the result to latitude/longitude axes.
yac_method : {'nnn', 'average', 'conservative'}, optional
YAC interpolation method. Defaults to ``'nnn'`` because nearest-neighbor
works for node-, edge-, and face-centered source data. ``'conservative'``
requires face-centered source data.
yac_options : dict, optional
YAC interpolation configuration options forwarded to the selected
YAC method.

Returns
-------
xarray.DataArray or xarray.Dataset
Remapped data with the source spatial dimension replaced by the
provided latitude and longitude dimensions.
"""

_validate_backend(backend)
if backend == "yac":
from uxarray.remap.yac import _yac_remap_to_rectilinear

return _yac_remap_to_rectilinear(
self.ux_obj,
lon,
lat,
yac_method or "nnn",
yac_options or {},
)

from uxarray.remap.structured import _native_remap_to_rectilinear

return _native_remap_to_rectilinear(self.ux_obj, lon, lat)

def bilinear(
self,
destination_grid: Grid,
Expand Down
Loading