From 8f717b739ce5b0874d481361120b798e8500d51c Mon Sep 17 00:00:00 2001 From: Sam Evans <47793072+Sevans711@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:17:23 -0400 Subject: [PATCH 1/4] clean up confusing/misleading error types as described by --- uxarray/core/aggregation.py | 4 ++-- uxarray/core/dataarray.py | 4 ++-- uxarray/core/dataset.py | 2 +- uxarray/grid/bounds.py | 4 ++-- uxarray/grid/grid.py | 14 +++++++------- uxarray/grid/neighbors.py | 13 +++++++------ uxarray/grid/slice.py | 4 ++-- uxarray/grid/utils.py | 2 +- uxarray/io/_esmf.py | 2 +- uxarray/io/_icon.py | 2 +- uxarray/io/_scrip.py | 2 +- uxarray/io/utils.py | 3 ++- uxarray/remap/yac.py | 2 +- 13 files changed, 30 insertions(+), 28 deletions(-) diff --git a/uxarray/core/aggregation.py b/uxarray/core/aggregation.py index b78bd6dbe..4f75aaeb8 100644 --- a/uxarray/core/aggregation.py +++ b/uxarray/core/aggregation.py @@ -90,7 +90,7 @@ def _node_to_face_aggregation(uxda, aggregation, aggregation_func_kwargs): uxda, NUMPY_AGGREGATIONS[aggregation], aggregation_func_kwargs ) else: - raise ValueError + raise TypeError return uxarray.core.dataarray.UxDataArray( uxgrid=uxda.uxgrid, @@ -158,7 +158,7 @@ def _node_to_edge_aggregation(uxda, aggregation, aggregation_func_kwargs): uxda, NUMPY_AGGREGATIONS[aggregation], aggregation_func_kwargs ) else: - raise ValueError + raise TypeError return uxarray.core.dataarray.UxDataArray( uxgrid=uxda.uxgrid, diff --git a/uxarray/core/dataarray.py b/uxarray/core/dataarray.py index 28fa22886..cf9419775 100644 --- a/uxarray/core/dataarray.py +++ b/uxarray/core/dataarray.py @@ -860,7 +860,7 @@ def zonal_anomaly(self, lat=(-90, 90, 10), conservative: bool = False): elif isinstance(lat, (list, np.ndarray)): edges = np.asarray(lat, dtype=float) else: - raise ValueError( + raise TypeError( "Invalid value for 'lat'. Must be a tuple (start, end, step) or array-like band edges." ) @@ -2167,7 +2167,7 @@ def get_dual(self): """ if _check_duplicate_nodes_indices(self.uxgrid): - raise RuntimeError("Duplicate nodes found, cannot construct dual") + raise GridInvalidError("Duplicate nodes found, cannot construct dual") if self.uxgrid.partial_sphere_coverage: warn( diff --git a/uxarray/core/dataset.py b/uxarray/core/dataset.py index 42057a41c..41194dfe2 100644 --- a/uxarray/core/dataset.py +++ b/uxarray/core/dataset.py @@ -708,7 +708,7 @@ def get_dual(self): """ if _check_duplicate_nodes_indices(self.uxgrid): - raise RuntimeError("Duplicate nodes found, cannot construct dual") + raise GridInvalidError("Duplicate nodes found, cannot construct dual") if self.uxgrid.partial_sphere_coverage: warn( diff --git a/uxarray/grid/bounds.py b/uxarray/grid/bounds.py index 626946244..573b6b095 100644 --- a/uxarray/grid/bounds.py +++ b/uxarray/grid/bounds.py @@ -416,7 +416,7 @@ def insert_pt_in_latlonbox(old_box, new_pt, is_lon_periodic=True): else: # Validate longitude point if not np.isnan(lon_pt) and (lon_pt < 0.0 or lon_pt > 2.0 * np.pi): - raise Exception("Longitude point out of range") + raise ValueError("Longitude point out of range") # Check for pole points is_pole_point = False @@ -465,7 +465,7 @@ def insert_pt_in_latlonbox(old_box, new_pt, is_lon_periodic=True): # Ensure widths are non-negative if (d_width_a < 0.0) or (d_width_b < 0.0): - raise Exception( + raise AssertionError( "Logic error in longitude box width calculation" ) diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 6fa02b069..3c86890f2 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -17,7 +17,7 @@ # Import the utility function for opening datasets with fallback from uxarray.core.utils import _open_dataset_with_fallback from uxarray.cross_sections import GridCrossSectionAccessor -from uxarray.errors import DataCenteringError, GridInvalidError +from uxarray.errors import DataCenteringError, DimensionError, GridInvalidError from uxarray.formatting_html import grid_repr from uxarray.grid.area import _get_all_face_area_from_coords from uxarray.grid.bounds import _populate_face_bounds @@ -569,7 +569,7 @@ def from_face_vertices( Indicates whether the inputted vertices are in lat/lon, with units in degrees """ if not isinstance(face_vertices, (list, tuple, np.ndarray)): - raise ValueError("Input must be either a list, tuple, or np.ndarray") + raise TypeError("Input must be either a list, tuple, or np.ndarray") face_vertices = np.asarray(face_vertices) @@ -580,7 +580,7 @@ def from_face_vertices( grid_ds = _read_face_vertices(np.array([face_vertices]), latlon) else: - raise RuntimeError( + raise DimensionError( f"Invalid Input Dimension: {face_vertices.ndim}. Expected dimension should be " f"3: [n_face, n_node, two/three] or 2 when only " f"one face is passed in." @@ -633,7 +633,7 @@ def validate(self, check_duplicates=True): print("Mesh validation successful.") return True else: - raise RuntimeError("Mesh validation failed.") + raise GridInvalidError("Mesh validation failed.") def construct_face_centers(self, method="cartesian average"): """Constructs face centers, this method provides users direct control @@ -1612,7 +1612,7 @@ def boundary_node_indices(self): """Indices of nodes that border regions not covered by any geometry (holes) in a partial grid.""" if "boundary_node_indices" not in self._ds: - raise ValueError + raise NotImplementedError return self._ds["boundary_node_indices"] @@ -1663,7 +1663,7 @@ def inverse_indices(self) -> xr.Dataset: if self.is_subset: return self._inverse_indices else: - raise Exception( + raise AttributeError( "Grid is not a subset, therefore no inverse face indices exist" ) @@ -2534,7 +2534,7 @@ def get_dual(self, check_duplicate_nodes: bool = False): if check_duplicate_nodes: if _check_duplicate_nodes_indices(self): # TODO: This is very slow - raise RuntimeError("Duplicate nodes found, cannot construct dual") + raise GridInvalidError("Duplicate nodes found, cannot construct dual") # Get dual mesh node face connectivity dual_node_face_conn = construct_dual(grid=self) diff --git a/uxarray/grid/neighbors.py b/uxarray/grid/neighbors.py index 1c4d4f145..195bf4138 100644 --- a/uxarray/grid/neighbors.py +++ b/uxarray/grid/neighbors.py @@ -4,6 +4,7 @@ from numpy import deg2rad from uxarray.constants import ERROR_TOLERANCE, INT_DTYPE, INT_FILL_VALUE +from uxarray.errors import DimensionError class KDTree: @@ -96,7 +97,7 @@ def _build_from_nodes(self): ).T else: - raise TypeError( + raise ValueError( f"Unknown coordinate_system, {self.coordinate_system}, use either 'cartesian' or " f"'spherical'" ) @@ -192,7 +193,7 @@ def _current_tree(self): elif self._coordinates == "edge centers": _tree = self._tree_from_edge_centers else: - raise TypeError( + raise ValueError( f"Unknown coordinates location, {self._coordinates}, use either 'nodes', 'face centers', " f"or 'edge centers'" ) @@ -1010,13 +1011,13 @@ def _prepare_xy_for_query(xy, use_radians, distance_metric): # expected shape is [n_pairs, 2] if xy.shape[1] == 3: - raise AssertionError( + raise DimensionError( "The dimension of each coordinate pair must be two (lon, lat). Did you attempt to query using Cartesian " "(x, y, z) coordinates?" ) if xy.shape[1] != 2: - raise AssertionError( + raise DimensionError( "The dimension of each coordinate pair must be two (lon, lat).)" ) @@ -1044,13 +1045,13 @@ def _prepare_xyz_for_query(xyz): # expected shape is [n_pairs, 3] if xyz.shape[1] == 2: - raise AssertionError( + raise DimensionError( "The dimension of each coordinate pair must be three (x, y, z). Did you attempt to query using latlon " "(lat, lon) coordinates?" ) if xyz.shape[1] != 3: - raise AssertionError( + raise DimensionError( "The dimension of each coordinate pair must be three (x, y, z).)" ) diff --git a/uxarray/grid/slice.py b/uxarray/grid/slice.py index 3bc97b9d1..3f2e509d4 100644 --- a/uxarray/grid/slice.py +++ b/uxarray/grid/slice.py @@ -32,7 +32,7 @@ def _slice_node_indices( """ if inclusive is False: - raise ValueError("Exclusive slicing is not yet supported.") + raise NotImplementedError("Exclusive slicing is not yet supported.") # faces that saddle nodes given in 'indices' face_indices = np.unique(grid.node_face_connectivity.values[indices].ravel()) @@ -62,7 +62,7 @@ def _slice_edge_indices( """ if inclusive is False: - raise ValueError("Exclusive slicing is not yet supported.") + raise NotImplementedError("Exclusive slicing is not yet supported.") # faces that saddle nodes given in 'indices' face_indices = np.unique(grid.edge_face_connectivity.values[indices].ravel()) diff --git a/uxarray/grid/utils.py b/uxarray/grid/utils.py index 9287bc325..d70962273 100644 --- a/uxarray/grid/utils.py +++ b/uxarray/grid/utils.py @@ -439,7 +439,7 @@ def make_setter(key: str): def setter(self, value): if not isinstance(value, xr.DataArray): - raise ValueError(f"{key} must be an xr.DataArray") + raise TypeError(f"{key} must be an xr.DataArray") self._ds[key] = value return setter diff --git a/uxarray/io/_esmf.py b/uxarray/io/_esmf.py index 195f0c91e..538fbf171 100644 --- a/uxarray/io/_esmf.py +++ b/uxarray/io/_esmf.py @@ -76,7 +76,7 @@ def _read_esmf(in_ds): ) else: - raise ValueError( + raise NotImplementedError( "Reading in ESMF grids with Cartesian coordinates not yet supported" ) diff --git a/uxarray/io/_icon.py b/uxarray/io/_icon.py index 9921535e0..149e5831e 100644 --- a/uxarray/io/_icon.py +++ b/uxarray/io/_icon.py @@ -156,4 +156,4 @@ def _read_icon(ext_ds, use_dual=False): if not use_dual: return _primal_to_ugrid(ext_ds, out_ds) else: - raise ValueError("Conversion of the ICON Dual mesh is not yet supported.") + raise NotImplementedError("Conversion of the ICON Dual mesh is not yet supported.") diff --git a/uxarray/io/_scrip.py b/uxarray/io/_scrip.py index 21edb1377..b24edac60 100644 --- a/uxarray/io/_scrip.py +++ b/uxarray/io/_scrip.py @@ -120,7 +120,7 @@ def _to_ugrid(in_ds, out_ds): ) else: - raise Exception("Structured scrip files are not yet supported") + raise NotImplementedError("Structured scrip files are not yet supported") # populate source dims source_dims_dict[in_ds["grid_center_lon"].dims[0]] = "n_face" diff --git a/uxarray/io/utils.py b/uxarray/io/utils.py index 59031e5db..2484172d1 100644 --- a/uxarray/io/utils.py +++ b/uxarray/io/utils.py @@ -1,6 +1,7 @@ import numpy as np import xarray as xr +from uxarray.errors import GridInvalidError from uxarray.io._esmf import _esmf_to_ugrid_dims from uxarray.io._icon import _icon_to_ugrid_dims from uxarray.io._mpas import _mpas_to_ugrid_dims @@ -120,7 +121,7 @@ def _parse_grid_type(dataset): mesh_type = "Structured" return mesh_type, lon_name, lat_name else: - raise RuntimeError("Failed to parse uxgrid information from xarray.Dataset.") + raise GridInvalidError("Failed to parse uxgrid information from xarray.Dataset.") return mesh_type, None, None diff --git a/uxarray/remap/yac.py b/uxarray/remap/yac.py index 2ec7ed623..93d1d4fea 100644 --- a/uxarray/remap/yac.py +++ b/uxarray/remap/yac.py @@ -122,7 +122,7 @@ def _get_lon_lat(grid, dim: str) -> tuple[np.ndarray, np.ndarray]: lon = getattr(grid, lon_attr, None) lat = getattr(grid, lat_attr, None) if lon is None or lat is None: - raise ValueError( + raise AttributeError( f"Grid does not provide {lon_attr}/{lat_attr} required for YAC remapping." ) return np.deg2rad(np.asarray(lon.values, dtype=np.float64)), np.deg2rad( From 877af8e1f2362a9e6e14a635b04a1feeacf3a2f6 Mon Sep 17 00:00:00 2001 From: Sam Evans <47793072+Sevans711@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:22:22 -0400 Subject: [PATCH 2/4] import uxarray.errors module in top __init__.py This means users can reliably refer to, e.g., ux.errors.DimensionError, instead of possibly needing to import uxarray.errors specifically and then do uxarray.errors.DimensionError. --- uxarray/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uxarray/__init__.py b/uxarray/__init__.py index c6cb8ec3d..50f0bade6 100644 --- a/uxarray/__init__.py +++ b/uxarray/__init__.py @@ -1,3 +1,4 @@ +from . import errors from . import tutorial from .constants import INT_DTYPE, INT_FILL_VALUE from .core.api import ( @@ -37,4 +38,5 @@ "INT_DTYPE", "INT_FILL_VALUE", "Grid", + "errors", ) From 52f46f365a2d6a6c9b3a229f7ce05cc5665115df Mon Sep 17 00:00:00 2001 From: Sam Evans <47793072+Sevans711@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:26:31 -0400 Subject: [PATCH 3/4] forgot pre-commit ruff formatting --- uxarray/__init__.py | 3 +-- uxarray/io/_icon.py | 4 +++- uxarray/io/utils.py | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/uxarray/__init__.py b/uxarray/__init__.py index 50f0bade6..677e2f7b6 100644 --- a/uxarray/__init__.py +++ b/uxarray/__init__.py @@ -1,5 +1,4 @@ -from . import errors -from . import tutorial +from . import errors, tutorial from .constants import INT_DTYPE, INT_FILL_VALUE from .core.api import ( concat, diff --git a/uxarray/io/_icon.py b/uxarray/io/_icon.py index 149e5831e..0f672e01a 100644 --- a/uxarray/io/_icon.py +++ b/uxarray/io/_icon.py @@ -156,4 +156,6 @@ def _read_icon(ext_ds, use_dual=False): if not use_dual: return _primal_to_ugrid(ext_ds, out_ds) else: - raise NotImplementedError("Conversion of the ICON Dual mesh is not yet supported.") + raise NotImplementedError( + "Conversion of the ICON Dual mesh is not yet supported." + ) diff --git a/uxarray/io/utils.py b/uxarray/io/utils.py index 2484172d1..bf1e1ed77 100644 --- a/uxarray/io/utils.py +++ b/uxarray/io/utils.py @@ -121,7 +121,9 @@ def _parse_grid_type(dataset): mesh_type = "Structured" return mesh_type, lon_name, lat_name else: - raise GridInvalidError("Failed to parse uxgrid information from xarray.Dataset.") + raise GridInvalidError( + "Failed to parse uxgrid information from xarray.Dataset." + ) return mesh_type, None, None From 5416e7e2e5deed64fdab041192627be85134fb8f Mon Sep 17 00:00:00 2001 From: Sam Evans <47793072+Sevans711@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:28:45 -0400 Subject: [PATCH 4/4] clean up confusing error types - fix in test suite --- test/core/test_api.py | 2 +- test/grid/grid/test_core.py | 2 +- test/io/test_utils.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/core/test_api.py b/test/core/test_api.py index 0d4de8650..e9de19936 100644 --- a/test/core/test_api.py +++ b/test/core/test_api.py @@ -85,7 +85,7 @@ def test_open_dataset_single_argument_rejects_invalid_combined_file(datasetpath) data_path = datasetpath("ugrid", "outCSne30", "outCSne30_var2.nc") - with pytest.raises(RuntimeError, match="Failed to parse uxgrid information from xarray.Dataset."): + with pytest.raises(ux.errors.GridInvalidError, match="Failed to parse uxgrid information from xarray.Dataset."): ux.open_dataset(data_path) diff --git a/test/grid/grid/test_core.py b/test/grid/grid/test_core.py index 9f2326d11..da5882bea 100644 --- a/test/grid/grid/test_core.py +++ b/test/grid/grid/test_core.py @@ -131,5 +131,5 @@ def test_dual_mesh_mpas(gridpath): def test_dual_duplicate(gridpath): """Test dual mesh creation with duplicate grids.""" dataset = ux.open_dataset(gridpath("ugrid", "geoflow-small", "grid.nc"), gridpath("ugrid", "geoflow-small", "grid.nc")) - with pytest.raises(RuntimeError): + with pytest.raises(ux.errors.GridInvalidError): dataset.get_dual() diff --git a/test/io/test_utils.py b/test/io/test_utils.py index d93222991..5ed560cc6 100644 --- a/test/io/test_utils.py +++ b/test/io/test_utils.py @@ -2,6 +2,7 @@ import pytest import xarray as xr +from uxarray.errors import GridInvalidError from uxarray.io.utils import _parse_grid_type @@ -61,5 +62,5 @@ def test_parse_grid_type_detects_structured_grid(): ], ) def test_parse_grid_type_rejects_incomplete_format_signals(dataset): - with pytest.raises(RuntimeError, match="Failed to parse uxgrid information from xarray.Dataset."): + with pytest.raises(GridInvalidError, match="Failed to parse uxgrid information from xarray.Dataset."): _parse_grid_type(dataset)