Skip to content

Commit 33ce60b

Browse files
authored
Infer 1D bounds for nD variables. (#328)
1 parent 1fbc074 commit 33ce60b

File tree

2 files changed

+45
-6
lines changed

2 files changed

+45
-6
lines changed

cf_xarray/accessor.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -456,20 +456,32 @@ def wrapper(obj: DataArray | Dataset, key: str):
456456
}
457457

458458

459-
def _guess_bounds_dim(da):
459+
def _guess_bounds_dim(da, dim=None):
460460
"""
461461
Guess bounds values given a 1D coordinate variable.
462462
Assumes equal spacing on either side of the coordinate label.
463463
"""
464-
assert da.ndim == 1
464+
if dim is None:
465+
if da.ndim != 1:
466+
raise ValueError(
467+
f"If dim is None, variable {da.name} must be 1D. Received {da.ndim}D variable instead."
468+
)
469+
(dim,) = da.dims
470+
if dim not in da.dims:
471+
(dim,) = da.cf.axes[dim]
472+
if dim not in da.coords:
473+
raise NotImplementedError(
474+
"Adding bounds for unindexed dimensions is not supported currently."
475+
)
465476

466-
dim = da.dims[0]
467477
diff = da.diff(dim)
468478
lower = da - diff / 2
469479
upper = da + diff / 2
470480
bounds = xr.concat([lower, upper], dim="bounds")
471481

472-
first = (bounds.isel({dim: 0}) - diff[0]).assign_coords({dim: da[dim][0]})
482+
first = (bounds.isel({dim: 0}) - diff.isel({dim: 0})).assign_coords(
483+
{dim: da[dim][0]}
484+
)
473485
result = xr.concat([first, bounds], dim=dim)
474486

475487
return result
@@ -2144,7 +2156,7 @@ def get_bounds_dim_name(self, key: str) -> str:
21442156
assert self._obj.sizes[bounds_dim] in [2, 4]
21452157
return bounds_dim
21462158

2147-
def add_bounds(self, keys: str | Iterable[str]):
2159+
def add_bounds(self, keys: str | Iterable[str], *, dim=None):
21482160
"""
21492161
Returns a new object with bounds variables. The bounds values are guessed assuming
21502162
equal spacing on either side of a coordinate label.
@@ -2153,6 +2165,9 @@ def add_bounds(self, keys: str | Iterable[str]):
21532165
----------
21542166
keys : str or Iterable[str]
21552167
Either a single variable name or a list of variable names.
2168+
dim : str, optional
2169+
Core dimension along whch to estimate bounds. If None, ``keys``
2170+
must refer to 1D variables only.
21562171
21572172
Returns
21582173
-------
@@ -2198,7 +2213,9 @@ def add_bounds(self, keys: str | Iterable[str]):
21982213
bname = f"{var}_bounds"
21992214
if bname in obj.variables:
22002215
raise ValueError(f"Bounds variable name {bname!r} will conflict!")
2201-
obj.coords[bname] = _guess_bounds_dim(obj[var].reset_coords(drop=True))
2216+
obj.coords[bname] = _guess_bounds_dim(
2217+
obj[var].reset_coords(drop=True), dim=dim
2218+
)
22022219
obj[var].attrs["bounds"] = bname
22032220

22042221
return self._maybe_to_dataarray(obj)

cf_xarray/tests/test_accessor.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,28 @@ def test_add_bounds_multiple():
779779
assert {"x1_bounds", "x2_bounds"} <= set(multiple.cf.add_bounds("X").variables)
780780

781781

782+
def test_add_bounds_nd_variable():
783+
784+
ds = xr.Dataset(
785+
{"z": (("x", "y"), np.arange(12).reshape(4, 3))},
786+
coords={"x": np.arange(4), "y": np.arange(3)},
787+
)
788+
789+
expected = (
790+
xr.concat([ds.z - 1.5, ds.z + 1.5], dim="bounds")
791+
.rename("z_bounds")
792+
.transpose("bounds", "y", "x")
793+
)
794+
with pytest.raises(ValueError):
795+
ds.cf.add_bounds("z")
796+
797+
actual = ds.cf.add_bounds("z", dim="x").z_bounds.reset_coords(drop=True)
798+
xr.testing.assert_identical(expected, actual)
799+
800+
with pytest.raises(NotImplementedError):
801+
ds.drop_vars("x").cf.add_bounds("z", dim="x")
802+
803+
782804
def test_bounds():
783805
ds = airds.copy(deep=True).cf.add_bounds("lat")
784806

0 commit comments

Comments
 (0)