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",