Skip to content

Commit 900c408

Browse files
committed
Add new NEMODataArray.clip() method to select data within a specified longitude-latitude range + unit tests. Add further NEMODataArray binary operators.
1 parent aaa65c8 commit 900c408

2 files changed

Lines changed: 134 additions & 0 deletions

File tree

nemo_cookbook/nemodataarray.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import matplotlib.pyplot as plt
2424
import numpy as np
2525
import xarray as xr
26+
from xarray.core import nputils
2627

2728
if TYPE_CHECKING:
2829
# Avoid circular import at runtime:
@@ -308,6 +309,73 @@ def apply_mask(
308309
result = self._da.where(mask, drop=drop)
309310

310311
return self._wrap(result)
312+
313+
def clip(
314+
self,
315+
bbox: tuple[float | int, float | int, float | int, float | int],
316+
) -> Self:
317+
"""
318+
Clip variable defined on a NEMO model grid to specified
319+
longitude and latitude range.
320+
321+
Parameters
322+
----------
323+
bbox : tuple
324+
Bounding box in the form (lon_min, lon_max, lat_min, lat_max).
325+
326+
Returns
327+
-------
328+
NEMODataArray
329+
Variable defined on a NEMO model grid clipped to bounding box.
330+
331+
Examples
332+
--------
333+
Clip sea surface temperature `tos_con` defined on T-points in a NEMO
334+
model parent domain in the bounding box (-40°E, 10°E, 35°N, 60°N):
335+
336+
>>> nemo['gridT/tos_con'].clip(bbox=(-40, 10, 35, 60))
337+
338+
See Also
339+
--------
340+
sel_like
341+
"""
342+
# -- Validate Inputs -- #
343+
if not isinstance(bbox, tuple) or len(bbox) != 4:
344+
raise ValueError(
345+
"bounding box must be a tuple (lon_min, lon_max, lat_min, lat_max)."
346+
)
347+
348+
# -- Clip data to bounding box & return NEMODataArray -- #
349+
# Define longitude & latitude coordinates of NEMO model grid:
350+
glam = self[f"{self._dom_prefix}glam{self._grid_suffix}"]
351+
gphi = self[f"{self._dom_prefix}gphi{self._grid_suffix}"]
352+
353+
# Define bbox mask:
354+
mask = (
355+
(glam >= bbox[0])
356+
& (glam <= bbox[1])
357+
& (gphi >= bbox[2])
358+
& (gphi <= bbox[3])
359+
)
360+
361+
# Find rows/columns containing at least one valid grid point:
362+
rows = mask.any(dim=self.i_name)
363+
cols = mask.any(dim=self.j_name)
364+
j_idx = np.where(rows.compute())[0]
365+
i_idx = np.where(cols.compute())[0]
366+
367+
if len(j_idx) == 0 or len(i_idx) == 0:
368+
raise ValueError(f"No {self._grid[-1]}-grid points found inside specified bbox.")
369+
370+
# Subset NEMODataArray data within bounding box:
371+
result = (self
372+
.where(mask, drop=False)
373+
.isel({self.j_name: slice(j_idx.min(), j_idx.max() + 1),
374+
self.i_name: slice(i_idx.min(), i_idx.max() + 1),
375+
})
376+
)
377+
378+
return result
311379

312380
def sel_like(
313381
self,
@@ -1369,6 +1437,33 @@ def __truediv__(self, other: Self | xr.DataArray | int | float) -> Self:
13691437
def __rtruediv__(self, other: Self | xr.DataArray | int | float) -> Self:
13701438
return self._rbinary_op(other, operator.truediv)
13711439

1440+
def __and__(self, other: Self | xr.DataArray) -> Self:
1441+
return self._binary_op(other, operator.and_)
1442+
1443+
def __xor__(self, other: Self | xr.DataArray) -> Self:
1444+
return self._binary_op(other, operator.xor)
1445+
1446+
def __or__(self, other: Self | xr.DataArray) -> Self:
1447+
return self._binary_op(other, operator.or_)
1448+
1449+
def __lt__(self, other: Self | xr.DataArray | int | float) -> Self:
1450+
return self._binary_op(other, operator.lt)
1451+
1452+
def __le__(self, other: Self | xr.DataArray | int | float) -> Self:
1453+
return self._binary_op(other, operator.le)
1454+
1455+
def __gt__(self, other: Self | xr.DataArray | int | float) -> Self:
1456+
return self._binary_op(other, operator.gt)
1457+
1458+
def __ge__(self, other: Self | xr.DataArray | int | float) -> Self:
1459+
return self._binary_op(other, operator.ge)
1460+
1461+
def __eq__(self, other: Self | xr.DataArray) -> Self:
1462+
return self._binary_op(other, nputils.array_eq)
1463+
1464+
def __ne__(self, other: Self | xr.DataArray) -> Self:
1465+
return self._binary_op(other, nputils.array_ne)
1466+
13721467
# ----------------
13731468
# Utility Methods
13741469
# ----------------

tests/test_nemodatarray_core.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,45 @@ def test_apply_custom_mask(self, dom_type, example_global_nemodatatree, example_
7575
assert result.data.equals(expected)
7676

7777

78+
class TestNEMODataArrayClip():
79+
@pytest.mark.parametrize("bbox", [[0, 1, 0, 2], (0, 1), "0 1 2 3"])
80+
def test_bbox_type(self, bbox, example_global_nemodatatree):
81+
# -- Verify ValueError is raised for invalid bbox -- #
82+
nemo = example_global_nemodatatree
83+
with pytest.raises(ValueError, match=re.escape("bounding box must be a tuple (lon_min, lon_max, lat_min, lat_max).")):
84+
nemo["gridT/tos_con"].clip(bbox=bbox)
85+
86+
@pytest.mark.parametrize("dom_type", ["global", "regional"])
87+
def test_clip(self, dom_type, example_global_nemodatatree, example_regional_nemodatatree):
88+
# -- Select NEMODataTree based on domain type -- #
89+
match dom_type:
90+
case "regional":
91+
nemo = example_regional_nemodatatree
92+
bbox = (40, 62, -50, -32)
93+
case "global":
94+
nemo = example_global_nemodatatree
95+
bbox = (-45, 60, -25, 30)
96+
case _:
97+
raise ValueError("dom_type must be 'global' or 'regional'")
98+
99+
# -- Clip NEMODataArray-- #
100+
nda_clipped = nemo["gridT/tos_con"].clip(bbox=bbox)
101+
102+
# -- Validate Clipped NEMODataArray -- #
103+
# Expect NEMODataArray is returned:
104+
assert isinstance(nda_clipped, NEMODataArray)
105+
106+
# Expect clipped grid dims sizes to be <= original NEMO model grid:
107+
assert nda_clipped.sizes["i"] <= nemo["gridT/tos_con"].sizes["i"]
108+
assert nda_clipped.sizes["j"] <= nemo["gridT/tos_con"].sizes["j"]
109+
110+
# Expect all grid coordinates to be within bounding box:
111+
assert nda_clipped["glamt"].min() >= bbox[0]
112+
assert nda_clipped["glamt"].max() <= bbox[1]
113+
assert nda_clipped["gphit"].min() >= bbox[2]
114+
assert nda_clipped["gphit"].max() <= bbox[3]
115+
116+
78117
class TestNEMODataArraySelLike:
79118
"""
80119
Test NEMODataArray.sel_like() Input Validation and Behaviour.

0 commit comments

Comments
 (0)