Skip to content

Commit 4bcbf77

Browse files
committed
Merge remote-tracking branch 'origin/rajeeja/yac' into rajeeja/yac
2 parents f720999 + ecb5bf5 commit 4bcbf77

5 files changed

Lines changed: 509 additions & 216 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ jobs:
1111
# github.repository == 'UXARRAY/uxarray'
1212
name: Python (${{ matrix.python-version }}, ${{ matrix.os }})
1313
runs-on: ${{ matrix.os }}
14+
env:
15+
MPLBACKEND: Agg
1416
defaults:
1517
run:
1618
shell: bash -l {0}

.github/workflows/yac-optional.yml

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ on:
1010

1111
jobs:
1212
yac-optional:
13-
name: YAC v3.9.3 (Ubuntu)
13+
name: YAC core v3.14.0_p1 (Ubuntu)
1414
runs-on: ubuntu-latest
1515
defaults:
1616
run:
1717
shell: bash -l {0}
1818
env:
19-
YAC_VERSION: v3.9.3
20-
YAXT_VERSION: v0.11.5
19+
YAC_VERSION: v3.14.0_p1
20+
YAXT_VERSION: v0.11.5.1
2121
MPIEXEC: /usr/bin/mpirun
2222
MPIRUN: /usr/bin/mpirun
2323
MPICC: /usr/bin/mpicc
@@ -50,8 +50,6 @@ jobs:
5050
automake \
5151
gawk \
5252
gfortran \
53-
libfyaml-dev \
54-
libnetcdf-dev \
5553
libopenmpi-dev \
5654
libtool \
5755
make \
@@ -68,7 +66,7 @@ jobs:
6866
- name: Install Python build dependencies
6967
run: |
7068
python -m pip install --upgrade pip
71-
python -m pip install cython mpi4py wheel
69+
python -m pip install cython wheel
7270
- name: Build and install YAXT
7371
run: |
7472
set -euxo pipefail
@@ -107,9 +105,11 @@ jobs:
107105
../configure \
108106
--prefix="${YAC_PREFIX}" \
109107
--with-yaxt-root="${YAC_PREFIX}" \
110-
--with-netcdf-root="${CONDA_PREFIX}" \
111-
--with-fyaml-root=/usr \
112-
--without-regard-for-quality \
108+
--disable-mci \
109+
--disable-utils \
110+
--disable-examples \
111+
--disable-tools \
112+
--disable-netcdf \
113113
--enable-python-bindings \
114114
CC="${MPICC}" \
115115
FC="${MPIF90}"
@@ -121,11 +121,18 @@ jobs:
121121
PY_VER="$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
122122
echo "LD_LIBRARY_PATH=${YAC_PREFIX}/lib:${LD_LIBRARY_PATH:-}" >> "${GITHUB_ENV}"
123123
echo "PYTHONPATH=${YAC_PREFIX}/lib/python${PY_VER}/site-packages:${YAC_PREFIX}/lib/python${PY_VER}/dist-packages:${PYTHONPATH:-}" >> "${GITHUB_ENV}"
124-
- name: Verify YAC Python bindings
124+
- name: Verify YAC core Python bindings
125125
run: |
126126
python - <<'PY'
127-
import yac
128-
print("YAC version:", getattr(yac, "__version__", "unknown"))
127+
from pathlib import Path
128+
import sys
129+
candidates = []
130+
for entry in sys.path:
131+
pkg = Path(entry) / "yac"
132+
candidates.extend(pkg.glob("core*.so"))
133+
candidates.extend(pkg.glob("core*.pyd"))
134+
assert candidates, "yac.core extension not found on sys.path"
135+
print("Found yac.core extension:", candidates[0])
129136
PY
130137
- name: Install uxarray
131138
run: |

test/test_remap_yac.py

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
import pytest
33

44
import uxarray as ux
5+
from uxarray.remap.yac import YacNotAvailableError, _import_yac
56

67

7-
yac = pytest.importorskip("yac")
8+
try:
9+
_import_yac()
10+
except YacNotAvailableError:
11+
pytest.skip("yac.core is not available", allow_module_level=True)
812

913

1014
def test_yac_nnn_node_remap(gridpath, datasetpath):
@@ -28,7 +32,7 @@ def test_yac_conservative_face_remap(gridpath):
2832
uxds = ux.open_dataset(mesh_path, mesh_path)
2933
dest = ux.open_grid(mesh_path)
3034

31-
out = uxds["latCell"].remap.nearest_neighbor(
35+
out = uxds["latCell"].remap(
3236
destination_grid=dest,
3337
remap_to="faces",
3438
backend="yac",
@@ -62,3 +66,179 @@ def test_yac_matches_uxarray_nearest_neighbor():
6266
)
6367
assert ux_out.shape == yac_out.shape
6468
assert (ux_out.values == yac_out.values).all()
69+
70+
71+
def test_yac_call_defaults_to_nnn():
72+
verts = np.array([(0.0, 90.0), (-180.0, 0.0), (0.0, -90.0)])
73+
grid = ux.open_grid(verts)
74+
da = ux.UxDataArray(
75+
np.asarray([1.0, 2.0, 3.0]),
76+
dims=["n_node"],
77+
coords={"n_node": [0, 1, 2]},
78+
uxgrid=grid,
79+
)
80+
81+
out = da.remap(
82+
destination_grid=grid,
83+
remap_to="nodes",
84+
backend="yac",
85+
)
86+
87+
assert out.shape == da.shape
88+
np.testing.assert_array_equal(out.values, da.values)
89+
90+
91+
def test_yac_invalid_backend_raises():
92+
verts = np.array([(0.0, 90.0), (-180.0, 0.0), (0.0, -90.0)])
93+
grid = ux.open_grid(verts)
94+
da = ux.UxDataArray(
95+
np.asarray([1.0, 2.0, 3.0]),
96+
dims=["n_node"],
97+
coords={"n_node": [0, 1, 2]},
98+
uxgrid=grid,
99+
)
100+
101+
with pytest.raises(ValueError, match="Invalid backend"):
102+
da.remap.nearest_neighbor(
103+
destination_grid=grid,
104+
remap_to="nodes",
105+
backend="bogus",
106+
)
107+
108+
109+
def test_yac_idw_not_implemented():
110+
verts = np.array([(0.0, 90.0), (-180.0, 0.0), (0.0, -90.0)])
111+
grid = ux.open_grid(verts)
112+
da = ux.UxDataArray(
113+
np.asarray([1.0, 2.0, 3.0]),
114+
dims=["n_node"],
115+
coords={"n_node": [0, 1, 2]},
116+
uxgrid=grid,
117+
)
118+
119+
with pytest.raises(NotImplementedError, match="inverse_distance_weighted"):
120+
da.remap.inverse_distance_weighted(
121+
destination_grid=grid,
122+
remap_to="nodes",
123+
backend="yac",
124+
yac_method="nnn",
125+
yac_options={"n": 1},
126+
)
127+
128+
129+
def test_yac_bilinear_face_remap(gridpath):
130+
mesh_path = gridpath("mpas", "QU", "mesh.QU.1920km.151026.nc")
131+
uxds = ux.open_dataset(mesh_path, mesh_path)
132+
dest = ux.open_grid(mesh_path)
133+
134+
out = uxds["latCell"].remap.bilinear(
135+
destination_grid=dest,
136+
remap_to="faces",
137+
backend="yac",
138+
)
139+
140+
assert out.size == dest.n_face
141+
142+
143+
def test_yac_conservative_rejects_non_face_data():
144+
verts = np.array([(0.0, 90.0), (-180.0, 0.0), (0.0, -90.0)])
145+
grid = ux.open_grid(verts)
146+
da = ux.UxDataArray(
147+
np.asarray([1.0, 2.0, 3.0]),
148+
dims=["n_node"],
149+
coords={"n_node": [0, 1, 2]},
150+
uxgrid=grid,
151+
)
152+
153+
with pytest.raises(ValueError, match="face-centered"):
154+
da.remap.nearest_neighbor(
155+
destination_grid=grid,
156+
remap_to="nodes",
157+
backend="yac",
158+
yac_method="conservative",
159+
yac_options={"order": 1},
160+
)
161+
162+
163+
def test_yac_preserves_spatial_coordinate_remap():
164+
verts = np.array([(0.0, 90.0), (-180.0, 0.0), (0.0, -90.0)])
165+
grid = ux.open_grid(verts)
166+
da = ux.UxDataArray(
167+
np.asarray([1.0, 2.0, 3.0]),
168+
dims=["n_node"],
169+
coords={
170+
"n_node": [0, 1, 2],
171+
"node_lon": (
172+
"n_node",
173+
np.array([0.0, -180.0, 0.0]),
174+
{"standard_name": "longitude", "units": "degrees_east"},
175+
),
176+
"node_lat": (
177+
"n_node",
178+
np.array([90.0, 0.0, -90.0]),
179+
{"standard_name": "latitude", "units": "degrees_north"},
180+
),
181+
},
182+
uxgrid=grid,
183+
)
184+
185+
out = da.remap.nearest_neighbor(
186+
destination_grid=grid,
187+
remap_to="nodes",
188+
backend="yac",
189+
yac_method="nnn",
190+
yac_options={"n": 1},
191+
)
192+
193+
np.testing.assert_array_equal(out.values, da.values)
194+
assert "node_lon" in out.coords
195+
assert "node_lat" in out.coords
196+
197+
198+
def test_yac_batched_remap_with_extra_dimension():
199+
verts = np.array([(0.0, 90.0), (-180.0, 0.0), (0.0, -90.0)])
200+
grid = ux.open_grid(verts)
201+
da = ux.UxDataArray(
202+
np.asarray([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]),
203+
dims=["time", "n_node"],
204+
coords={"time": [0, 1], "n_node": [0, 1, 2]},
205+
uxgrid=grid,
206+
)
207+
208+
out = da.remap.nearest_neighbor(
209+
destination_grid=grid,
210+
remap_to="nodes",
211+
backend="yac",
212+
yac_method="nnn",
213+
yac_options={"n": 1},
214+
)
215+
216+
assert out.shape == da.shape
217+
np.testing.assert_array_equal(out.values, da.values)
218+
219+
220+
def test_yac_batched_remap_with_fractional_mask():
221+
verts = np.array([(0.0, 90.0), (-180.0, 0.0), (0.0, -90.0)])
222+
grid = ux.open_grid(verts)
223+
da = ux.UxDataArray(
224+
np.asarray([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]]),
225+
dims=["time", "n_node"],
226+
coords={"time": [0, 1], "n_node": [0, 1, 2]},
227+
uxgrid=grid,
228+
)
229+
frac_mask = np.ones_like(da.values, dtype=np.float64)
230+
231+
out = da.remap.nearest_neighbor(
232+
destination_grid=grid,
233+
remap_to="nodes",
234+
backend="yac",
235+
yac_method="nnn",
236+
yac_options={
237+
"n": 1,
238+
"frac_mask_fallback_value": 0.0,
239+
"frac_mask": frac_mask,
240+
},
241+
)
242+
243+
assert out.shape == da.shape
244+
np.testing.assert_array_equal(out.values, da.values)

uxarray/remap/accessor.py

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
from uxarray.remap.inverse_distance_weighted import _inverse_distance_weighted_remap
1212
from uxarray.remap.nearest_neighbor import _nearest_neighbor_remap
1313

14+
_VALID_BACKENDS = ("uxarray", "yac")
15+
16+
17+
def _validate_backend(backend: str) -> None:
18+
if backend not in _VALID_BACKENDS:
19+
raise ValueError(
20+
f"Invalid backend '{backend}'. Expected one of {_VALID_BACKENDS}."
21+
)
22+
1423

1524
class RemapAccessor:
1625
"""Expose remapping methods on UxDataArray and UxDataset objects."""
@@ -41,13 +50,10 @@ def __call__(
4150
Calling `.remap(...)` with no explicit method will invoke
4251
`nearest_neighbor(...)`.
4352
"""
44-
return self.nearest_neighbor(
45-
*args,
46-
backend=backend,
47-
yac_method=yac_method,
48-
yac_options=yac_options,
49-
**kwargs,
50-
)
53+
nn_kwargs: dict = {"backend": backend, "yac_options": yac_options}
54+
if yac_method is not None:
55+
nn_kwargs["yac_method"] = yac_method
56+
return self.nearest_neighbor(*args, **nn_kwargs, **kwargs)
5157

5258
def nearest_neighbor(
5359
self,
@@ -84,6 +90,7 @@ def nearest_neighbor(
8490
A new object with data mapped onto `destination_grid`.
8591
"""
8692

93+
_validate_backend(backend)
8794
if backend == "yac":
8895
from uxarray.remap.yac import _yac_remap
8996

@@ -136,12 +143,14 @@ def inverse_distance_weighted(
136143
A new object with data mapped onto `destination_grid`.
137144
"""
138145

146+
_validate_backend(backend)
139147
if backend == "yac":
140-
from uxarray.remap.yac import _yac_remap
141-
142-
yac_kwargs = yac_options or {}
143-
return _yac_remap(
144-
self.ux_obj, destination_grid, remap_to, yac_method, yac_kwargs
148+
raise NotImplementedError(
149+
"inverse_distance_weighted with backend='yac' is not implemented. "
150+
"The YAC backend currently supports only 'nnn' and 'conservative' "
151+
"methods and will not perform inverse-distance-weighted remapping. "
152+
"Use backend='uxarray' for IDW, or choose a different remapping "
153+
"method that is supported by YAC."
145154
)
146155
return _inverse_distance_weighted_remap(
147156
self.ux_obj, destination_grid, remap_to, power, k
@@ -152,7 +161,6 @@ def bilinear(
152161
destination_grid: Grid,
153162
remap_to: str = "faces",
154163
backend: str = "uxarray",
155-
yac_method: str | None = None,
156164
yac_options: dict | None = None,
157165
**kwargs,
158166
) -> UxDataArray | UxDataset:
@@ -167,24 +175,23 @@ def bilinear(
167175
Which grid element receives the remapped values.
168176
169177
backend : {'uxarray', 'yac'}, default='uxarray'
170-
Remapping backend to use. When set to 'yac', requires YAC to be
171-
available on PYTHONPATH.
172-
yac_method : {'nnn', 'conservative'}, optional
173-
YAC interpolation method. Required when backend='yac'.
178+
Remapping backend to use. When set to 'yac', bilinear remapping is
179+
routed through YAC's average interpolation.
174180
yac_options : dict, optional
175-
YAC interpolation configuration options.
181+
YAC interpolation configuration options for the average method.
176182
177183
Returns
178184
-------
179185
UxDataArray or UxDataset
180186
A new object with data mapped onto `destination_grid`.
181187
"""
182188

189+
_validate_backend(backend)
183190
if backend == "yac":
184191
from uxarray.remap.yac import _yac_remap
185192

186193
yac_kwargs = yac_options or {}
187194
return _yac_remap(
188-
self.ux_obj, destination_grid, remap_to, yac_method, yac_kwargs
195+
self.ux_obj, destination_grid, remap_to, "average", yac_kwargs
189196
)
190197
return _bilinear(self.ux_obj, destination_grid, remap_to)

0 commit comments

Comments
 (0)