From 493c006eaed6c406f03a3d8d276438266d547abf Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Fri, 23 Jan 2026 16:20:20 -0800 Subject: [PATCH 01/11] fixed data_vars bug in RegularGrid - removed RegularGrid2D -- doesn't appear to be different, and not used? --- tests/test_grids/test_regular_grid.py | 112 ++++++++++---------- xarray_subset_grid/accessor.py | 18 +++- xarray_subset_grid/grids/regular_grid.py | 30 +++--- xarray_subset_grid/grids/regular_grid_2d.py | 23 ++-- 4 files changed, 101 insertions(+), 82 deletions(-) diff --git a/tests/test_grids/test_regular_grid.py b/tests/test_grids/test_regular_grid.py index 772b5ed..bd21ac5 100644 --- a/tests/test_grids/test_regular_grid.py +++ b/tests/test_grids/test_regular_grid.py @@ -12,9 +12,9 @@ from xarray_subset_grid.grids.regular_grid import RegularGrid -TEST_DATA = Path(__file__).parent.parent / "example_data" +EXAMPLE_DATA = Path(__file__).parent.parent / "example_data" -TEST_FILE1 = TEST_DATA / "AMSEAS-subset.nc" +TEST_FILE1 = EXAMPLE_DATA / "AMSEAS-subset.nc" # NGOFS2_RGRID.nc is a small subset of the regridded NGOFS2 model. @@ -34,7 +34,7 @@ def test_recognise_not(): """ should not recognise an SGrid """ - ds = xr.open_dataset(TEST_DATA / "arakawa_c_test_grid.nc") + ds = xr.open_dataset(EXAMPLE_DATA / "arakawa_c_test_grid.nc") assert not RegularGrid.recognize(ds) @@ -43,66 +43,70 @@ def test_recognise_not(): # These from the ugrid tests -- need to be adapted ####### -# def test_grid_vars(): -# """ -# Check if the grid vars are defined properly -# """ -# ds = xr.open_dataset(EXAMPLE_DATA / "SFBOFS_subset1.nc") - -# ds = ugrid.assign_ugrid_topology(ds, **grid_topology) - -# grid_vars = ds.xsg.grid_vars - -# # ['mesh', 'nv', 'lon', 'lat', 'lonc', 'latc'] -# assert grid_vars == set(["mesh", "nv", "nbe", "lon", "lat", "lonc", "latc"]) - +def test_grid_vars(): + """ + Check if the grid vars are defined properly + """ + ds = xr.open_dataset(EXAMPLE_DATA / "AMSEAS-subset.nc") -# def test_data_vars(): -# """ -# Check if the grid vars are defined properly + grid_vars = ds.xsg.grid_vars -# This is not currently working correctly! -# """ -# ds = xr.open_dataset(EXAMPLE_DATA / "SFBOFS_subset1.nc") -# ds = ugrid.assign_ugrid_topology(ds, **grid_topology) + # ['mesh', 'nv', 'lon', 'lat', 'lonc', 'latc'] + assert grid_vars == {'lat', 'lon'} -# data_vars = ds.xsg.data_vars -# assert set(data_vars) == set( -# [ -# "h", -# "zeta", -# "temp", -# "salinity", -# "u", -# "v", -# "uwind_speed", -# "vwind_speed", -# "wet_nodes", -# "wet_cells", -# ] -# ) +def test_data_vars(): + """ + Check if the data vars are defined properly + This is not currently working correctly! -# def test_extra_vars(): -# """ -# Check if the extra vars are defined properly + it finds extra stuff + """ + ds = xr.open_dataset(EXAMPLE_DATA / "AMSEAS-subset.nc") + + data_vars = ds.xsg.data_vars + + # the extra "time" variables are not using the grid + # so they should not be listed as data_vars + assert data_vars == { + 'water_w', + 'salinity', + 'surf_roughness', + 'surf_temp_flux', + 'water_v', + # 'time_offset', + 'water_temp', + 'water_baro_v', + 'surf_atm_press', + 'surf_el', + 'surf_salt_flux', + 'water_u', + 'surf_wnd_stress_gridy', + 'water_baro_u', + 'watdep', + 'surf_solar_flux', + # 'time1_run', + 'surf_wnd_stress_gridx', + # 'time1_offset' + } + +def test_extra_vars(): + """ + Check if the extra vars are defined properly -# This is not currently working correctly! -# """ -# ds = xr.open_dataset(EXAMPLE_DATA / "SFBOFS_subset1.nc") -# ds = ugrid.assign_ugrid_topology(ds, **grid_topology) + """ + ds = xr.open_dataset(EXAMPLE_DATA / "AMSEAS-subset.nc") -# extra_vars = ds.xsg.extra_vars + extra_vars = ds.xsg.extra_vars -# print([*ds]) -# print(f"{extra_vars=}") -# assert extra_vars == set( -# [ -# "nf_type", -# "Times", -# ] -# ) + # the extra "time" variables are not using the grid + # so they should be listed as extra_vars + assert extra_vars == { + 'time_offset', + 'time1_run', + 'time1_offset' + } # def test_coords(): diff --git a/xarray_subset_grid/accessor.py b/xarray_subset_grid/accessor.py index 2868b4e..ccde2b6 100644 --- a/xarray_subset_grid/accessor.py +++ b/xarray_subset_grid/accessor.py @@ -5,9 +5,21 @@ import xarray as xr from xarray_subset_grid.grid import Grid -from xarray_subset_grid.grids import FVCOMGrid, RegularGrid, RegularGrid2d, SELFEGrid, SGrid, UGrid - -_grid_impls = [FVCOMGrid, SELFEGrid, UGrid, SGrid, RegularGrid2d, RegularGrid] +from xarray_subset_grid.grids import (FVCOMGrid, + RegularGrid, + # @D version doesn't appear to be different ?? + # RegularGrid2d, + SELFEGrid, + SGrid, + UGrid) + +_grid_impls = [FVCOMGrid, + SELFEGrid, + UGrid, + SGrid, + # RegularGrid2d, + RegularGrid + ] def register_grid_impl(grid_impl: Grid, priority: int = 0): diff --git a/xarray_subset_grid/grids/regular_grid.py b/xarray_subset_grid/grids/regular_grid.py index ee49048..6554aee 100644 --- a/xarray_subset_grid/grids/regular_grid.py +++ b/xarray_subset_grid/grids/regular_grid.py @@ -27,9 +27,8 @@ class RegularGridPolygonSelector(Selector): polygon: list[tuple[float, float]] | np.ndarray _polygon_mask: xr.DataArray - def __init__( - self, polygon: list[tuple[float, float]] | np.ndarray, mask: xr.DataArray, name: str - ): + def __init__(self, polygon: list[tuple[float, float]] | np.ndarray, mask: xr.DataArray, + name: str): super().__init__() self.name = name self.polygon = polygon @@ -64,7 +63,6 @@ def select(self, ds: xr.Dataset) -> xr.Dataset: class RegularGrid(Grid): """Grid implementation for regular lat/lng grids.""" - @staticmethod def recognize(ds: xr.Dataset) -> bool: """Recognize if the dataset matches the given grid.""" @@ -102,17 +100,17 @@ def data_vars(self, ds: xr.Dataset) -> set[str]: """ lat = ds.cf.coordinates["latitude"][0] lon = ds.cf.coordinates["longitude"][0] - return { - var - for var in ds.data_vars - if var not in {lat, lon} - and "latitude" in var.cf.coordinates - and "longitude" in var.cf.coordinates + data_vars = {var.name for var in ds.data_vars.values() + if var.name not in {lat, lon} + and "latitude" in var.cf.coordinates + and "longitude" in var.cf.coordinates } + return data_vars - def compute_polygon_subset_selector( - self, ds: xr.Dataset, polygon: list[tuple[float, float]], name: str = None - ) -> Selector: + def compute_polygon_subset_selector(self, + ds: xr.Dataset, + polygon: list[tuple[float, float]], + name: str = None) -> Selector: lat = ds.cf["latitude"] lon = ds.cf["longitude"] @@ -120,9 +118,9 @@ def compute_polygon_subset_selector( polygon = normalize_polygon_x_coords(x, polygon) polygon_mask = ray_tracing_numpy(x, lat.flat, polygon).reshape(lon.shape) - selector = RegularGridPolygonSelector( - polygon=polygon, mask=polygon_mask, name=name or "selector" - ) + selector = RegularGridPolygonSelector(polygon=polygon, + mask=polygon_mask, + name=name or "selector") return selector def compute_bbox_subset_selector( diff --git a/xarray_subset_grid/grids/regular_grid_2d.py b/xarray_subset_grid/grids/regular_grid_2d.py index 56ef57a..2e213da 100644 --- a/xarray_subset_grid/grids/regular_grid_2d.py +++ b/xarray_subset_grid/grids/regular_grid_2d.py @@ -1,3 +1,9 @@ +# 2D and 3D should share a lot of code +# +# do we need a separate class for this? +# This doesn't appear, right now, to check for depth to dedermine if it's 2D + + import numpy as np import xarray as xr @@ -6,13 +12,12 @@ from xarray_subset_grid.utils import compute_2d_subset_mask + class RegularGrid2dSelector(Selector): polygon: list[tuple[float, float]] | np.ndarray _subset_mask: xr.DataArray - def __init__( - self, polygon: list[tuple[float, float]] | np.ndarray, subset_mask: xr.DataArray, name: str - ): + def __init__(self, polygon: list[tuple[float, float]] | np.ndarray, subset_mask: xr.DataArray, name: str): super().__init__() self.name = name self.polygon = polygon @@ -72,13 +77,13 @@ def data_vars(self, ds: xr.Dataset) -> set[str]: """ lat = ds.cf.coordinates["latitude"][0] lon = ds.cf.coordinates["longitude"][0] - return { - var - for var in ds.data_vars - if var not in {lat, lon} - and "latitude" in var.cf.coordinates - and "longitude" in var.cf.coordinates + data_vars = {var.name for var in ds.data_vars.values() + if var.name not in {lat, lon} + and "latitude" in var.cf.coordinates + and "longitude" in var.cf.coordinates } + return data_vars + def compute_polygon_subset_selector( self, ds: xr.Dataset, polygon: list[tuple[float, float]], name: str = None From fe585f95fce7f8b4b002f54d6d3bacdddadd5210 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Fri, 23 Jan 2026 16:25:37 -0800 Subject: [PATCH 02/11] some lint cleanup --- xarray_subset_grid/accessor.py | 16 +++++++++------- xarray_subset_grid/grids/regular_grid_2d.py | 6 ++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/xarray_subset_grid/accessor.py b/xarray_subset_grid/accessor.py index ccde2b6..648f21b 100644 --- a/xarray_subset_grid/accessor.py +++ b/xarray_subset_grid/accessor.py @@ -5,13 +5,15 @@ import xarray as xr from xarray_subset_grid.grid import Grid -from xarray_subset_grid.grids import (FVCOMGrid, - RegularGrid, - # @D version doesn't appear to be different ?? - # RegularGrid2d, - SELFEGrid, - SGrid, - UGrid) +from xarray_subset_grid.grids import ( + FVCOMGrid, + RegularGrid, + # @D version doesn't appear to be different ?? + # RegularGrid2d, + SELFEGrid, + SGrid, + UGrid, +) _grid_impls = [FVCOMGrid, SELFEGrid, diff --git a/xarray_subset_grid/grids/regular_grid_2d.py b/xarray_subset_grid/grids/regular_grid_2d.py index 2e213da..38445b6 100644 --- a/xarray_subset_grid/grids/regular_grid_2d.py +++ b/xarray_subset_grid/grids/regular_grid_2d.py @@ -12,12 +12,14 @@ from xarray_subset_grid.utils import compute_2d_subset_mask - class RegularGrid2dSelector(Selector): polygon: list[tuple[float, float]] | np.ndarray _subset_mask: xr.DataArray - def __init__(self, polygon: list[tuple[float, float]] | np.ndarray, subset_mask: xr.DataArray, name: str): + def __init__(self, + polygon: list[tuple[float, float]] | np.ndarray, + subset_mask: xr.DataArray, + name: str): super().__init__() self.name = name self.polygon = polygon From 5ac391bf9c6beef2b299e5fd2f649a15c46298da Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 26 Jan 2026 20:13:51 -0800 Subject: [PATCH 03/11] fix for bug with lat (or lon) not increasing ... --- .../example_data/2D-rectangular_grid_wind.nc | Bin 0 -> 121393 bytes .../rectangular_grid_decreasing.nc | Bin 0 -> 122349 bytes tests/test_grids/test_regular_grid.py | 77 +++++++++++++----- xarray_subset_grid/grids/regular_grid.py | 25 ++++-- 4 files changed, 77 insertions(+), 25 deletions(-) create mode 100644 tests/example_data/2D-rectangular_grid_wind.nc create mode 100644 tests/example_data/rectangular_grid_decreasing.nc diff --git a/tests/example_data/2D-rectangular_grid_wind.nc b/tests/example_data/2D-rectangular_grid_wind.nc new file mode 100644 index 0000000000000000000000000000000000000000..1be0a504c18bbefbda521bc6fcfb76dbf9dcc228 GIT binary patch literal 121393 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 A0{{R3 literal 0 HcmV?d00001 diff --git a/tests/example_data/rectangular_grid_decreasing.nc b/tests/example_data/rectangular_grid_decreasing.nc new file mode 100644 index 0000000000000000000000000000000000000000..dc1f7c0c5eb04bbbbdec77d37c76ea3e88762397 GIT binary patch literal 122349 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 literal 0 HcmV?d00001 diff --git a/tests/test_grids/test_regular_grid.py b/tests/test_grids/test_regular_grid.py index bd21ac5..8c53cc5 100644 --- a/tests/test_grids/test_regular_grid.py +++ b/tests/test_grids/test_regular_grid.py @@ -108,27 +108,66 @@ def test_extra_vars(): 'time1_offset' } +def test_subset_to_bb(): + """ + Not a complete test by any means, but the basics are there. -# def test_coords(): -# ds = xr.open_dataset(EXAMPLE_DATA / "SFBOFS_subset1.nc") -# ds = ugrid.assign_ugrid_topology(ds, **grid_topology) + NOTE: it doesn't test if the variables got subset corectly ... -# coords = ds.xsg.coords - -# print(f"{coords=}") -# print(f"{ds.coords=}") - -# assert set(coords) == set( -# [ -# "lon", -# "lat", -# "lonc", -# "latc", -# "time", -# "siglay", -# "siglev", -# ] -# ) + """ + ds = xr.open_dataset(EXAMPLE_DATA / "2D-rectangular_grid_wind.nc") + + print("bounds:", ds['lat'].data.min(), ds['lat'].data.max()) + print("bounds:", ds['lon'].data.min(), ds['lon'].data.max()) + print(ds.xsg.data_vars) + + bbox = (0, 0, 0.5, 0.5) + + ds2 = ds.xsg.subset_bbox(bbox) + + assert ds2['lat'].size == 15 + assert ds2['lon'].size == 15 + + new_bounds = (ds2['lat'].data.min(), + ds2['lon'].data.min(), + ds2['lat'].data.max(), + ds2['lon'].data.max() + ) + assert new_bounds == bbox + +def test_decreasing_latitude(): + """ + Some datasets have the latitude or longitude decreasing: 10, 9, 8 etc. + e.g the NOAA GFS met model + + subsetting should still work + + """ + ds = xr.open_dataset(EXAMPLE_DATA / "rectangular_grid_decreasing.nc") + + print("bounds:", ds['lat'].data.min(), + ds['lat'].data.max(), + ds['lon'].data.min(), + ds['lon'].data.max()) + print(ds.xsg.data_vars) + + bbox = (0, 0, 0.5, 0.5) + + ds2 = ds.xsg.subset_bbox(bbox) + + print(ds2['lat'].size) + print(ds2['lon'].size) + + assert ds2['lat'].size == 15 + assert ds2['lon'].size == 15 + + new_bounds = (ds2['lat'].data.min(), + ds2['lon'].data.min(), + ds2['lat'].data.max(), + ds2['lon'].data.max() + ) + print(new_bounds) + assert new_bounds == bbox # def test_vertical_levels(): diff --git a/xarray_subset_grid/grids/regular_grid.py b/xarray_subset_grid/grids/regular_grid.py index 6554aee..1a33637 100644 --- a/xarray_subset_grid/grids/regular_grid.py +++ b/xarray_subset_grid/grids/regular_grid.py @@ -57,7 +57,21 @@ def __init__(self, bbox: tuple[float, float, float, float]): self._latitude_selection = slice(bbox[1], bbox[3]) def select(self, ds: xr.Dataset) -> xr.Dataset: - """Perform the selection on the dataset.""" + """ + Perform the selection on the dataset. + """ + lat = ds[ds.cf.coordinates.get("latitude")[0]] + lon = ds[ds.cf.coordinates.get("longitude")[0]] + if np.all(np.diff(lat) < 0): + # swap the slice if the latitudes are decending + self._latitude_selection = slice(self._latitude_selection.stop, + self._latitude_selection.start) + # and np.all(np.diff(lon) > 0): + if np.all(np.diff(lon) < 0): + # swap the slice if the longitudes are decending + self._longitude_selection = slice(self._longitude_selection.stop, + self._longitude_selection.start) + return ds.cf.sel(lon=self._longitude_selection, lat=self._latitude_selection) @@ -123,11 +137,10 @@ def compute_polygon_subset_selector(self, name=name or "selector") return selector - def compute_bbox_subset_selector( - self, - ds: xr.Dataset, - bbox: tuple[float, float, float, float], - ) -> Selector: + def compute_bbox_subset_selector(self, + ds: xr.Dataset, + bbox: tuple[float, float, float, float], + ) -> Selector: bbox = normalize_bbox_x_coords(ds.cf["longitude"].values, bbox) selector = RegularGridBBoxSelector(bbox) return selector From 5a6df53445306c7b70eb10f47c25b0d923436641 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 26 Jan 2026 20:27:29 -0800 Subject: [PATCH 04/11] made test non-square --- tests/test_grids/test_regular_grid.py | 44 +++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/test_grids/test_regular_grid.py b/tests/test_grids/test_regular_grid.py index 8c53cc5..4816211 100644 --- a/tests/test_grids/test_regular_grid.py +++ b/tests/test_grids/test_regular_grid.py @@ -117,22 +117,25 @@ def test_subset_to_bb(): """ ds = xr.open_dataset(EXAMPLE_DATA / "2D-rectangular_grid_wind.nc") - print("bounds:", ds['lat'].data.min(), ds['lat'].data.max()) - print("bounds:", ds['lon'].data.min(), ds['lon'].data.max()) - print(ds.xsg.data_vars) + print("initial bounds:", ds['lon'].data.min(), + ds['lat'].data.min(), + ds['lon'].data.max(), + ds['lat'].data.max(), + ) - bbox = (0, 0, 0.5, 0.5) + bbox = (-0.5, 0, 0.5, 0.5) ds2 = ds.xsg.subset_bbox(bbox) assert ds2['lat'].size == 15 - assert ds2['lon'].size == 15 + assert ds2['lon'].size == 29 - new_bounds = (ds2['lat'].data.min(), - ds2['lon'].data.min(), + new_bounds = (ds2['lon'].data.min(), + ds2['lat'].data.min(), + ds2['lon'].data.max(), ds2['lat'].data.max(), - ds2['lon'].data.max() ) + print("new bounds:", new_bounds) assert new_bounds == bbox def test_decreasing_latitude(): @@ -145,28 +148,25 @@ def test_decreasing_latitude(): """ ds = xr.open_dataset(EXAMPLE_DATA / "rectangular_grid_decreasing.nc") - print("bounds:", ds['lat'].data.min(), - ds['lat'].data.max(), - ds['lon'].data.min(), - ds['lon'].data.max()) - print(ds.xsg.data_vars) + print("initial bounds:", ds['lon'].data.min(), + ds['lat'].data.min(), + ds['lon'].data.max(), + ds['lat'].data.max(), + ) - bbox = (0, 0, 0.5, 0.5) + bbox = (-0.5, 0, 0.5, 0.5) ds2 = ds.xsg.subset_bbox(bbox) - print(ds2['lat'].size) - print(ds2['lon'].size) - assert ds2['lat'].size == 15 - assert ds2['lon'].size == 15 + assert ds2['lon'].size == 29 - new_bounds = (ds2['lat'].data.min(), - ds2['lon'].data.min(), + new_bounds = (ds2['lon'].data.min(), + ds2['lat'].data.min(), + ds2['lon'].data.max(), ds2['lat'].data.max(), - ds2['lon'].data.max() ) - print(new_bounds) + print("new bounds:", new_bounds) assert new_bounds == bbox From b3fc8ecc546c7a2dfc4ec3a40d3ecfea915c40c6 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Mon, 26 Jan 2026 21:00:35 -0800 Subject: [PATCH 05/11] fixed polygon subsetting for regular grid. --- tests/test_grids/test_regular_grid.py | 34 ++++++++++++ xarray_subset_grid/grids/regular_grid.py | 66 ++++++++++++++---------- 2 files changed, 74 insertions(+), 26 deletions(-) diff --git a/tests/test_grids/test_regular_grid.py b/tests/test_grids/test_regular_grid.py index 4816211..1579052 100644 --- a/tests/test_grids/test_regular_grid.py +++ b/tests/test_grids/test_regular_grid.py @@ -169,6 +169,40 @@ def test_decreasing_latitude(): print("new bounds:", new_bounds) assert new_bounds == bbox +def test_subset_polygon(): + """ + Not a complete test by any means, but the basics are there. + + NOTE: it doesn't test if the variables got subset corectly ... + + """ + ds = xr.open_dataset(EXAMPLE_DATA / "2D-rectangular_grid_wind.nc") + + print("initial bounds:", ds['lon'].data.min(), + ds['lat'].data.min(), + ds['lon'].data.max(), + ds['lat'].data.max(), + ) + + poly = [(-0.5, 0.0), (0.0, 0.5), (0.5, 0.5), (0.5, 0.0), (0, 0.0)] + # this poly has this bounding box: + # bbox = (-0.5, 0, 0.5, 0.5) + # so results should be the same as the bbox tests + + ds2 = ds.xsg.subset_polygon(poly) + + assert ds2['lat'].size == 15 + assert ds2['lon'].size == 29 + + new_bounds = (ds2['lon'].data.min(), + ds2['lat'].data.min(), + ds2['lon'].data.max(), + ds2['lat'].data.max(), + ) + print("new bounds:", new_bounds) + assert new_bounds == (-0.5, 0, 0.5, 0.5) + + # def test_vertical_levels(): # ds = xr.open_dataset(EXAMPLE_DATA / "SFBOFS_subset1.nc") diff --git a/xarray_subset_grid/grids/regular_grid.py b/xarray_subset_grid/grids/regular_grid.py index 1a33637..ae50d9b 100644 --- a/xarray_subset_grid/grids/regular_grid.py +++ b/xarray_subset_grid/grids/regular_grid.py @@ -21,26 +21,29 @@ ) -class RegularGridPolygonSelector(Selector): - """Polygon Selector for regular lat/lon grids.""" +# class RegularGridPolygonSelector(Selector): +# """Polygon Selector for regular lat/lon grids.""" +# # with a regular grid, you have to select the full boudning box anyway +# # this this simply computes the bounding box, and used that - polygon: list[tuple[float, float]] | np.ndarray - _polygon_mask: xr.DataArray +# polygon: list[tuple[float, float]] | np.ndarray +# _polygon_mask: xr.DataArray - def __init__(self, polygon: list[tuple[float, float]] | np.ndarray, mask: xr.DataArray, - name: str): - super().__init__() - self.name = name - self.polygon = polygon - self.polygon_mask = mask +# def __init__(self, polygon: list[tuple[float, float]] | np.ndarray, mask: xr.DataArray, +# name: str): +# super().__init__() +# self.name = name +# self.polygon = polygon +# self.polygon_mask = mask + +# def select(self, ds: xr.Dataset) -> xr.Dataset: +# """Perform the selection on the dataset.""" +# ds_subset = ds.cf.isel( +# lon=self._polygon_mask, +# lat=self._polygon_mask, +# ) +# return ds_subset - def select(self, ds: xr.Dataset) -> xr.Dataset: - """Perform the selection on the dataset.""" - ds_subset = ds.cf.isel( - lon=self._polygon_mask, - lat=self._polygon_mask, - ) - return ds_subset class RegularGridBBoxSelector(Selector): @@ -74,6 +77,20 @@ def select(self, ds: xr.Dataset) -> xr.Dataset: return ds.cf.sel(lon=self._longitude_selection, lat=self._latitude_selection) +class RegularGridPolygonSelector(RegularGridBBoxSelector): + """Polygon Selector for regular lat/lon grids.""" + # with a regular grid, you have to select the full bounding box anyway + # this this simply computes the bounding box, and uses the same code. + + def __init__(self, polygon: list[tuple[float, float]] | np.ndarray): + polygon = np.asarray(polygon) + bbox = (polygon[:,0].min(), + polygon[:,1].min(), + polygon[:,0].max(), + polygon[:,1].max(), + ) + super().__init__(bbox=bbox) + class RegularGrid(Grid): """Grid implementation for regular lat/lng grids.""" @@ -124,17 +141,14 @@ def data_vars(self, ds: xr.Dataset) -> set[str]: def compute_polygon_subset_selector(self, ds: xr.Dataset, polygon: list[tuple[float, float]], - name: str = None) -> Selector: - lat = ds.cf["latitude"] - lon = ds.cf["longitude"] + ) -> Selector: + + polygon = np.asarray(polygon) + lon = ds.cf["longitude"].data - x = np.array(lon.flat) - polygon = normalize_polygon_x_coords(x, polygon) - polygon_mask = ray_tracing_numpy(x, lat.flat, polygon).reshape(lon.shape) + polygon = normalize_polygon_x_coords(lon, polygon) - selector = RegularGridPolygonSelector(polygon=polygon, - mask=polygon_mask, - name=name or "selector") + selector = RegularGridPolygonSelector(polygon=polygon) return selector def compute_bbox_subset_selector(self, From f75a9c16ece9a97b126a00713210d0f49b4edb7f Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Wed, 4 Feb 2026 12:06:18 -0800 Subject: [PATCH 06/11] added tests from gitHub PR from Nesar976 --- tests/test_grids/test_regular_grid.py | 81 +++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/tests/test_grids/test_regular_grid.py b/tests/test_grids/test_regular_grid.py index 1579052..c00e719 100644 --- a/tests/test_grids/test_regular_grid.py +++ b/tests/test_grids/test_regular_grid.py @@ -4,10 +4,13 @@ from pathlib import Path -try: - import fsspec -except ImportError: - fsspec = None +import numpy as np + +# try: +# import fsspec +# except ImportError: +# fsspec = None + import xarray as xr from xarray_subset_grid.grids.regular_grid import RegularGrid @@ -39,6 +42,76 @@ def test_recognise_not(): assert not RegularGrid.recognize(ds) +def create_synthetic_rectangular_grid_dataset(decreasing=False): + """ + Create a synthetic dataset with regular grid. + + can be either decreasing or increasing in latitude + """ + + lon = np.linspace(-100, -80, 21) + if decreasing: + lat = np.linspace(50, 30, 21) + else: + lat = np.linspace(30, 50, 21) + + data = np.random.rand(21, 21) + + ds = xr.Dataset( + data_vars={ + "temp": (("lat", "lon"), data), + "salt": (("lat", "lon"), data), + }, + coords={ + "lat": lat, + "lon": lon, + }, + ) + # Add cf attributes + ds.lat.attrs = {"standard_name": "latitude", "units": "degrees_north"} + ds.lon.attrs = {"standard_name": "longitude", "units": "degrees_east"} + ds.temp.attrs = {"standard_name": "sea_water_temperature"} + + return ds + +# might not be needed if tested elsewhere. +def test_data_vars_error(): + print("Testing data_vars error...") + ds = create_synthetic_dataset() + # Ensure it is recognized as a RegularGrid + assert RegularGrid.recognize(ds) + + # Access xsg accessor + data_vars = ds.xsg.data_vars + print(f"data_vars: {data_vars}") + + assert data_vars == set{} + + +def test_decreasing_coords(): + print("\nTesting decreasing coordinates support...") + ds = create_synthetic_rectangular_grid_dataset(decreasing=True) + # assert RegularGrid.recognize(ds) + + # bbox: (min_lon, min_lat, max_lon, max_lat) + bbox = (-95, 35, -85, 45) + + subset = ds.xsg.subset_bbox(bbox) + print(f"Subset size: {subset.sizes}") + + # Check if subset has data + assert subset.sizes["lat"] > 0 + assert subset.sizes["lon"] > 0 + # if subset.sizes["lat"] == 0 or subset.sizes["lon"] == 0: + # print("FAILURE: Subset has dimension size 0") + # else: + # print("SUCCESS: Subset has data") + + # except Exception as e: + # print(f"Caught unexpected error in decreasing coords subsetting: {e}") + + + ####### # These from the ugrid tests -- need to be adapted ####### From 2ca7f2146a89f4b8f378f77fdccecc3b2bf408d5 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Wed, 4 Feb 2026 12:11:08 -0800 Subject: [PATCH 07/11] added data_vars tests --- tests/test_grids/test_regular_grid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_grids/test_regular_grid.py b/tests/test_grids/test_regular_grid.py index c00e719..8b421c9 100644 --- a/tests/test_grids/test_regular_grid.py +++ b/tests/test_grids/test_regular_grid.py @@ -77,7 +77,7 @@ def create_synthetic_rectangular_grid_dataset(decreasing=False): # might not be needed if tested elsewhere. def test_data_vars_error(): print("Testing data_vars error...") - ds = create_synthetic_dataset() + ds = create_synthetic_rectangular_grid_dataset() # Ensure it is recognized as a RegularGrid assert RegularGrid.recognize(ds) @@ -85,7 +85,7 @@ def test_data_vars_error(): data_vars = ds.xsg.data_vars print(f"data_vars: {data_vars}") - assert data_vars == set{} + assert data_vars == {'salt', 'temp'} def test_decreasing_coords(): From 48a8fa651cf592597cd767240e1a79ca6b3352b4 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Wed, 4 Feb 2026 12:53:37 -0800 Subject: [PATCH 08/11] merge of tests for PR for regular grids. --- tests/test_grids/test_regular_grid.py | 77 +++++++++++++-------------- xarray_subset_grid/utils.py | 20 +++++++ 2 files changed, 56 insertions(+), 41 deletions(-) diff --git a/tests/test_grids/test_regular_grid.py b/tests/test_grids/test_regular_grid.py index 8b421c9..136f25f 100644 --- a/tests/test_grids/test_regular_grid.py +++ b/tests/test_grids/test_regular_grid.py @@ -46,7 +46,7 @@ def create_synthetic_rectangular_grid_dataset(decreasing=False): """ Create a synthetic dataset with regular grid. - can be either decreasing or increasing in latitude + Can be either decreasing or increasing in latitude """ lon = np.linspace(-100, -80, 21) @@ -74,47 +74,8 @@ def create_synthetic_rectangular_grid_dataset(decreasing=False): return ds -# might not be needed if tested elsewhere. -def test_data_vars_error(): - print("Testing data_vars error...") - ds = create_synthetic_rectangular_grid_dataset() - # Ensure it is recognized as a RegularGrid - assert RegularGrid.recognize(ds) - - # Access xsg accessor - data_vars = ds.xsg.data_vars - print(f"data_vars: {data_vars}") - - assert data_vars == {'salt', 'temp'} -def test_decreasing_coords(): - print("\nTesting decreasing coordinates support...") - ds = create_synthetic_rectangular_grid_dataset(decreasing=True) - # assert RegularGrid.recognize(ds) - - # bbox: (min_lon, min_lat, max_lon, max_lat) - bbox = (-95, 35, -85, 45) - - subset = ds.xsg.subset_bbox(bbox) - print(f"Subset size: {subset.sizes}") - - # Check if subset has data - assert subset.sizes["lat"] > 0 - assert subset.sizes["lon"] > 0 - # if subset.sizes["lat"] == 0 or subset.sizes["lon"] == 0: - # print("FAILURE: Subset has dimension size 0") - # else: - # print("SUCCESS: Subset has data") - - # except Exception as e: - # print(f"Caught unexpected error in decreasing coords subsetting: {e}") - - - -####### -# These from the ugrid tests -- need to be adapted -####### def test_grid_vars(): """ @@ -164,10 +125,26 @@ def test_data_vars(): # 'time1_offset' } +# might not be needed if tested elsewhere. +def test_data_vars2(): + """ + redundant with above, by already written ... + """ + print("Testing data_vars error...") + ds = create_synthetic_rectangular_grid_dataset() + # Ensure it is recognized as a RegularGrid + assert RegularGrid.recognize(ds) + + # Access xsg accessor + data_vars = ds.xsg.data_vars + print(f"data_vars: {data_vars}") + + assert data_vars == {'salt', 'temp'} + + def test_extra_vars(): """ Check if the extra vars are defined properly - """ ds = xr.open_dataset(EXAMPLE_DATA / "AMSEAS-subset.nc") @@ -242,6 +219,24 @@ def test_decreasing_latitude(): print("new bounds:", new_bounds) assert new_bounds == bbox +def test_decreasing_coords(): + """ + Redundant with above, but already written ... + """ + print("\nTesting decreasing coordinates support...") + ds = create_synthetic_rectangular_grid_dataset(decreasing=True) + # assert RegularGrid.recognize(ds) + + # bbox: (min_lon, min_lat, max_lon, max_lat) + bbox = (-95, 35, -85, 45) + + subset = ds.xsg.subset_bbox(bbox) + print(f"Subset size: {subset.sizes}") + + # Check if subset has data + assert subset.sizes["lat"] > 0 + assert subset.sizes["lon"] > 0 + def test_subset_polygon(): """ Not a complete test by any means, but the basics are there. diff --git a/xarray_subset_grid/utils.py b/xarray_subset_grid/utils.py index 1b0b535..e7a3a2b 100644 --- a/xarray_subset_grid/utils.py +++ b/xarray_subset_grid/utils.py @@ -2,6 +2,7 @@ import cf_xarray # noqa import numpy as np +from dateutil.parser import parse as parsetime import xarray as xr @@ -151,3 +152,22 @@ def compute_2d_subset_mask( polygon_mask = np.where(polygon_mask > 1, True, False) return xr.DataArray(polygon_mask, dims=mask_dims) + +def asdatetime(dt): + """ + makes sure the input is a datetime.datetime object + + if it already is, it will be passed through. + + If not it will attempt to parse a string to make a datetime object. + + None will also be passed through silently + """ + if dt is None: + return dt + # if not isinstance(dt, datetime): + if not isinstance(dt, (datetime, cftime.datetime)): + # assume it's an iso string, or something that dateutils can parse. + return parsetime(dt, ignoretz=True) + else: + return dt From 0b5d17b914f6f7baec7e693f9a2cfeb9f456edf1 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Wed, 4 Feb 2026 15:19:46 -0800 Subject: [PATCH 09/11] Fixed bug with regular grid mis-identifying UGRIDS --- .../example_data/SFBOFS_subset1.nc | Bin .../example_data/small_ugrid_zero_based.nc | Bin .../example_data/tris_and_bounds.nc | Bin tests/test_grids/test_regular_grid.py | 14 ++++++++---- tests/test_grids/test_ugrid.py | 6 ++---- tests/test_visualization/test_mpl_plotting.py | 2 +- xarray_subset_grid/grids/regular_grid.py | 20 ++++++++++++++---- 7 files changed, 29 insertions(+), 13 deletions(-) rename {docs/examples => tests}/example_data/SFBOFS_subset1.nc (100%) rename {docs/examples => tests}/example_data/small_ugrid_zero_based.nc (100%) rename {docs/examples => tests}/example_data/tris_and_bounds.nc (100%) diff --git a/docs/examples/example_data/SFBOFS_subset1.nc b/tests/example_data/SFBOFS_subset1.nc similarity index 100% rename from docs/examples/example_data/SFBOFS_subset1.nc rename to tests/example_data/SFBOFS_subset1.nc diff --git a/docs/examples/example_data/small_ugrid_zero_based.nc b/tests/example_data/small_ugrid_zero_based.nc similarity index 100% rename from docs/examples/example_data/small_ugrid_zero_based.nc rename to tests/example_data/small_ugrid_zero_based.nc diff --git a/docs/examples/example_data/tris_and_bounds.nc b/tests/example_data/tris_and_bounds.nc similarity index 100% rename from docs/examples/example_data/tris_and_bounds.nc rename to tests/example_data/tris_and_bounds.nc diff --git a/tests/test_grids/test_regular_grid.py b/tests/test_grids/test_regular_grid.py index 136f25f..174d1d5 100644 --- a/tests/test_grids/test_regular_grid.py +++ b/tests/test_grids/test_regular_grid.py @@ -6,6 +6,7 @@ import numpy as np +# only needed if you want to hit AWS servers. # try: # import fsspec # except ImportError: @@ -15,9 +16,10 @@ from xarray_subset_grid.grids.regular_grid import RegularGrid +import pytest + EXAMPLE_DATA = Path(__file__).parent.parent / "example_data" -TEST_FILE1 = EXAMPLE_DATA / "AMSEAS-subset.nc" # NGOFS2_RGRID.nc is a small subset of the regridded NGOFS2 model. @@ -28,16 +30,20 @@ def test_recognise(): """ works for at least one file ... """ - ds = xr.open_dataset(TEST_FILE1) + ds = xr.open_dataset(EXAMPLE_DATA / "AMSEAS-subset.nc") assert RegularGrid.recognize(ds) -def test_recognise_not(): +@pytest.mark.parametrize("test_file", [ + # EXAMPLE_DATA / "arakawa_c_test_grid.nc", + EXAMPLE_DATA / "small_ugrid_zero_based.nc", + ]) +def test_recognise_not(test_file): """ should not recognise an SGrid """ - ds = xr.open_dataset(EXAMPLE_DATA / "arakawa_c_test_grid.nc") + ds = xr.open_dataset(test_file) assert not RegularGrid.recognize(ds) diff --git a/tests/test_grids/test_ugrid.py b/tests/test_grids/test_ugrid.py index 3bdcfd3..ebc84ae 100644 --- a/tests/test_grids/test_ugrid.py +++ b/tests/test_grids/test_ugrid.py @@ -15,9 +15,7 @@ from xarray_subset_grid import Selector from xarray_subset_grid.grids import ugrid -EXAMPLE_DATA = Path(__file__).parent.parent.parent / "docs" / "examples" / "example_data" - -TEST_FILE1 = EXAMPLE_DATA / "SFBOFS_subset1.nc" +EXAMPLE_DATA = Path(__file__).parent.parent / "example_data" # SFBOFS_subset1.nc is a smallish subset of the SFBOFS FVCOM model @@ -218,7 +216,7 @@ # cell:standard_name = "cell number" ; # cell:long_name = "Mapping to original mesh cell number" ; -# topology for TEST_FILE1 +# topology for SFBOFS_subset1 grid_topology = { "node_coordinates": "lon lat", "face_node_connectivity": "nv", diff --git a/tests/test_visualization/test_mpl_plotting.py b/tests/test_visualization/test_mpl_plotting.py index 2564158..8e6cf31 100644 --- a/tests/test_visualization/test_mpl_plotting.py +++ b/tests/test_visualization/test_mpl_plotting.py @@ -21,7 +21,7 @@ pytestmark = pytest.mark.skip(reason="matplotlib is not installed") -EXAMPLE_DATA = Path(__file__).parent.parent.parent / "docs" / "examples" / "example_data" +EXAMPLE_DATA = Path(__file__).parent.parent / "example_data" OUTPUT_DIR = Path(__file__).parent / "output" diff --git a/xarray_subset_grid/grids/regular_grid.py b/xarray_subset_grid/grids/regular_grid.py index ae50d9b..6c2d589 100644 --- a/xarray_subset_grid/grids/regular_grid.py +++ b/xarray_subset_grid/grids/regular_grid.py @@ -96,16 +96,28 @@ class RegularGrid(Grid): """Grid implementation for regular lat/lng grids.""" @staticmethod def recognize(ds: xr.Dataset) -> bool: - """Recognize if the dataset matches the given grid.""" + """ + Recognize if the dataset matches the given grid. + """ lat = ds.cf.coordinates.get("latitude", None) lon = ds.cf.coordinates.get("longitude", None) if lat is None or lon is None: return False + # choose first one -- valid assumption?? + lat = lat[0] + lon = lon[0] # Make sure the coordinates are 1D and match - lat_ndim = ds[lat[0]].ndim - lon_ndim = ds[lon[0]].ndim - return lat_ndim == lon_ndim and lon_ndim == 1 + if not (1 == ds[lat].ndim == ds[lon].ndim): + return False + + # make sure that at least one variable is using both the + # latitude and longitude dimensions + # (ugrids have both coordinates, but not both dimensions) + for var_name, var in ds.data_vars.items(): + if (lon in var.dims) and (lat in var.dims): + return True + return False @property def name(self) -> str: From 08e699fb70d40ad068219d7009abbf8b3140c81f Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Wed, 4 Feb 2026 15:38:03 -0800 Subject: [PATCH 10/11] cleaned up some lint --- tests/test_grids/test_regular_grid.py | 4 +--- xarray_subset_grid/grids/regular_grid.py | 2 -- xarray_subset_grid/utils.py | 6 ++++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/test_grids/test_regular_grid.py b/tests/test_grids/test_regular_grid.py index 174d1d5..09d5fce 100644 --- a/tests/test_grids/test_regular_grid.py +++ b/tests/test_grids/test_regular_grid.py @@ -5,19 +5,17 @@ from pathlib import Path import numpy as np +import pytest # only needed if you want to hit AWS servers. # try: # import fsspec # except ImportError: # fsspec = None - import xarray as xr from xarray_subset_grid.grids.regular_grid import RegularGrid -import pytest - EXAMPLE_DATA = Path(__file__).parent.parent / "example_data" diff --git a/xarray_subset_grid/grids/regular_grid.py b/xarray_subset_grid/grids/regular_grid.py index 6c2d589..a8a70a8 100644 --- a/xarray_subset_grid/grids/regular_grid.py +++ b/xarray_subset_grid/grids/regular_grid.py @@ -17,10 +17,8 @@ from xarray_subset_grid.utils import ( normalize_bbox_x_coords, normalize_polygon_x_coords, - ray_tracing_numpy, ) - # class RegularGridPolygonSelector(Selector): # """Polygon Selector for regular lat/lon grids.""" # # with a regular grid, you have to select the full boudning box anyway diff --git a/xarray_subset_grid/utils.py b/xarray_subset_grid/utils.py index e7a3a2b..150d3c1 100644 --- a/xarray_subset_grid/utils.py +++ b/xarray_subset_grid/utils.py @@ -1,9 +1,11 @@ import warnings +from datetime import datetime import cf_xarray # noqa +import cftime import numpy as np -from dateutil.parser import parse as parsetime import xarray as xr +from dateutil.parser import parse as parsetime def normalize_polygon_x_coords(x, poly): @@ -166,7 +168,7 @@ def asdatetime(dt): if dt is None: return dt # if not isinstance(dt, datetime): - if not isinstance(dt, (datetime, cftime.datetime)): + if not isinstance(dt, datetime | cftime.datetime): # assume it's an iso string, or something that dateutils can parse. return parsetime(dt, ignoretz=True) else: From 735cd4ee5941975c903040b1bd8dfef8633be7d5 Mon Sep 17 00:00:00 2001 From: Chris Barker Date: Wed, 4 Feb 2026 16:35:35 -0800 Subject: [PATCH 11/11] added more tests for recognise() and some lint. --- tests/conftest.py | 17 ++++++++++++ tests/test_grids/test_regular_grid.py | 16 +++++------- tests/test_grids/test_ugrid.py | 37 +++++++++++++++++++++++++++ xarray_subset_grid/grids/ugrid.py | 4 +-- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1e23dc6..4886252 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,7 @@ # conftest: some configuration for the tests +from pathlib import Path + import pytest @@ -35,3 +37,18 @@ def pytest_collection_modifyitems(config, items): # if envnames: # if item.config.getoption("-E") not in envnames: # pytest.skip(f"test requires env in {envnames!r}") + +EXAMPLE_DATA = Path(__file__).parent / 'example_data' + +UGRID_FILES = [EXAMPLE_DATA / 'SFBOFS_subset1.nc', + EXAMPLE_DATA / 'small_ugrid_zero_based.nc', + EXAMPLE_DATA / 'tris_and_bounds.nc', + ] + +SGRID_FILES = [EXAMPLE_DATA / 'arakawa_c_test_grid.nc', + ] + +RGRID_FILES = [EXAMPLE_DATA / '2D-rectangular_grid_wind.nc', + EXAMPLE_DATA / 'rectangular_grid_decreasing.nc', + EXAMPLE_DATA / 'AMSEAS-subset.nc', + ] diff --git a/tests/test_grids/test_regular_grid.py b/tests/test_grids/test_regular_grid.py index 09d5fce..95f1abe 100644 --- a/tests/test_grids/test_regular_grid.py +++ b/tests/test_grids/test_regular_grid.py @@ -14,6 +14,7 @@ # fsspec = None import xarray as xr +from tests.conftest import RGRID_FILES, SGRID_FILES, UGRID_FILES from xarray_subset_grid.grids.regular_grid import RegularGrid EXAMPLE_DATA = Path(__file__).parent.parent / "example_data" @@ -23,23 +24,20 @@ # It was created by the "OFS subsetter" - -def test_recognise(): +@pytest.mark.parametrize("test_file", RGRID_FILES) +def test_recognize(test_file): """ works for at least one file ... """ - ds = xr.open_dataset(EXAMPLE_DATA / "AMSEAS-subset.nc") + ds = xr.open_dataset(test_file) assert RegularGrid.recognize(ds) -@pytest.mark.parametrize("test_file", [ - # EXAMPLE_DATA / "arakawa_c_test_grid.nc", - EXAMPLE_DATA / "small_ugrid_zero_based.nc", - ]) -def test_recognise_not(test_file): +@pytest.mark.parametrize("test_file", UGRID_FILES + SGRID_FILES) +def test_recognize_not(test_file): """ - should not recognise an SGrid + should not recognize an SGrid """ ds = xr.open_dataset(test_file) diff --git a/tests/test_grids/test_ugrid.py b/tests/test_grids/test_ugrid.py index ebc84ae..1c0e99e 100644 --- a/tests/test_grids/test_ugrid.py +++ b/tests/test_grids/test_ugrid.py @@ -12,11 +12,48 @@ import pytest import xarray as xr +from tests.conftest import RGRID_FILES, SGRID_FILES, UGRID_FILES from xarray_subset_grid import Selector from xarray_subset_grid.grids import ugrid +from xarray_subset_grid.grids.ugrid import UGrid EXAMPLE_DATA = Path(__file__).parent.parent / "example_data" +@pytest.mark.parametrize("test_file", UGRID_FILES[:3]) +def test_recognize(test_file): + """ + works for at least one file ... + """ + print("testing: ", test_file) + ds = xr.open_dataset(test_file) + try: + ds.cf.cf_roles["mesh_topology"][0] + except KeyError: # no mesh variable + # Hacky way to deal with non-conforming examples + # This should be in a config somewhere, or ?? + if 'tris' in ds: + grid_top = {'face_node_connectivity': 'tris', + 'node_coordinates': ('lon', 'lat') + } + elif 'nv' in ds: + grid_top = {'face_node_connectivity': 'nv', + 'node_coordinates': ('lon', 'lat') + } + ds = ugrid.assign_ugrid_topology(ds, **grid_top) + + assert UGrid.recognize(ds) + + +@pytest.mark.parametrize("test_file", RGRID_FILES + SGRID_FILES) +def test_recognize_not(test_file): + """ + should not recognize an SGrid + """ + ds = xr.open_dataset(test_file) + + assert not UGrid.recognize(ds) + + # SFBOFS_subset1.nc is a smallish subset of the SFBOFS FVCOM model # It was created by the "OFS subsetter" diff --git a/xarray_subset_grid/grids/ugrid.py b/xarray_subset_grid/grids/ugrid.py index b2b0a8e..4428581 100644 --- a/xarray_subset_grid/grids/ugrid.py +++ b/xarray_subset_grid/grids/ugrid.py @@ -116,7 +116,7 @@ def recognize(ds: xr.Dataset) -> bool: try: mesh_key = ds.cf.cf_roles["mesh_topology"][0] mesh = ds[mesh_key] - except Exception: + except KeyError: return False return mesh.attrs.get("face_node_connectivity") is not None @@ -360,7 +360,7 @@ def assign_ugrid_topology( ``` grid_topology = {'node_coordinates': ('lon', 'lat'), 'face_node_connectivity': 'nv', - 'node_coordinates': ('lon', 'lat'), + 'edge_coordinates': ('lon', 'lat'), 'face_coordinates': ('lonc', 'latc'), }