Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions test/core/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def test_open_dataset_single_argument_rejects_directory_grid(tmp_path):
"""Requires a separate data file for directory-based grids."""

with pytest.raises(
ValueError, match="single directory argument is not supported"
ValueError, match="single directory is not supported"
):
ux.open_dataset(tmp_path)

Expand Down Expand Up @@ -282,5 +282,5 @@ def test_open_multigrid_missing_grid_error(gridpath):
"""Requesting a missing grid should raise."""
grid_file = gridpath("scrip", "oasis", "grids.nc")

with pytest.raises(ValueError, match="Grid 'land' not found"):
with pytest.raises(ValueError, match="grid 'land' not found in the provided file"):
ux.open_multigrid(grid_file, gridnames=["land"])
4 changes: 2 additions & 2 deletions test/core/test_vector_calculus.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def test_divergence_input_validation(self, gridpath, datasetpath):
u_component = uxds['t2m']

# Test with non-UxDataArray
with pytest.raises(TypeError, match="other must be a UxDataArray"):
with pytest.raises(TypeError, match=r"u.divergence\(v\) expected UxDataArray v; got type\(v\)="):
u_component.divergence(np.array([1, 2, 3]))

# Test with different grids (create a simple test case)
Expand Down Expand Up @@ -455,7 +455,7 @@ def test_curl_input_validation(self, gridpath, datasetpath):
u_component = uxds['t2m']

# Test with non-UxDataArray
with pytest.raises(TypeError, match="other must be a UxDataArray"):
with pytest.raises(TypeError, match=r"u.curl\(v\) expected UxDataArray v; got type\(v\)="):
u_component.curl(np.array([1, 2, 3]))

# Test with different grids (create a simple test case)
Expand Down
10 changes: 5 additions & 5 deletions test/grid/integrate/test_zonal.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,11 @@ def test_conservative_step_size_validation(self, gridpath, datasetpath):
uxds = ux.open_dataset(grid_path, data_path)

# Test negative step size
with pytest.raises(ValueError, match="Step size must be positive"):
with pytest.raises(ValueError, match="Expected step>0"):
uxds["psi"].zonal_mean(lat=(-90, 90, -10), conservative=True)

# Test zero step size
with pytest.raises(ValueError, match="Step size must be positive"):
with pytest.raises(ValueError, match="Expected step>0"):
uxds["psi"].zonal_mean(lat=(-90, 90, 0), conservative=True)

def test_conservative_full_sphere_conservation(self, gridpath, datasetpath):
Expand Down Expand Up @@ -381,7 +381,7 @@ def test_non_face_centered_raises(self, gridpath, datasetpath):
uxda = ux.UxDataArray(
np.zeros(uxgrid.n_node), dims=["n_node"], uxgrid=uxgrid
)
with pytest.raises(DataCenteringError, match="face-centered"):
with pytest.raises(DataCenteringError, match="non-face_centered data is not currently supported"):
uxda.zonal_anomaly()

def test_invalid_lat_input_raises(self):
Expand All @@ -390,9 +390,9 @@ def test_invalid_lat_input_raises(self):
uxda = ux.UxDataArray(
np.zeros(uxgrid.n_face), dims=["n_face"], uxgrid=uxgrid
)
with pytest.raises(ValueError, match="Step size"):
with pytest.raises(ValueError, match="Expected step>0"):
uxda.zonal_anomaly(lat=(-90, 90, 0))
with pytest.raises(ValueError, match="Step size"):
with pytest.raises(ValueError, match="Expected step>0"):
uxda.zonal_anomaly(lat=(-90, 90, -1))
with pytest.raises(ValueError):
uxda.zonal_anomaly(lat=[42.0]) # too few edges
Expand Down
2 changes: 1 addition & 1 deletion test/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def test_to_raster_reuse_mapping(gridpath, tmpdir):

# Modified pixel mapping raises error
pixel_mapping.attrs["ax_shape"] = (2, 3)
with pytest.raises(ValueError, match=r"Pixel mapping incompatible with ax\. shape \(2, 3\) !="):
with pytest.raises(ValueError, match=r"Provided pixel_mapping values incompatible with ax raster attrs: shape \(2, 3\) !="):
_ = uxds['bottomDepth'].to_raster(
ax=ax, pixel_mapping=pixel_mapping
)
Expand Down
4 changes: 3 additions & 1 deletion uxarray/core/accessors.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ def wrapped(*args, **kwargs):

def _process_result(self, result):
"""Process method results to preserve uxgrid. To be overridden by subclasses."""
raise NotImplementedError("Subclasses must implement _process_result")
raise NotImplementedError(
f"_process_result, for BaseAccessor subclass {type(self).__name__}"
)

# Delegation for common dunder methods
def __iter__(self):
Expand Down
40 changes: 16 additions & 24 deletions uxarray/core/aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ def _uxda_grid_aggregate(uxda, destination, aggregation, **kwargs):
UxDataArray."""
if destination is None:
raise ValueError(
"Attempting to perform a topological aggregation, but no destination was provided."
)
f"Missing destination (got destination=None) in topological_{aggregation})."
) # e.g. aggregation will be something like "mean", "min", "any", ...

if uxda._node_centered():
# aggregation of a node-centered data variable
Expand All @@ -35,38 +35,26 @@ def _uxda_grid_aggregate(uxda, destination, aggregation, **kwargs):
return _node_to_edge_aggregation(uxda, aggregation, kwargs)
else:
raise DataCenteringError(
f"Invalid destination for a node-centered data variable. Expected"
f"one of ['face', 'edge' but received {destination}"
f"Node-centered data requires destination='face' or 'edge'; "
f"got destination={destination!r}, during topological_{aggregation}."
)

elif uxda._edge_centered():
# aggregation of an edge-centered data variable
raise NotImplementedError(
"Aggregation of edge-centered data variables is not yet supported."
)
# if destination == "node":
# pass
# elif destination == "face":
# pass
# else:
# raise ValueError("TODO: )

elif uxda._face_centered():
# aggregation of a face-centered data variable
raise NotImplementedError(
"Aggregation of face-centered data variables is not yet supported."
)
# if destination == "node":
# pass
# elif destination == "edge":
# pass
# else:
# raise ValueError("TODO: ")

else:
raise DataCenteringError(
"Invalid data mapping. Data variable is expected to be mapped to either the "
"nodes, faces, or edges of the source grid."
f"topological_{aggregation} expected node_centered, edge_centered, or face_centered data; "
f"got data with uxda.data_location={uxda.data_location!r} with dimensions {uxda.dims!r}."
)


Expand All @@ -76,8 +64,8 @@ def _node_to_face_aggregation(uxda, aggregation, aggregation_func_kwargs):

if not uxda._node_centered():
raise DataCenteringError(
f"Data Variable must be mapped to the corner nodes of each face, with dimension "
f"{uxda.uxgrid.n_face}."
f"Expected node_centered data; got data with uxda.data_location={uxda.data_location!r} "
f"with dimensions {uxda.dims!r}, during _node_to_face_aggregation."
)

if isinstance(uxda.data, np.ndarray):
Expand All @@ -91,7 +79,9 @@ def _node_to_face_aggregation(uxda, aggregation, aggregation_func_kwargs):
uxda, NUMPY_AGGREGATIONS[aggregation], aggregation_func_kwargs
)
else:
raise TypeError
raise TypeError(
f"Expected numpy or dask array; got type(uxda.data)={type(uxda.data)}."
)

return uxarray.core.dataarray.UxDataArray(
uxgrid=uxda.uxgrid,
Expand Down Expand Up @@ -146,8 +136,8 @@ def _node_to_edge_aggregation(uxda, aggregation, aggregation_func_kwargs):

if not uxda._node_centered():
raise DataCenteringError(
f"Data Variable must be mapped to the corner nodes of each face, with dimension "
f"{uxda.uxgrid.n_face}."
f"Expected node_centered data; got data with uxda.data_location={uxda.data_location!r} "
f"with dimensions {uxda.dims!r}, during _node_to_edge_aggregation."
)

if isinstance(uxda.data, np.ndarray):
Expand All @@ -161,7 +151,9 @@ def _node_to_edge_aggregation(uxda, aggregation, aggregation_func_kwargs):
uxda, NUMPY_AGGREGATIONS[aggregation], aggregation_func_kwargs
)
else:
raise TypeError
raise TypeError(
f"Expected numpy or dask array; got type(uxda.data)={type(uxda.data)}."
)

return uxarray.core.dataarray.UxDataArray(
uxgrid=uxda.uxgrid,
Expand Down
43 changes: 33 additions & 10 deletions uxarray/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,14 @@ def open_grid(
if os.path.isfile(nod2d_path) and os.path.isfile(elem2d_path):
grid = Grid.from_dataset(grid_filename_or_obj)
else:
_missing = []
if not os.path.isfile(nod2d_path):
_missing.append("'nod2d.out'")
if not os.path.isfile(elem2d_path):
_missing.append("'elem2d.out'")
raise FileNotFoundError(
f"The directory '{grid_filename_or_obj}' must contain both 'nod2d.out' and 'elem2d.out'."
"open_grid(directory) expects FESOM2 ASCII dataset with 'nod2d.out' and 'elem2d.out' files, but "
f"got directory={os.path.abspath(grid_filename_or_obj)!r}, which is missing {' and '.join(_missing)}."
)

elif isinstance(grid_filename_or_obj, dict):
Expand Down Expand Up @@ -188,6 +194,12 @@ def open_multigrid(
mask_ds = xr.open_dataset(mask_filename)
mask_ds_opened = True

# human-readable str telling what was provided to open_multigrid(). Useful for error messages.
if isinstance(grid_filename_or_obj, (str, os.PathLike)):
_provided_input_str = f"file, {os.path.abspath(grid_filename_or_obj)!r}"
else:
_provided_input_str = str(type(grid_filename_or_obj))

try:
active_value_map: Mapping[str, MaskValue] | None = (
mask_active_value if isinstance(mask_active_value, Mapping) else None
Expand Down Expand Up @@ -236,8 +248,8 @@ def _active_mask_values_for_grid(grid_name: str) -> np.ndarray:
if format_type == "single_scrip":
if gridnames is not None and "grid" not in gridnames:
raise ValueError(
f"Requested grids {gridnames} not found. "
"This file contains a single grid named 'grid'."
f"Requested grids (gridnames={gridnames}) not found in the provided {_provided_input_str}, "
"in open_multigrid(). Only 'grid' is available in single-grid SCRIP files."
)
grid_ds_ugrid, source_dims_dict = _read_scrip(grid_ds)
return {
Expand All @@ -249,7 +261,9 @@ def _active_mask_values_for_grid(grid_name: str) -> np.ndarray:
}

if not grids_dict:
raise GridInvalidError(f"No grids detected in file: {grid_filename_or_obj}")
raise GridInvalidError(
f"Failed to detect any grids in the provided {_provided_input_str}, in open_multigrid()."
)

available_grids = list(grids_dict.keys())

Expand All @@ -265,7 +279,8 @@ def _active_mask_values_for_grid(grid_name: str) -> np.ndarray:
for name in requested:
if name not in grids_dict:
raise ValueError(
f"Grid '{name}' not found. Available grids: {available_grids}"
f"open_multigrid() grid '{name}' not found in the provided {_provided_input_str}, "
f"in open_multigrid(). Available grids: {available_grids}"
)
grids_to_load.append(name)

Expand Down Expand Up @@ -293,9 +308,14 @@ def _active_mask_values_for_grid(grid_name: str) -> np.ndarray:
active_indices = np.flatnonzero(active_mask)
grid = grid.isel(n_face=active_indices)
else:
_provided_mask_str = (
f"file, {os.path.abspath(mask_filename)!r}"
if isinstance(mask_filename, (str, os.PathLike))
else str(type(mask_filename))
)
warn(
f"Mask variable '{mask_var}' not found in mask file; "
f"grid '{grid_name}' will be returned without masking."
f"Mask variable {mask_var!r} not found in the provided mask {_provided_mask_str}. "
f"Grid {grid_name!r} will be returned without masking."
)

loaded_grids[grid_name] = grid
Expand Down Expand Up @@ -426,7 +446,8 @@ def open_dataset(
if isinstance(grid_filename_or_obj, (str, os.PathLike)):
if os.path.isdir(grid_filename_or_obj):
raise ValueError(
"ux.open_dataset() with a single directory argument is not supported. "
"ux.open_dataset(arg0) with no other arguments and arg0 a single directory is not supported, "
f"but got arg0 indicating path to directory: {os.path.abspath(grid_filename_or_obj)!r}."
"Supply a path to a grid file instead. Directory-based grids (e.g. a "
"FESOM2 ASCII grid) are only recognized when a separate data file is "
"also provided, i.e. ux.open_dataset(grid_directory, data_file)."
Expand All @@ -440,8 +461,10 @@ def open_dataset(
elif isinstance(grid_filename_or_obj, xr.Dataset):
ds = grid_filename_or_obj
else:
raise ValueError(
"If filename_or_obj is omitted, grid_filename_or_obj must be a file path or xarray.Dataset."
raise TypeError(
"Expected grid_filename_or_obj to be a file path or xarray.Dataset when filename_or_obj "
f"is not provided, but got type(grid_filename_or_obj)={type(grid_filename_or_obj)}, "
"in ux.open_dataset(grid_filename_or_obj, filename_or_obj=None)."
)

uxgrid, _ = _get_grid(ds, chunks, chunk_grid, use_dual, grid_kwargs, **kwargs)
Expand Down
Loading
Loading