From 67aeaf1d821722d765d872385edf629a525f5682 Mon Sep 17 00:00:00 2001 From: cmdupuis3 Date: Thu, 10 Sep 2026 12:11:02 -0500 Subject: [PATCH] Fix ESMF encode truncating the fill value into a valid node index `_encode_esmf` applied the 0-based to 1-based offset to the whole connectivity array, padding included, and `elementConn` is encoded as int32. `INT_FILL_VALUE + 1` is `-2**63 + 1`, which the narrowing truncates to `1`: every padded slot lands on disk as node 0. Nothing raises. The file is well formed, so any reader -- including this one -- takes those slots for real vertices, giving faces spurious extra nodes and incorrect polygons. Offset only the valid indices and write ESMF's own -1 into the padding. Co-Authored-By: Claude Opus 5 --- test/io/test_esmf.py | 47 +++++++++++++++++++++++++++++++++++++++++++- uxarray/io/_esmf.py | 11 ++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/test/io/test_esmf.py b/test/io/test_esmf.py index fbf910e1c..01d248e16 100644 --- a/test/io/test_esmf.py +++ b/test/io/test_esmf.py @@ -3,7 +3,7 @@ import pytest import xarray as xr import numpy as np -from uxarray.constants import ERROR_TOLERANCE +from uxarray.constants import ERROR_TOLERANCE, INT_FILL_VALUE def test_read_esmf(gridpath): @@ -103,3 +103,48 @@ def test_esmf_round_trip_consistency(gridpath): # Clean up temporary test file if os.path.exists(esmf_filepath): os.remove(esmf_filepath) + + +def test_encode_esmf_ragged_indices_are_usable(tmp_path): + """Every index written to elementConn names a real node or is the fill value. + + `elementConn` is encoded as int32. Offsetting INT_FILL_VALUE along with the + valid indices leaves `-2**63 + 1` in the padded slots, which the narrowing + truncates to `1` -- node 0, indistinguishable from a real vertex to any + reader. Nothing raises, so check the encoded output rather than a round trip. + """ + uxgrid = ux.Grid.from_topology( + node_lon=np.array([0.0, 10.0, 10.0, 0.0, 20.0]), + node_lat=np.array([0.0, 0.0, 10.0, 10.0, 0.0]), + face_node_connectivity=np.array([ + [0, 1, 2, 3], + [1, 4, 2, INT_FILL_VALUE], + [0, 3, 4, INT_FILL_VALUE], + ]), + fill_value=INT_FILL_VALUE, + ) + + # The padded slots are exactly the ones numElementConn reports as unused + is_padding = np.array([ + [False, False, False, False], + [False, False, False, True], + [False, False, False, True], + ]) + + encoded = uxgrid.to_xarray("ESMF") + assert encoded["elementConn"].attrs["_FillValue"] == -1 + np.testing.assert_array_equal(encoded["elementConn"].values == -1, is_padding) + np.testing.assert_array_equal(encoded["numElementConn"].values, [4, 3, 3]) + + # Check what actually lands on disk: the fill value has to survive int32 + path = tmp_path / "esmf_ragged.nc" + encoded.to_netcdf(path) + with xr.open_dataset(path, mask_and_scale=False) as ds: + on_disk = ds["elementConn"].values + + assert on_disk.dtype == np.int32 + np.testing.assert_array_equal(on_disk == -1, is_padding) + + valid = on_disk[~is_padding] + assert valid.min() >= 1, "elementConn holds an index below 1" + assert valid.max() <= uxgrid.n_node, "elementConn indexes a node that does not exist" diff --git a/uxarray/io/_esmf.py b/uxarray/io/_esmf.py index 538fbf171..59ad7ea3c 100644 --- a/uxarray/io/_esmf.py +++ b/uxarray/io/_esmf.py @@ -144,8 +144,17 @@ def _encode_esmf(ds: xr.Dataset) -> xr.Dataset: # Face Node Connectivity (elementConn) if "face_node_connectivity" in ds: # ESMF elementConn is 1-based, with -1 for unused; UGRID is 0-based + face_node_conn = ds["face_node_connectivity"] + + # Only offset the valid indices. Applying the offset to INT_FILL_VALUE and + # letting it fall through to the int32 encoding below truncates it into a + # small, valid node index, silently turning padding into real vertices. + element_conn = xr.where( + face_node_conn == INT_FILL_VALUE, -1, face_node_conn + 1 + ) + out_ds["elementConn"] = xr.DataArray( - ds["face_node_connectivity"] + 1, + element_conn, dims=("elementCount", "maxNodePElement"), attrs={ "long_name": "Node Indices that define the element connectivity",