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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jobs:
# github.repository == 'UXARRAY/uxarray'
name: Python (${{ matrix.python-version }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
env:
MPLBACKEND: Agg
defaults:
run:
shell: bash -l {0}
Expand Down
142 changes: 142 additions & 0 deletions .github/workflows/yac-optional.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
name: YAC Optional CI

on:
pull_request:
paths:
- ".github/workflows/yac-optional.yml"
- "uxarray/remap/**"
- "test/test_remap_yac.py"
workflow_dispatch:

jobs:
yac-optional:
name: YAC core v3.14.0_p1 (Ubuntu)
runs-on: ubuntu-latest
defaults:
run:
shell: bash -l {0}
env:
YAC_VERSION: v3.14.0_p1
YAXT_VERSION: v0.11.5.1
MPIEXEC: /usr/bin/mpirun
MPIRUN: /usr/bin/mpirun
MPICC: /usr/bin/mpicc
MPIFC: /usr/bin/mpif90
MPIF90: /usr/bin/mpif90
OMPI_ALLOW_RUN_AS_ROOT: 1
OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1
steps:
- name: checkout
uses: actions/checkout@v4
with:
token: ${{ github.token }}

- name: conda_setup
uses: conda-incubator/setup-miniconda@v3
with:
activate-environment: uxarray_build
channel-priority: strict
python-version: "3.11"
channels: conda-forge
environment-file: ci/environment.yml
miniforge-variant: Miniforge3
miniforge-version: latest

- name: Install build dependencies (apt)
run: |
sudo apt-get update
sudo apt-get install -y \
autoconf \
automake \
gawk \
gfortran \
libopenmpi-dev \
libtool \
make \
openmpi-bin \
pkg-config
- name: Verify MPI tools
run: |
which mpirun
which mpicc
which mpif90
mpirun --version
mpicc --version
mpif90 --version
- name: Install Python build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install cython wheel
- name: Build and install YAXT
run: |
set -euxo pipefail
YAC_PREFIX="${GITHUB_WORKSPACE}/yac_prefix"
echo "YAC_PREFIX=${YAC_PREFIX}" >> "${GITHUB_ENV}"
git clone --depth 1 --branch "${YAXT_VERSION}" https://gitlab.dkrz.de/dkrz-sw/yaxt.git
if [ ! -x yaxt/configure ]; then
if [ -x yaxt/autogen.sh ]; then
(cd yaxt && ./autogen.sh)
else
(cd yaxt && autoreconf -i)
fi
fi
mkdir -p yaxt/build
cd yaxt/build
../configure \
--prefix="${YAC_PREFIX}" \
--without-regard-for-quality \
CC="${MPICC}" \
FC="${MPIF90}"
make -j2
make install
- name: Build and install YAC
run: |
set -euxo pipefail
git clone --depth 1 --branch "${YAC_VERSION}" https://gitlab.dkrz.de/dkrz-sw/yac.git
if [ ! -x yac/configure ]; then
if [ -x yac/autogen.sh ]; then
(cd yac && ./autogen.sh)
else
(cd yac && autoreconf -i)
fi
fi
mkdir -p yac/build
cd yac/build
../configure \
--prefix="${YAC_PREFIX}" \
--with-yaxt-root="${YAC_PREFIX}" \
--disable-mci \
--disable-utils \
--disable-examples \
--disable-tools \
--disable-netcdf \
--enable-python-bindings \
CC="${MPICC}" \
FC="${MPIF90}"
make -j2
make install
- name: Configure YAC runtime paths
run: |
set -euxo pipefail
PY_VER="$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
echo "LD_LIBRARY_PATH=${YAC_PREFIX}/lib:${LD_LIBRARY_PATH:-}" >> "${GITHUB_ENV}"
echo "PYTHONPATH=${YAC_PREFIX}/lib/python${PY_VER}/site-packages:${YAC_PREFIX}/lib/python${PY_VER}/dist-packages:${PYTHONPATH:-}" >> "${GITHUB_ENV}"
- name: Verify YAC core Python bindings
run: |
python - <<'PY'
from pathlib import Path
import sys
candidates = []
for entry in sys.path:
pkg = Path(entry) / "yac"
candidates.extend(pkg.glob("core*.so"))
candidates.extend(pkg.glob("core*.pyd"))
assert candidates, "yac.core extension not found on sys.path"
print("Found yac.core extension:", candidates[0])
PY
- name: Install uxarray
run: |
python -m pip install . --no-deps
- name: Run tests (uxarray with YAC)
run: |
python -m pytest test/test_remap_yac.py
68 changes: 68 additions & 0 deletions test/test_remap_yac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import numpy as np
import pytest

import uxarray as ux
from uxarray.remap.yac import YacNotAvailableError, _import_yac


try:
_import_yac()
except YacNotAvailableError:
pytest.skip("yac.core is not available", allow_module_level=True)


def test_yac_nnn_node_remap(gridpath, datasetpath):
grid_path = gridpath("ugrid", "geoflow-small", "grid.nc")
uxds = ux.open_dataset(grid_path, datasetpath("ugrid", "geoflow-small", "v1.nc"))
dest = ux.open_grid(grid_path)

out = uxds["v1"].remap.nearest_neighbor(
destination_grid=dest,
remap_to="nodes",
backend="yac",
yac_method="nnn",
yac_options={"n": 1},
)
assert out.size > 0
assert "n_node" in out.dims


def test_yac_conservative_face_remap(gridpath):
mesh_path = gridpath("mpas", "QU", "mesh.QU.1920km.151026.nc")
uxds = ux.open_dataset(mesh_path, mesh_path)
dest = ux.open_grid(mesh_path)

out = uxds["latCell"].remap.nearest_neighbor(
destination_grid=dest,
remap_to="faces",
backend="yac",
yac_method="conservative",
Comment on lines +35 to +39
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test exercises yac_method='conservative' through the nearest_neighbor(...) API, which is semantically confusing (it’s not nearest-neighbor). Once the public API is clarified (e.g., a dedicated conservative(...) method or a generic .remap(...) entrypoint), update this test to use the method that matches the behavior being tested.

Suggested change
out = uxds["latCell"].remap.nearest_neighbor(
destination_grid=dest,
remap_to="faces",
backend="yac",
yac_method="conservative",
out = uxds["latCell"].remap.conservative(
destination_grid=dest,
remap_to="faces",
backend="yac",

Copilot uses AI. Check for mistakes.
yac_options={"order": 1},
)
assert out.size == dest.n_face


def test_yac_matches_uxarray_nearest_neighbor():
verts = np.array([(0.0, 90.0), (-180.0, 0.0), (0.0, -90.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,
)

ux_out = da.remap.nearest_neighbor(
destination_grid=grid,
remap_to="nodes",
backend="uxarray",
)
yac_out = da.remap.nearest_neighbor(
destination_grid=grid,
remap_to="nodes",
backend="yac",
yac_method="nnn",
yac_options={"n": 1},
)
assert ux_out.shape == yac_out.shape
assert (ux_out.values == yac_out.values).all()
88 changes: 83 additions & 5 deletions uxarray/remap/accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,36 @@ def __repr__(self) -> str:
+ " • inverse_distance_weighted(destination_grid, remap_to='faces', power=2, k=8)\n"
)

def __call__(self, *args, **kwargs) -> UxDataArray | UxDataset:
def __call__(
self,
*args,
backend: str = "uxarray",
yac_method: str | None = None,
yac_options: dict | None = None,
**kwargs,
) -> UxDataArray | UxDataset:
"""
Shortcut for nearest-neighbor remapping.

Calling `.remap(...)` with no explicit method will invoke
`nearest_neighbor(...)`.
"""
return self.nearest_neighbor(*args, **kwargs)
return self.nearest_neighbor(
*args,
backend=backend,
yac_method=yac_method,
yac_options=yac_options,
**kwargs,
)
Comment on lines +30 to +50
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemapAccessor.__call__ forwards yac_method=None into nearest_neighbor(...), which overrides that method’s default of 'nnn' and will cause .remap(..., backend='yac') to raise ValueError unexpectedly. Consider defaulting yac_method to 'nnn' here when backend=='yac', or only passing yac_method through when it’s not None.

Copilot uses AI. Check for mistakes.

def nearest_neighbor(
self, destination_grid: Grid, remap_to: str = "faces", **kwargs
self,
destination_grid: Grid,
remap_to: str = "faces",
backend: str = "uxarray",
yac_method: str | None = "nnn",
yac_options: dict | None = None,
**kwargs,
) -> UxDataArray | UxDataset:
"""
Perform nearest-neighbor remapping.
Expand All @@ -51,16 +70,39 @@ def nearest_neighbor(
remap_to : {'nodes', 'edges', 'faces'}, default='faces'
Which grid element receives the remapped values.

backend : {'uxarray', 'yac'}, default='uxarray'
Remapping backend to use. When set to 'yac', requires YAC to be
available on PYTHONPATH.
yac_method : {'nnn', 'conservative'}, optional
YAC interpolation method. Defaults to 'nnn' when backend='yac'.
yac_options : dict, optional
YAC interpolation configuration options.

Returns
-------
UxDataArray or UxDataset
A new object with data mapped onto `destination_grid`.
"""

if backend == "yac":
from uxarray.remap.yac import _yac_remap

yac_kwargs = yac_options or {}
return _yac_remap(
self.ux_obj, destination_grid, remap_to, yac_method, yac_kwargs
)
return _nearest_neighbor_remap(self.ux_obj, destination_grid, remap_to)
Comment on lines +87 to 94
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backend is only checked for the special-case value 'yac'; any other unexpected value silently falls back to the UXarray implementation. To avoid typos changing behavior without warning, validate backend (e.g., allow only 'uxarray'/'yac' and raise ValueError otherwise).

Copilot uses AI. Check for mistakes.

def inverse_distance_weighted(
self, destination_grid: Grid, remap_to: str = "faces", power=2, k=8, **kwargs
self,
destination_grid: Grid,
remap_to: str = "faces",
power=2,
k=8,
backend: str = "uxarray",
yac_method: str | None = None,
yac_options: dict | None = None,
**kwargs,
) -> UxDataArray | UxDataset:
"""
Perform inverse-distance-weighted (IDW) remapping.
Expand All @@ -80,18 +122,39 @@ def inverse_distance_weighted(
k : int, default=8
Number of nearest source points to include in the weighted average.

backend : {'uxarray', 'yac'}, default='uxarray'
Remapping backend to use. When set to 'yac', requires YAC to be
available on PYTHONPATH.
yac_method : {'nnn', 'conservative'}, optional
YAC interpolation method. Required when backend='yac'.
yac_options : dict, optional
YAC interpolation configuration options.

Returns
-------
UxDataArray or UxDataset
A new object with data mapped onto `destination_grid`.
"""

if backend == "yac":
from uxarray.remap.yac import _yac_remap

yac_kwargs = yac_options or {}
return _yac_remap(
self.ux_obj, destination_grid, remap_to, yac_method, yac_kwargs
Comment on lines +140 to +144
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When backend=='yac', inverse_distance_weighted(...) routes to _yac_remap(...), but _yac_remap only supports 'nnn' / 'conservative' and will not perform IDW. This makes inverse_distance_weighted(backend='yac') return results that don’t match the method name/contract. Consider raising NotImplementedError/ValueError for backend='yac' here until a real YAC-IDW implementation exists (or map explicitly to the equivalent YAC method if that’s intended).

Suggested change
from uxarray.remap.yac import _yac_remap
yac_kwargs = yac_options or {}
return _yac_remap(
self.ux_obj, destination_grid, remap_to, yac_method, yac_kwargs
raise NotImplementedError(
"inverse_distance_weighted with backend='yac' is not implemented. "
"The YAC backend currently supports only 'nnn' and 'conservative' "
"methods and will not perform inverse-distance-weighted remapping. "
"Use backend='uxarray' for IDW, or choose a different remapping "
"method that is supported by YAC."

Copilot uses AI. Check for mistakes.
)
return _inverse_distance_weighted_remap(
self.ux_obj, destination_grid, remap_to, power, k
)

def bilinear(
self, destination_grid: Grid, remap_to: str = "faces", **kwargs
self,
destination_grid: Grid,
remap_to: str = "faces",
backend: str = "uxarray",
yac_method: str | None = None,
yac_options: dict | None = None,
**kwargs,
) -> UxDataArray | UxDataset:
"""
Perform bilinear remapping.
Expand All @@ -103,10 +166,25 @@ def bilinear(
remap_to : {'nodes', 'edges', 'faces'}, default='faces'
Which grid element receives the remapped values.

backend : {'uxarray', 'yac'}, default='uxarray'
Remapping backend to use. When set to 'yac', requires YAC to be
available on PYTHONPATH.
yac_method : {'nnn', 'conservative'}, optional
YAC interpolation method. Required when backend='yac'.
yac_options : dict, optional
YAC interpolation configuration options.

Returns
-------
UxDataArray or UxDataset
A new object with data mapped onto `destination_grid`.
"""

if backend == "yac":
from uxarray.remap.yac import _yac_remap

yac_kwargs = yac_options or {}
return _yac_remap(
self.ux_obj, destination_grid, remap_to, yac_method, yac_kwargs
)
Comment on lines +183 to +189
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When backend=='yac', bilinear(...) routes to _yac_remap(...), but _yac_remap only supports 'nnn' / 'conservative' and will not perform bilinear interpolation. This means bilinear(backend='yac') can silently do a different method than requested. Consider rejecting backend='yac' for bilinear until a true YAC-bilinear path is implemented (or wire it up explicitly).

Copilot uses AI. Check for mistakes.
return _bilinear(self.ux_obj, destination_grid, remap_to)
Loading
Loading