Skip to content

Commit 67060d2

Browse files
authored
Merge branch 'main' into interpolate-cross-section
2 parents e24ccf0 + 743f067 commit 67060d2

17 files changed

Lines changed: 720 additions & 284 deletions

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ repos:
1414

1515
- repo: https://github.com/astral-sh/ruff-pre-commit
1616
# Ruff version.
17-
rev: v0.12.4
17+
rev: v0.12.7
1818
hooks:
1919
- id: ruff
2020
name: lint with ruff

CODE_OF_CONDUCT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Our Pledge
44

5-
We, as contributors, creators, stewards, and maintainers (collectively referred to as "contributors" below), of GeoCAT projects pledge to make participation in our software, system or hardware project and community a safe, productive, and welcoming experience for everyone. All contributors are required to abide by this Code of Conduct. This includes respectful treatment of everyone regardless of age, body size, disability, ethnicity, gender identity or expression, level of experience, nationality, political affiliation, veteran status, pregnancy, genetic information, physical appearance, race, religion, or sexual orientation, as well as any other characteristic protected under applicable US federal or state law.
5+
We, as contributors, creators, stewards, and maintainers (collectively referred to as "contributors" below), of the UXarray project pledge to make participation in our software, system or hardware project and community a safe, productive, and welcoming experience for everyone. All contributors are required to abide by this Code of Conduct. This includes respectful treatment of everyone regardless of age, body size, disability, ethnicity, gender identity or expression, level of experience, nationality, political affiliation, veteran status, pregnancy, genetic information, physical appearance, race, religion, or sexual orientation, as well as any other characteristic protected under applicable US federal or state law.
66

77
## Our Standards
88

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():

0 commit comments

Comments
 (0)