|
| 1 | +""" |
| 2 | +Common IO tests that apply to all grid formats. These tests make sure the |
| 3 | +same basic things work no matter which file format you start with. |
| 4 | +""" |
| 5 | + |
| 6 | +import pytest |
| 7 | +import numpy as np |
| 8 | +import xarray as xr |
| 9 | +import uxarray as ux |
| 10 | +from pathlib import Path |
| 11 | +import tempfile |
| 12 | +import os |
| 13 | +import warnings |
| 14 | +from numpy.testing import assert_array_equal, assert_allclose |
| 15 | +from uxarray.constants import ERROR_TOLERANCE, INT_DTYPE, INT_FILL_VALUE |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | +current_path = Path(os.path.dirname(os.path.realpath(__file__))) |
| 20 | + |
| 21 | +# Define all testable format combinations |
| 22 | +# Format: (format_type, subpath, filename) |
| 23 | +IO_READ_TEST_FORMATS = [ |
| 24 | + ("ugrid", "ugrid/quad-hexagon", "grid.nc"), |
| 25 | + ("ugrid", "ugrid/outCSne30", "outCSne30.ug"), |
| 26 | + ("ugrid", "ugrid/outRLL1deg", "outRLL1deg.ug"), |
| 27 | + ("mpas", "mpas/QU/480", "grid.nc"), |
| 28 | + ("esmf", "esmf/ne30", "ne30pg3.grid.nc"), |
| 29 | + ("exodus", "exodus/outCSne8", "outCSne8.g"), |
| 30 | + ("exodus", "exodus/mixed", "mixed.exo"), |
| 31 | + ("scrip", "scrip/outCSne8", "outCSne8.nc"), |
| 32 | + ("icon", "icon/R02B04", "icon_grid_0010_R02B04_G.nc"), |
| 33 | + ("fesom", "fesom/pi", None), # Special case - multiple files |
| 34 | + ("healpix", None, None), # Constructed via classmethod |
| 35 | +] |
| 36 | + |
| 37 | +# Formats that support writing |
| 38 | +WRITABLE_FORMATS = ["ugrid", "exodus", "scrip", "esmf"] |
| 39 | + |
| 40 | +# Format conversion test pairs - removed for now as format conversion |
| 41 | +# requires more sophisticated handling than simple to_netcdf |
| 42 | + |
| 43 | + |
| 44 | +@pytest.fixture(params=IO_READ_TEST_FORMATS) |
| 45 | +def grid_from_format(request): |
| 46 | + """Load a Grid from each supported format for parameterized tests. |
| 47 | +
|
| 48 | + Handles special cases (FESOM multi-file, HEALPix) and tags the grid with |
| 49 | + ``_test_format`` for easier debugging. |
| 50 | + """ |
| 51 | + format_name, subpath, filename = request.param |
| 52 | + |
| 53 | + if format_name == "fesom" and filename is None: |
| 54 | + # Special handling for FESOM with multiple input files |
| 55 | + fesom_data_path = current_path / "meshfiles" / subpath / "data" |
| 56 | + fesom_mesh_path = current_path / "meshfiles" / subpath |
| 57 | + grid = ux.open_grid(fesom_mesh_path, fesom_data_path) |
| 58 | + elif format_name == "healpix": |
| 59 | + # Construct a basic HEALPix grid |
| 60 | + grid = ux.Grid.from_healpix(zoom=1, pixels_only=False) |
| 61 | + else: |
| 62 | + grid_path = current_path / "meshfiles" / subpath / filename |
| 63 | + if not grid_path.exists(): |
| 64 | + pytest.skip(f"Test file not found: {grid_path}") |
| 65 | + |
| 66 | + # Handle special cases |
| 67 | + if format_name == "mpas": |
| 68 | + grid = ux.open_grid(grid_path, use_dual=False) |
| 69 | + else: |
| 70 | + grid = ux.open_grid(grid_path) |
| 71 | + |
| 72 | + # Add format info to the grid for test identification |
| 73 | + grid._test_format = format_name |
| 74 | + return grid |
| 75 | + |
| 76 | + |
| 77 | +class TestIOCommon: |
| 78 | + """Common IO tests across all formats. Helps catch format-specific |
| 79 | + regressions early and keep behavior consistent. |
| 80 | + """ |
| 81 | + |
| 82 | + def test_return_type(self, grid_from_format): |
| 83 | + """Open each format and return a ux.Grid. Checks that the public API |
| 84 | + is consistent across readers. |
| 85 | + """ |
| 86 | + grid = grid_from_format |
| 87 | + |
| 88 | + # Basic validation |
| 89 | + assert isinstance(grid, ux.Grid) |
| 90 | + |
| 91 | + def test_ugrid_compliance(self, grid_from_format): |
| 92 | + """Check that a loaded grid looks like a UGRID mesh. We look for |
| 93 | + required topology, coordinates, proper fill values, reasonable degree |
| 94 | + ranges, and that ``validate()`` passes. |
| 95 | + """ |
| 96 | + grid = grid_from_format |
| 97 | + |
| 98 | + # Basic topology and coordinate presence |
| 99 | + assert 'face_node_connectivity' in grid.connectivity |
| 100 | + assert 'node_lon' in grid.coordinates |
| 101 | + assert 'node_lat' in grid.coordinates |
| 102 | + |
| 103 | + # Required dimensions |
| 104 | + assert 'n_node' in grid.dims |
| 105 | + assert 'n_face' in grid.dims |
| 106 | + |
| 107 | + # Validate grid structure |
| 108 | + assert grid.validate() |
| 109 | + |
| 110 | + # Check UGRID compliance |
| 111 | + # 1. Connectivity should use proper fill values |
| 112 | + assert grid.face_node_connectivity._FillValue == INT_FILL_VALUE |
| 113 | + |
| 114 | + # 3. Check that grid has been properly standardized by uxarray |
| 115 | + # (Not all input files have Conventions attribute, but uxarray should handle them) |
| 116 | + |
| 117 | + def test_grid_properties_consistency(self, grid_from_format): |
| 118 | + """Make sure core dims and variables are present with the expected |
| 119 | + dtypes across formats. Avoid surprises for downstream code. |
| 120 | + """ |
| 121 | + grid = grid_from_format |
| 122 | + |
| 123 | + # Check that all grids have the essential properties |
| 124 | + assert 'n_node' in grid.dims |
| 125 | + assert 'n_face' in grid.dims |
| 126 | + assert 'face_node_connectivity' in grid.connectivity |
| 127 | + assert 'node_lon' in grid.coordinates |
| 128 | + assert 'node_lat' in grid.coordinates |
| 129 | + |
| 130 | + # Check data types are consistent |
| 131 | + assert np.issubdtype(grid.face_node_connectivity.dtype, np.integer) |
| 132 | + assert np.issubdtype(grid.node_lon.dtype, np.floating) |
| 133 | + assert np.issubdtype(grid.node_lat.dtype, np.floating) |
0 commit comments