From 641b9f2ddd5da9c0e52f2f8bf2823f2c5a1e47e7 Mon Sep 17 00:00:00 2001 From: pochedls Date: Mon, 15 Aug 2022 20:28:57 -0700 Subject: [PATCH] Modify logic to not throw error for singleton coordinates (with no bounds) (#313) * do not throw valuerror if coord length <= 1 in order to accommodate single timestep datasets; simply do not add bounds and warn user * update docstring * add test and ensure warning is suppressed * Refactor `add_missing_bounds()` - Update try and except statements to skip multidimensional or single dimension bounds - Reduce nesting for cleaner code - Update docstrings with explicit criteria on when bounds can be added for coordinates * Update tests/test_bounds.py * Add continue statement if bounds are found * Update xcdat/bounds.py Co-authored-by: Tom Vo --- tests/test_bounds.py | 22 +++++++++++++--- xcdat/bounds.py | 63 ++++++++++++++++++++++++++++++++------------ 2 files changed, 65 insertions(+), 20 deletions(-) diff --git a/tests/test_bounds.py b/tests/test_bounds.py index 52a95d68..0e101cf9 100644 --- a/tests/test_bounds.py +++ b/tests/test_bounds.py @@ -80,6 +80,25 @@ def test_adds_bounds_to_the_dataset_skips_nondimensional_axes(self): # and added height coordinate assert result.identical(ds) + def test_skips_adding_bounds_for_coords_that_are_multidimensional_or_len_of_1(self): + # Multidimensional + lat = xr.DataArray( + data=np.array([[0, 1, 2], [3, 4, 5]]), + dims=["placeholder_1", "placeholder_2"], + attrs={"units": "degrees_north", "axis": "Y"}, + ) + # Length <=1 + lon = xr.DataArray( + data=np.array([0]), + dims=["lon"], + attrs={"units": "degrees_east", "axis": "X"}, + ) + ds = xr.Dataset(coords={"lat": lat, "lon": lon}) + + result = ds.bounds.add_missing_bounds("Y") + + assert result.identical(ds) + class TestGetBounds: @pytest.fixture(autouse=True) @@ -166,9 +185,6 @@ def test_raises_errors_for_data_dim_and_length(self): # If coords dimensions does not equal 1. with pytest.raises(ValueError): ds.bounds.add_bounds("Y") - # If coords are length of <=1. - with pytest.raises(ValueError): - ds.bounds.add_bounds("X") def test_raises_error_if_lat_coord_var_units_is_not_in_degrees(self): lat = xr.DataArray( diff --git a/xcdat/bounds.py b/xcdat/bounds.py index b9a50537..06f55082 100644 --- a/xcdat/bounds.py +++ b/xcdat/bounds.py @@ -117,8 +117,16 @@ def keys(self) -> List[str]: def add_missing_bounds(self, width: float = 0.5) -> xr.Dataset: """Adds missing coordinate bounds for supported axes in the Dataset. - This function loops through the Dataset's axes and adds coordinate - bounds for an axis that doesn't have any. + This function loops through the Dataset's axes and attempts to adds + bounds to its coordinates if they don't exist. The coordinates must meet + the following criteria in order to add bounds: + + 1. The axis for the coordinates are "X", "Y", "T", or "Z" + 2. Coordinates are a single dimension, not multidimensional + 3. Coordinates are a length > 1 (not singleton) + 4. Bounds must not already exist. + * Determined by attempting to map the coordinate variable's + "bounds" attr (if set) to the bounds data variable of the same key. Parameters ---------- @@ -133,24 +141,31 @@ def add_missing_bounds(self, width: float = 0.5) -> xr.Dataset: axes = CF_NAME_MAP.keys() for axis in axes: - coord_var = None + # Check if the axis coordinates can be mapped to. + try: + get_axis_coord(self._dataset, axis) + except KeyError: + continue + # Determine if the axis is also a dimension by determining if there + # is overlap between the CF axis names and the dimension names. If + # not, skip over axis for validation. + if len(set(CF_NAME_MAP[axis]) & set(self._dataset.dims.keys())) == 0: + continue + + # Check if bounds also exist using the "bounds" attribute. + # Otherwise, try to add bounds if it meets the function's criteria. try: - coord_var = get_axis_coord(self._dataset, axis) + self.get_bounds(axis) + continue except KeyError: pass - # determine if the axis is also a dimension by determining - # if there is overlap between the CF axis names and the dimension - # names. If not, skip over axis for validation. - if len(set(CF_NAME_MAP[axis]) & set(self._dataset.dims.keys())) == 0: + try: + self._dataset = self.add_bounds(axis, width) + except ValueError: continue - if coord_var is not None: - try: - self.get_bounds(axis) - except KeyError: - self._dataset = self.add_bounds(axis, width) return self._dataset def get_bounds(self, axis: CFAxisName) -> xr.DataArray: @@ -201,6 +216,16 @@ def get_bounds(self, axis: CFAxisName) -> xr.DataArray: def add_bounds(self, axis: CFAxisName, width: float = 0.5) -> xr.Dataset: """Add bounds for an axis using its coordinate points. + The coordinates must meet the following criteria in order to add + bounds: + + 1. The axis for the coordinates are "X", "Y", "T", or "Z" + 2. Coordinates are a single dimension, not multidimensional + 3. Coordinates are a length > 1 (not singleton) + 4. Bounds must not already exist. + * Determined by attempting to map the coordinate variable's + "bounds" attr (if set) to the bounds data variable of the same key. + Parameters ---------- axis : CFAxisName @@ -252,8 +277,6 @@ def _add_bounds(self, axis: CFAxisName, width: float = 0.5) -> xr.Dataset: ------ ValueError If coords dimensions does not equal 1. - ValueError - If coords are length of <=1. Notes ----- @@ -272,9 +295,15 @@ def _add_bounds(self, axis: CFAxisName, width: float = 0.5) -> xr.Dataset: # Validate coordinate shape and dimensions if coord_var.ndim != 1: - raise ValueError("Cannot generate bounds for multidimensional coordinates.") + raise ValueError( + f"Cannot generate bounds for coordinate variable '{coord_var.name}'" + " because it is multidimensional coordinates." + ) if coord_var.shape[0] <= 1: - raise ValueError("Cannot generate bounds for a coordinate of length <= 1.") + raise ValueError( + f"Cannot generate bounds for coordinate variable '{coord_var.name}'" + " which has a length <= 1." + ) # Retrieve coordinate dimension to calculate the diffs between points. dim = coord_var.dims[0]