Skip to content

Commit 743f067

Browse files
authored
Merge pull request #1282 from UXARRAY/rajeeja/fix_to_netcdf_ugrid
Deprecate Grid.encode_as, Add esmf writer, Add tests, fix to_netcdf bug after healpix mesh creation
2 parents c40e239 + ff7aca3 commit 743f067

File tree

12 files changed

+637
-233
lines changed

12 files changed

+637
-233
lines changed

test/test_esmf.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
import os
33
from pathlib import Path
44
import pytest
5-
5+
import xarray as xr
6+
import numpy as np
7+
from uxarray.constants import ERROR_TOLERANCE
68
current_path = Path(os.path.dirname(os.path.realpath(__file__)))
79

810
esmf_ne30_grid_path = current_path / 'meshfiles' / "esmf" / "ne30" / "ne30pg3.grid.nc"
911
esmf_ne30_data_path = current_path / 'meshfiles' / "esmf" / "ne30" / "ne30pg3.data.nc"
12+
gridfile_ne30 = current_path / 'meshfiles' / "ugrid" / "outCSne30" / "outCSne30.ug"
1013

1114
def test_read_esmf():
1215
"""Tests the reading of an ESMF grid file and its encoding into the UGRID
@@ -35,3 +38,72 @@ def test_read_esmf_dataset():
3538

3639
for dim in dims:
3740
assert dim in uxds.dims
41+
42+
def test_esmf_round_trip_consistency():
43+
"""Test round-trip serialization of grid objects through ESMF xarray format.
44+
45+
Validates that grid objects can be successfully converted to ESMF xarray.Dataset
46+
format, serialized to disk, and reloaded while maintaining numerical accuracy
47+
and topological integrity.
48+
49+
The test verifies:
50+
- Successful conversion to ESMF xarray format
51+
- File I/O round-trip consistency
52+
- Preservation of face-node connectivity (exact)
53+
- Preservation of node coordinates (within numerical tolerance)
54+
55+
Raises:
56+
AssertionError: If any round-trip validation fails
57+
"""
58+
# Load original grid
59+
original_grid = ux.open_grid(gridfile_ne30)
60+
61+
# Convert to ESMF xarray format
62+
esmf_dataset = original_grid.to_xarray("ESMF")
63+
64+
# Verify dataset structure
65+
assert isinstance(esmf_dataset, xr.Dataset)
66+
assert 'nodeCoords' in esmf_dataset
67+
assert 'elementConn' in esmf_dataset
68+
69+
# Define output file path
70+
esmf_filepath = "test_esmf_ne30.nc"
71+
72+
# Remove existing test file to ensure clean state
73+
if os.path.exists(esmf_filepath):
74+
os.remove(esmf_filepath)
75+
76+
try:
77+
# Serialize dataset to disk
78+
esmf_dataset.to_netcdf(esmf_filepath)
79+
80+
# Reload grid from serialized file
81+
reloaded_grid = ux.open_grid(esmf_filepath)
82+
83+
# Validate topological consistency (face-node connectivity)
84+
# Integer connectivity arrays must be exactly preserved
85+
np.testing.assert_array_equal(
86+
original_grid.face_node_connectivity.values,
87+
reloaded_grid.face_node_connectivity.values,
88+
err_msg="ESMF face connectivity mismatch"
89+
)
90+
91+
# Validate coordinate consistency with numerical tolerance
92+
# Coordinate transformations and I/O precision may introduce minor differences
93+
np.testing.assert_allclose(
94+
original_grid.node_lon.values,
95+
reloaded_grid.node_lon.values,
96+
err_msg="ESMF longitude mismatch",
97+
rtol=ERROR_TOLERANCE
98+
)
99+
np.testing.assert_allclose(
100+
original_grid.node_lat.values,
101+
reloaded_grid.node_lat.values,
102+
err_msg="ESMF latitude mismatch",
103+
rtol=ERROR_TOLERANCE
104+
)
105+
106+
finally:
107+
# Clean up temporary test file
108+
if os.path.exists(esmf_filepath):
109+
os.remove(esmf_filepath)

test/test_exodus.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,30 @@ def test_mixed_exodus():
3333
"""Read/write an exodus file with two types of faces (triangle and quadrilaterals) and writes a ugrid file."""
3434
uxgrid = ux.open_grid(exo2_filename)
3535

36-
uxgrid.encode_as("UGRID")
37-
uxgrid.encode_as("Exodus")
38-
# Add assertions or checks as needed
36+
ugrid_obj = uxgrid.to_xarray("UGRID")
37+
exo_obj = uxgrid.to_xarray("Exodus")
38+
39+
ugrid_obj.to_netcdf("test_ugrid.nc")
40+
exo_obj.to_netcdf("test_exo.exo")
41+
42+
ugrid_load_saved = ux.open_grid("test_ugrid.nc")
43+
exodus_load_saved = ux.open_grid("test_exo.exo")
44+
45+
# Face node connectivity comparison
46+
assert np.array_equal(ugrid_load_saved.face_node_connectivity.values, uxgrid.face_node_connectivity.values)
47+
assert np.array_equal(uxgrid.face_node_connectivity.values, exodus_load_saved.face_node_connectivity.values)
48+
49+
# Node coordinates comparison
50+
assert np.array_equal(ugrid_load_saved.node_lon.values, uxgrid.node_lon.values)
51+
assert np.array_equal(uxgrid.node_lon.values, exodus_load_saved.node_lon.values)
52+
assert np.array_equal(ugrid_load_saved.node_lat.values, uxgrid.node_lat.values)
53+
54+
# Cleanup
55+
ugrid_load_saved._ds.close()
56+
exodus_load_saved._ds.close()
57+
del ugrid_load_saved, exodus_load_saved
58+
os.remove("test_ugrid.nc")
59+
os.remove("test_exo.exo")
3960

4061
def test_standardized_dtype_and_fill():
4162
"""Test to see if Mesh2_Face_Nodes uses the expected integer datatype and expected fill value as set in constants.py."""

test/test_grid.py

Lines changed: 114 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,115 @@ def test_grid_with_holes():
9696
assert grid_without_holes.global_sphere_coverage
9797

9898

99-
def test_grid_encode_as():
100-
"""Reads a ugrid file and encodes it as `xarray.Dataset` in various types."""
101-
grid_CSne30.encode_as("UGRID")
102-
grid_RLL1deg.encode_as("UGRID")
103-
grid_RLL10deg_CSne4.encode_as("UGRID")
99+
def test_grid_ugrid_exodus_roundtrip():
100+
"""Test round-trip serialization of grid objects through UGRID and Exodus xarray formats.
104101
105-
grid_CSne30.encode_as("Exodus")
106-
grid_RLL1deg.encode_as("Exodus")
107-
grid_RLL10deg_CSne4.encode_as("Exodus")
102+
Validates that grid objects can be successfully converted to xarray.Dataset
103+
objects in both UGRID and Exodus formats, serialized to disk, and reloaded
104+
while maintaining numerical accuracy and topological integrity.
105+
106+
The test verifies:
107+
- Successful conversion to UGRID and Exodus xarray formats
108+
- File I/O round-trip consistency
109+
- Preservation of face-node connectivity (exact)
110+
- Preservation of node coordinates (within numerical tolerance)
111+
112+
Raises:
113+
AssertionError: If any round-trip validation fails
114+
"""
115+
116+
# Convert grids to xarray.Dataset objects in different formats
117+
ugrid_datasets = {
118+
'CSne30': grid_CSne30.to_xarray("UGRID"),
119+
'RLL1deg': grid_RLL1deg.to_xarray("UGRID"),
120+
'RLL10deg_CSne4': grid_RLL10deg_CSne4.to_xarray("UGRID")
121+
}
122+
123+
exodus_datasets = {
124+
'CSne30': grid_CSne30.to_xarray("Exodus"),
125+
'RLL1deg': grid_RLL1deg.to_xarray("Exodus"),
126+
'RLL10deg_CSne4': grid_RLL10deg_CSne4.to_xarray("Exodus")
127+
}
128+
129+
# Define test cases with corresponding grid objects
130+
test_grids = {
131+
'CSne30': grid_CSne30,
132+
'RLL1deg': grid_RLL1deg,
133+
'RLL10deg_CSne4': grid_RLL10deg_CSne4
134+
}
135+
136+
# Perform round-trip validation for each grid type
137+
test_files = []
138+
139+
for grid_name in test_grids.keys():
140+
ugrid_dataset = ugrid_datasets[grid_name]
141+
exodus_dataset = exodus_datasets[grid_name]
142+
original_grid = test_grids[grid_name]
143+
144+
# Define output file paths
145+
ugrid_filepath = f"test_ugrid_{grid_name}.nc"
146+
exodus_filepath = f"test_exodus_{grid_name}.exo"
147+
test_files.append(ugrid_filepath)
148+
test_files.append(exodus_filepath)
149+
150+
# Serialize datasets to disk
151+
ugrid_dataset.to_netcdf(ugrid_filepath)
152+
exodus_dataset.to_netcdf(exodus_filepath)
153+
154+
# Reload grids from serialized files
155+
reloaded_ugrid = ux.open_grid(ugrid_filepath)
156+
reloaded_exodus = ux.open_grid(exodus_filepath)
157+
158+
# Validate topological consistency (face-node connectivity)
159+
# Integer connectivity arrays must be exactly preserved
160+
np.testing.assert_array_equal(
161+
original_grid.face_node_connectivity.values,
162+
reloaded_ugrid.face_node_connectivity.values,
163+
err_msg=f"UGRID face connectivity mismatch for {grid_name}"
164+
)
165+
np.testing.assert_array_equal(
166+
original_grid.face_node_connectivity.values,
167+
reloaded_exodus.face_node_connectivity.values,
168+
err_msg=f"Exodus face connectivity mismatch for {grid_name}"
169+
)
170+
171+
# Validate coordinate consistency with numerical tolerance
172+
# Coordinate transformations and I/O precision may introduce minor differences
173+
np.testing.assert_allclose(
174+
original_grid.node_lon.values,
175+
reloaded_ugrid.node_lon.values,
176+
err_msg=f"UGRID longitude mismatch for {grid_name}",
177+
rtol=ERROR_TOLERANCE
178+
)
179+
np.testing.assert_allclose(
180+
original_grid.node_lon.values,
181+
reloaded_exodus.node_lon.values,
182+
err_msg=f"Exodus longitude mismatch for {grid_name}",
183+
rtol=ERROR_TOLERANCE
184+
)
185+
np.testing.assert_allclose(
186+
original_grid.node_lat.values,
187+
reloaded_ugrid.node_lat.values,
188+
err_msg=f"UGRID latitude mismatch for {grid_name}",
189+
rtol=ERROR_TOLERANCE
190+
)
191+
np.testing.assert_allclose(
192+
original_grid.node_lat.values,
193+
reloaded_exodus.node_lat.values,
194+
err_msg=f"Exodus latitude mismatch for {grid_name}",
195+
rtol=ERROR_TOLERANCE
196+
)
197+
198+
# This might be need for windows "ermissionError: [WinError 32] -- file accessed by another process"
199+
reloaded_exodus._ds.close()
200+
reloaded_ugrid._ds.close()
201+
del reloaded_exodus
202+
del reloaded_ugrid
203+
204+
# Clean up temporary test files
205+
for filepath in test_files:
206+
if os.path.exists(filepath):
207+
os.remove(filepath)
108208

109209

110210
def test_grid_init_verts():
@@ -147,21 +247,21 @@ def test_grid_init_verts():
147247

148248
assert vgrid.n_face == 6
149249
assert vgrid.n_node == 8
150-
vgrid.encode_as("UGRID")
250+
vgrid.to_xarray("UGRID")
151251

152252
faces_verts_one = np.array([
153253
np.array([[150, 10], [160, 20], [150, 30], [135, 30], [125, 20], [135, 10]])
154254
])
155255
vgrid = ux.open_grid(faces_verts_one, latlon=True)
156256
assert vgrid.n_face == 1
157257
assert vgrid.n_node == 6
158-
vgrid.encode_as("UGRID")
258+
vgrid.to_xarray("UGRID")
159259

160260
faces_verts_single_face = np.array([[150, 10], [160, 20], [150, 30], [135, 30], [125, 20], [135, 10]])
161261
vgrid = ux.open_grid(faces_verts_single_face, latlon=True)
162262
assert vgrid.n_face == 1
163263
assert vgrid.n_node == 6
164-
vgrid.encode_as("UGRID")
264+
vgrid.to_xarray("UGRID")
165265

166266

167267
def test_grid_init_verts_different_input_datatype():
@@ -174,7 +274,7 @@ def test_grid_init_verts_different_input_datatype():
174274
vgrid = ux.open_grid(faces_verts_ndarray, latlon=True)
175275
assert vgrid.n_face == 3
176276
assert vgrid.n_node == 14
177-
vgrid.encode_as("UGRID")
277+
vgrid.to_xarray("UGRID")
178278

179279
faces_verts_list = [[[150, 10], [160, 20], [150, 30], [135, 30], [125, 20], [135, 10]],
180280
[[125, 20], [135, 30], [125, 60], [110, 60], [100, 30], [105, 20]],
@@ -183,7 +283,7 @@ def test_grid_init_verts_different_input_datatype():
183283
assert vgrid.n_face == 3
184284
assert vgrid.n_node == 14
185285
assert vgrid.validate()
186-
vgrid.encode_as("UGRID")
286+
vgrid.to_xarray("UGRID")
187287

188288
faces_verts_tuples = [
189289
((150, 10), (160, 20), (150, 30), (135, 30), (125, 20), (135, 10)),
@@ -194,7 +294,7 @@ def test_grid_init_verts_different_input_datatype():
194294
assert vgrid.n_face == 3
195295
assert vgrid.n_node == 14
196296
assert vgrid.validate()
197-
vgrid.encode_as("UGRID")
297+
vgrid.to_xarray("UGRID")
198298

199299

200300
def test_grid_init_verts_fill_values():

test/test_healpix.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import xarray as xr
77
import pandas as pd
88
from pathlib import Path
9+
from uxarray.constants import ERROR_TOLERANCE
910

1011

1112
current_path = Path(os.path.dirname(os.path.realpath(__file__)))
@@ -99,3 +100,89 @@ def test_invalid_cells():
99100
xrda = xr.DataArray(data=np.ones(11), dims=['cell']).to_dataset(name='cell')
100101
with pytest.raises(ValueError):
101102
uxda = ux.UxDataset.from_healpix(xrda)
103+
104+
def test_healpix_round_trip_consistency(tmp_path):
105+
"""Test round-trip serialization of HEALPix grid through UGRID and Exodus formats.
106+
107+
Validates that HEALPix grid objects can be successfully converted to xarray.Dataset
108+
objects in both UGRID and Exodus formats, serialized to disk, and reloaded
109+
while maintaining numerical accuracy and topological integrity.
110+
111+
Args:
112+
tmp_path: pytest fixture providing temporary directory
113+
114+
Raises:
115+
AssertionError: If any round-trip validation fails
116+
"""
117+
# Create HEALPix grid
118+
original_grid = ux.Grid.from_healpix(zoom=3)
119+
120+
# Access node coordinates to ensure they're generated before encoding
121+
_ = original_grid.node_lon
122+
_ = original_grid.node_lat
123+
124+
# Convert to xarray.Dataset objects in different formats
125+
ugrid_dataset = original_grid.to_xarray("UGRID")
126+
exodus_dataset = original_grid.to_xarray("Exodus")
127+
128+
# Define output file paths using tmp_path fixture
129+
ugrid_filepath = tmp_path / "healpix_test_ugrid.nc"
130+
exodus_filepath = tmp_path / "healpix_test_exodus.exo"
131+
132+
# Serialize datasets to disk
133+
ugrid_dataset.to_netcdf(ugrid_filepath)
134+
exodus_dataset.to_netcdf(exodus_filepath)
135+
136+
# Verify files were created successfully
137+
assert ugrid_filepath.exists()
138+
assert ugrid_filepath.stat().st_size > 0
139+
assert exodus_filepath.exists()
140+
assert exodus_filepath.stat().st_size > 0
141+
142+
# Reload grids from serialized files
143+
reloaded_ugrid = ux.open_grid(ugrid_filepath)
144+
reloaded_exodus = ux.open_grid(exodus_filepath)
145+
146+
# Validate topological consistency (face-node connectivity)
147+
# Integer connectivity arrays must be exactly preserved
148+
np.testing.assert_array_equal(
149+
original_grid.face_node_connectivity.values,
150+
reloaded_ugrid.face_node_connectivity.values,
151+
err_msg="UGRID face connectivity mismatch for HEALPix"
152+
)
153+
np.testing.assert_array_equal(
154+
original_grid.face_node_connectivity.values,
155+
reloaded_exodus.face_node_connectivity.values,
156+
err_msg="Exodus face connectivity mismatch for HEALPix"
157+
)
158+
159+
# Validate coordinate consistency with numerical tolerance
160+
# Coordinate transformations and I/O precision may introduce minor differences
161+
np.testing.assert_allclose(
162+
original_grid.node_lon.values,
163+
reloaded_ugrid.node_lon.values,
164+
err_msg="UGRID longitude mismatch for HEALPix",
165+
rtol=ERROR_TOLERANCE
166+
)
167+
np.testing.assert_allclose(
168+
original_grid.node_lon.values,
169+
reloaded_exodus.node_lon.values,
170+
err_msg="Exodus longitude mismatch for HEALPix",
171+
rtol=ERROR_TOLERANCE
172+
)
173+
np.testing.assert_allclose(
174+
original_grid.node_lat.values,
175+
reloaded_ugrid.node_lat.values,
176+
err_msg="UGRID latitude mismatch for HEALPix",
177+
rtol=ERROR_TOLERANCE
178+
)
179+
np.testing.assert_allclose(
180+
original_grid.node_lat.values,
181+
reloaded_exodus.node_lat.values,
182+
err_msg="Exodus latitude mismatch for HEALPix",
183+
rtol=ERROR_TOLERANCE
184+
)
185+
186+
# Validate grid dimensions are preserved
187+
assert reloaded_ugrid.n_face == original_grid.n_face
188+
assert reloaded_exodus.n_face == original_grid.n_face

0 commit comments

Comments
 (0)