From 86cf568651617345bfb57588064faca9e04cf6ae Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Thu, 2 Jan 2025 16:09:20 -0600 Subject: [PATCH 01/16] Initial Work --- test/test_subset.py | 25 +++++++++++++++++++++++-- uxarray/grid/grid.py | 26 ++++++++++++++++++++++---- uxarray/grid/slice.py | 16 ++++++++++------ uxarray/subset/grid_accessor.py | 27 ++++++++++++++++++--------- 4 files changed, 73 insertions(+), 21 deletions(-) diff --git a/test/test_subset.py b/test/test_subset.py index a1bfaed7f..2d5452fbe 100644 --- a/test/test_subset.py +++ b/test/test_subset.py @@ -104,8 +104,6 @@ def test_grid_bounding_box_subset(): bbox_antimeridian[0], bbox_antimeridian[1], element=element) - - def test_uxda_isel(): uxds = ux.open_dataset(GRID_PATHS[0], DATA_PATHS[0]) @@ -113,6 +111,7 @@ def test_uxda_isel(): assert len(sub) == 3 + def test_uxda_isel_with_coords(): uxds = ux.open_dataset(GRID_PATHS[0], DATA_PATHS[0]) uxds = uxds.assign_coords({"lon_face": uxds.uxgrid.face_lon}) @@ -120,3 +119,25 @@ def test_uxda_isel_with_coords(): assert "lon_face" in sub.coords assert len(sub.coords['lon_face']) == 3 + + +def test_inverse_face_indices(): + grid = ux.open_grid(GRID_PATHS[0]) + + # Test nearest neighbor subsetting + coord = [0, 0] + subset = grid.subset.nearest_neighbor(coord, k=1, element="face centers", inverse_indices=True) + + assert subset.inverse_face_indices is not None + + # Test bounding box subsetting + box = [(-10, 10), (-10, 10)] + subset = grid.subset.bounding_box(box[0], box[1], element="edge centers", inverse_indices=True) + + assert subset.inverse_face_indices is not None + + # Test bounding circle subsetting + center_coord = [0, 0] + subset = grid.subset.bounding_circle(center_coord, r=10, element="nodes", inverse_indices=True) + + assert subset.inverse_face_indices is not None diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index e1bac7d24..7bedb343e 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -1506,6 +1506,18 @@ def global_sphere_coverage(self): (i.e. contains no holes)""" return not self.partial_sphere_coverage + @property + def inverse_face_indices(self): + if self._is_subset: + return self._ds["inverse_face_indices"] + + @property + def _is_subset(self): + """Boolean indicator for whether the Grid is from a subset or not.""" + if "_is_subset" not in self._ds: + self._ds["_is_subset"] = False + return self._ds["_is_subset"] + def chunk(self, n_node="auto", n_edge="auto", n_face="auto"): """Converts all arrays to dask arrays with given chunks across grid dimensions in-place. @@ -2201,7 +2213,7 @@ def get_dual(self): return dual - def isel(self, **dim_kwargs): + def isel(self, inverse_indices=False, **dim_kwargs): """Indexes an unstructured grid along a given dimension (``n_node``, ``n_edge``, or ``n_face``) and returns a new grid. @@ -2226,13 +2238,19 @@ def isel(self, **dim_kwargs): raise ValueError("Indexing must be along a single dimension.") if "n_node" in dim_kwargs: - return _slice_node_indices(self, dim_kwargs["n_node"]) + return _slice_node_indices( + self, dim_kwargs["n_node"], inverse_indices=inverse_indices + ) elif "n_edge" in dim_kwargs: - return _slice_edge_indices(self, dim_kwargs["n_edge"]) + return _slice_edge_indices( + self, dim_kwargs["n_edge"], inverse_indices=inverse_indices + ) elif "n_face" in dim_kwargs: - return _slice_face_indices(self, dim_kwargs["n_face"]) + return _slice_face_indices( + self, dim_kwargs["n_face"], inverse_indices=inverse_indices + ) else: raise ValueError( diff --git a/uxarray/grid/slice.py b/uxarray/grid/slice.py index bc660332e..430389ba8 100644 --- a/uxarray/grid/slice.py +++ b/uxarray/grid/slice.py @@ -10,7 +10,7 @@ pass -def _slice_node_indices(grid, indices, inclusive=True): +def _slice_node_indices(grid, indices, inclusive=True, inverse_indices=False): """Slices (indexes) an unstructured grid given a list/array of node indices, returning a new Grid composed of elements that contain the nodes specified in the indices. @@ -33,10 +33,10 @@ def _slice_node_indices(grid, indices, inclusive=True): face_indices = np.unique(grid.node_face_connectivity.values[indices].ravel()) face_indices = face_indices[face_indices != INT_FILL_VALUE] - return _slice_face_indices(grid, face_indices) + return _slice_face_indices(grid, face_indices, inverse_indices=inverse_indices) -def _slice_edge_indices(grid, indices, inclusive=True): +def _slice_edge_indices(grid, indices, inclusive=True, inverse_indices=False): """Slices (indexes) an unstructured grid given a list/array of edge indices, returning a new Grid composed of elements that contain the edges specified in the indices. @@ -59,10 +59,10 @@ def _slice_edge_indices(grid, indices, inclusive=True): face_indices = np.unique(grid.edge_face_connectivity.values[indices].ravel()) face_indices = face_indices[face_indices != INT_FILL_VALUE] - return _slice_face_indices(grid, face_indices) + return _slice_face_indices(grid, face_indices, inverse_indices=inverse_indices) -def _slice_face_indices(grid, indices, inclusive=True): +def _slice_face_indices(grid, indices, inclusive=True, inverse_indices=False): """Slices (indexes) an unstructured grid given a list/array of face indices, returning a new Grid composed of elements that contain the faces specified in the indices. @@ -77,7 +77,6 @@ def _slice_face_indices(grid, indices, inclusive=True): Whether to perform inclusive (i.e. elements must contain at least one desired feature from a slice) as opposed to exclusive (i.e elements be made up all desired features from a slice) """ - if inclusive is False: raise ValueError("Exclusive slicing is not yet supported.") @@ -132,4 +131,9 @@ def _slice_face_indices(grid, indices, inclusive=True): # drop any conn that would require re-computation ds = ds.drop_vars(conn_name) + if inverse_indices: + ds["inverse_face_indices"] = indices + + ds["_is_subset"] = True + return Grid.from_dataset(ds, source_grid_spec=grid.source_grid_spec) diff --git a/uxarray/subset/grid_accessor.py b/uxarray/subset/grid_accessor.py index 60dc8c800..c80686228 100644 --- a/uxarray/subset/grid_accessor.py +++ b/uxarray/subset/grid_accessor.py @@ -33,6 +33,7 @@ def bounding_box( lat_bounds: Union[Tuple, List, np.ndarray], element: Optional[str] = "nodes", method: Optional[str] = "coords", + inverse_indices=False, **kwargs, ): """Subsets an unstructured grid between two latitude and longitude @@ -53,6 +54,8 @@ def bounding_box( face centers, or edge centers lie within the bounds. element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` + inverse_indices : bool + Flag to indicate whether to store the original grids face indices for later use """ if method == "coords": @@ -101,11 +104,11 @@ def bounding_box( ) if element == "nodes": - return self.uxgrid.isel(n_node=indices) + return self.uxgrid.isel(inverse_indices, n_node=indices) elif element == "face centers": - return self.uxgrid.isel(n_face=indices) + return self.uxgrid.isel(inverse_indices, n_face=indices) elif element == "edge centers": - return self.uxgrid.isel(n_edge=indices) + return self.uxgrid.isel(inverse_indices, n_edge=indices) else: raise ValueError(f"Method '{method}' not supported.") @@ -115,6 +118,7 @@ def bounding_circle( center_coord: Union[Tuple, List, np.ndarray], r: Union[float, int], element: Optional[str] = "nodes", + inverse_indices=False, **kwargs, ): """Subsets an unstructured grid by returning all elements within some @@ -128,6 +132,8 @@ def bounding_circle( Radius of bounding circle (in degrees) element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` + inverse_indices : bool + Flag to indicate whether to store the original grids face indices for later use """ coords = np.asarray(center_coord) @@ -141,13 +147,14 @@ def bounding_circle( f"No elements founding within the bounding circle with radius {r} when querying {element}" ) - return self._index_grid(ind, element) + return self._index_grid(ind, element, inverse_indices) def nearest_neighbor( self, center_coord: Union[Tuple, List, np.ndarray], k: int, element: Optional[str] = "nodes", + inverse_indices=False, **kwargs, ): """Subsets an unstructured grid by returning the ``k`` closest @@ -161,6 +168,8 @@ def nearest_neighbor( Number of neighbors to query element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` + inverse_indices : bool + Flag to indicate whether to store the original grids face indices for later use """ coords = np.asarray(center_coord) @@ -169,7 +178,7 @@ def nearest_neighbor( _, ind = tree.query(coords, k) - return self._index_grid(ind, element) + return self._index_grid(ind, element, inverse_indices=inverse_indices) def _get_tree(self, coords, tree_type): """Internal helper for obtaining the desired KDTree or BallTree.""" @@ -187,12 +196,12 @@ def _get_tree(self, coords, tree_type): return tree - def _index_grid(self, ind, tree_type): + def _index_grid(self, ind, tree_type, inverse_indices=False): """Internal helper for indexing a grid with indices based off the provided tree type.""" if tree_type == "nodes": - return self.uxgrid.isel(n_node=ind) + return self.uxgrid.isel(inverse_indices, n_node=ind) elif tree_type == "edge centers": - return self.uxgrid.isel(n_edge=ind) + return self.uxgrid.isel(inverse_indices, n_edge=ind) else: - return self.uxgrid.isel(n_face=ind) + return self.uxgrid.isel(inverse_indices, n_face=ind) From 5f6285d4322581e004058e8b62528cb2d266d597 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Fri, 3 Jan 2025 09:30:07 -0600 Subject: [PATCH 02/16] Addressed review comments --- uxarray/grid/grid.py | 22 ++++++++++++---------- uxarray/grid/slice.py | 4 +--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 7bedb343e..8e2a64775 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -160,6 +160,7 @@ def __init__( grid_ds: xr.Dataset, source_grid_spec: Optional[str] = None, source_dims_dict: Optional[dict] = {}, + is_subset=False, ): # check if inputted dataset is a minimum representable 2D UGRID unstructured grid if not _validate_minimum_ugrid(grid_ds): @@ -191,6 +192,7 @@ def __init__( # initialize attributes self._antimeridian_face_indices = None self._ds.assign_attrs({"source_grid_spec": self.source_grid_spec}) + self.is_subset = is_subset # cached parameters for GeoDataFrame conversions self._gdf_cached_parameters = { @@ -242,7 +244,9 @@ def __init__( cross_section = UncachedAccessor(GridCrossSectionAccessor) @classmethod - def from_dataset(cls, dataset, use_dual: Optional[bool] = False, **kwargs): + def from_dataset( + cls, dataset, use_dual: Optional[bool] = False, is_subset=False, **kwargs + ): """Constructs a ``Grid`` object from a dataset. Parameters @@ -301,7 +305,7 @@ def from_dataset(cls, dataset, use_dual: Optional[bool] = False, **kwargs): except TypeError: raise ValueError("Unsupported Grid Format") - return cls(grid_ds, source_grid_spec, source_dims_dict) + return cls(grid_ds, source_grid_spec, source_dims_dict, is_subset=is_subset) @classmethod def from_file( @@ -1508,15 +1512,13 @@ def global_sphere_coverage(self): @property def inverse_face_indices(self): - if self._is_subset: + """Indices for a subset that map each face in the subset back to the original grid""" + if self.is_subset: return self._ds["inverse_face_indices"] - - @property - def _is_subset(self): - """Boolean indicator for whether the Grid is from a subset or not.""" - if "_is_subset" not in self._ds: - self._ds["_is_subset"] = False - return self._ds["_is_subset"] + else: + raise Exception( + "Grid is not a subset, therefore no inverse face indices exist" + ) def chunk(self, n_node="auto", n_edge="auto", n_face="auto"): """Converts all arrays to dask arrays with given chunks across grid diff --git a/uxarray/grid/slice.py b/uxarray/grid/slice.py index 430389ba8..c748ce588 100644 --- a/uxarray/grid/slice.py +++ b/uxarray/grid/slice.py @@ -134,6 +134,4 @@ def _slice_face_indices(grid, indices, inclusive=True, inverse_indices=False): if inverse_indices: ds["inverse_face_indices"] = indices - ds["_is_subset"] = True - - return Grid.from_dataset(ds, source_grid_spec=grid.source_grid_spec) + return Grid.from_dataset(ds, source_grid_spec=grid.source_grid_spec, is_subset=True) From dcd717f23ef022a58e54dc36d9cf3e21192761be Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Fri, 3 Jan 2025 09:31:40 -0600 Subject: [PATCH 03/16] Updated doc string --- uxarray/grid/grid.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 8e2a64775..36b239151 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -256,6 +256,8 @@ def from_dataset( containing ASCII files represents a FESOM2 grid. use_dual : bool, default=False When reading in MPAS formatted datasets, indicates whether to use the Dual Mesh + is_subset : bool, default=False + Bool flag to indicate whether a grid is a subset """ if isinstance(dataset, xr.Dataset): From d23058f60ae6ae638c864d26d41e7cc3fb97dcd4 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Fri, 3 Jan 2025 15:58:49 -0600 Subject: [PATCH 04/16] Added inverse_indices support for data arrays and cross sections --- uxarray/core/dataarray.py | 14 ++++++++++---- uxarray/cross_sections/dataarray_accessor.py | 12 ++++++++---- uxarray/cross_sections/grid_accessor.py | 14 ++++++++++++-- uxarray/subset/dataarray_accessor.py | 15 ++++++++++++--- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/uxarray/core/dataarray.py b/uxarray/core/dataarray.py index 216b2309c..bfec83280 100644 --- a/uxarray/core/dataarray.py +++ b/uxarray/core/dataarray.py @@ -1035,7 +1035,7 @@ def _edge_centered(self) -> bool: "n_edge" dimension)""" return "n_edge" in self.dims - def isel(self, ignore_grid=False, *args, **kwargs): + def isel(self, ignore_grid=False, inverse_indices=False, *args, **kwargs): """Grid-informed implementation of xarray's ``isel`` method, which enables indexing across grid dimensions. @@ -1069,11 +1069,17 @@ def isel(self, ignore_grid=False, *args, **kwargs): raise ValueError("Only one grid dimension can be sliced at a time") if "n_node" in kwargs: - sliced_grid = self.uxgrid.isel(n_node=kwargs["n_node"]) + sliced_grid = self.uxgrid.isel( + n_node=kwargs["n_node"], inverse_indices=inverse_indices + ) elif "n_edge" in kwargs: - sliced_grid = self.uxgrid.isel(n_edge=kwargs["n_edge"]) + sliced_grid = self.uxgrid.isel( + n_edge=kwargs["n_edge"], inverse_indices=inverse_indices + ) else: - sliced_grid = self.uxgrid.isel(n_face=kwargs["n_face"]) + sliced_grid = self.uxgrid.isel( + n_face=kwargs["n_face"], inverse_indices=inverse_indices + ) return self._slice_from_grid(sliced_grid) diff --git a/uxarray/cross_sections/dataarray_accessor.py b/uxarray/cross_sections/dataarray_accessor.py index 6f82a8f2e..0c9166fe6 100644 --- a/uxarray/cross_sections/dataarray_accessor.py +++ b/uxarray/cross_sections/dataarray_accessor.py @@ -22,7 +22,7 @@ def __repr__(self): return prefix + methods_heading - def constant_latitude(self, lat: float): + def constant_latitude(self, lat: float, inverse_indices: bool = False): """Extracts a cross-section of the data array by selecting all faces that intersect with a specified line of constant latitude. @@ -31,6 +31,8 @@ def constant_latitude(self, lat: float): lat : float The latitude at which to extract the cross-section, in degrees. Must be between -90.0 and 90.0 + inverse_indices : bool, optional + If True, stores the original grid indices Returns ------- @@ -60,9 +62,9 @@ def constant_latitude(self, lat: float): faces = self.uxda.uxgrid.get_faces_at_constant_latitude(lat) - return self.uxda.isel(n_face=faces) + return self.uxda.isel(n_face=faces, inverse_indices=inverse_indices) - def constant_longitude(self, lon: float): + def constant_longitude(self, lon: float, inverse_indices: bool = False): """Extracts a cross-section of the data array by selecting all faces that intersect with a specified line of constant longitude. @@ -71,6 +73,8 @@ def constant_longitude(self, lon: float): lon : float The latitude at which to extract the cross-section, in degrees. Must be between -180.0 and 180.0 + inverse_indices : bool, optional + If True, stores the original grid indices Returns ------- @@ -102,7 +106,7 @@ def constant_longitude(self, lon: float): lon, ) - return self.uxda.isel(n_face=faces) + return self.uxda.isel(n_face=faces, inverse_indices=inverse_indices) def gca(self, *args, **kwargs): raise NotImplementedError diff --git a/uxarray/cross_sections/grid_accessor.py b/uxarray/cross_sections/grid_accessor.py index ee30bd913..4b9ce82f2 100644 --- a/uxarray/cross_sections/grid_accessor.py +++ b/uxarray/cross_sections/grid_accessor.py @@ -25,6 +25,7 @@ def constant_latitude( self, lat: float, return_face_indices: bool = False, + inverse_indices: bool = False, ): """Extracts a cross-section of the grid by selecting all faces that intersect with a specified line of constant latitude. @@ -36,6 +37,8 @@ def constant_latitude( Must be between -90.0 and 90.0 return_face_indices : bool, optional If True, also returns the indices of the faces that intersect with the line of constant latitude. + inverse_indices : bool, optional + If True, stores the original grid indices Returns ------- @@ -66,7 +69,9 @@ def constant_latitude( if len(faces) == 0: raise ValueError(f"No intersections found at lat={lat}.") - grid_at_constant_lat = self.uxgrid.isel(n_face=faces) + grid_at_constant_lat = self.uxgrid.isel( + n_face=faces, inverse_indices=inverse_indices + ) if return_face_indices: return grid_at_constant_lat, faces @@ -77,6 +82,7 @@ def constant_longitude( self, lon: float, return_face_indices: bool = False, + inverse_indices: bool = False, ): """Extracts a cross-section of the grid by selecting all faces that intersect with a specified line of constant longitude. @@ -88,6 +94,8 @@ def constant_longitude( Must be between -90.0 and 90.0 return_face_indices : bool, optional If True, also returns the indices of the faces that intersect with the line of constant longitude. + inverse_indices : bool, optional + If True, stores the original grid indices Returns ------- @@ -117,7 +125,9 @@ def constant_longitude( if len(faces) == 0: raise ValueError(f"No intersections found at lon={lon}") - grid_at_constant_lon = self.uxgrid.isel(n_face=faces) + grid_at_constant_lon = self.uxgrid.isel( + n_face=faces, inverse_indices=inverse_indices + ) if return_face_indices: return grid_at_constant_lon, faces diff --git a/uxarray/subset/dataarray_accessor.py b/uxarray/subset/dataarray_accessor.py index 3624e1e3e..b058505e6 100644 --- a/uxarray/subset/dataarray_accessor.py +++ b/uxarray/subset/dataarray_accessor.py @@ -33,6 +33,7 @@ def bounding_box( lat_bounds: Union[Tuple, List, np.ndarray], element: Optional[str] = "nodes", method: Optional[str] = "coords", + inverse_indices=False, **kwargs, ): """Subsets an unstructured grid between two latitude and longitude @@ -53,9 +54,11 @@ def bounding_box( face centers, or edge centers lie within the bounds. element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` + inverse_indices : bool + Flag to indicate whether to store the original grids face indices for later use """ grid = self.uxda.uxgrid.subset.bounding_box( - lon_bounds, lat_bounds, element, method + lon_bounds, lat_bounds, element, method, inverse_indices=inverse_indices ) return self.uxda._slice_from_grid(grid) @@ -65,6 +68,7 @@ def bounding_circle( center_coord: Union[Tuple, List, np.ndarray], r: Union[float, int], element: Optional[str] = "nodes", + inverse_indices=False, **kwargs, ): """Subsets an unstructured grid by returning all elements within some @@ -78,9 +82,11 @@ def bounding_circle( Radius of bounding circle (in degrees) element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` + inverse_indices : bool + Flag to indicate whether to store the original grids face indices for later use """ grid = self.uxda.uxgrid.subset.bounding_circle( - center_coord, r, element, **kwargs + center_coord, r, element, inverse_indices=inverse_indices**kwargs ) return self.uxda._slice_from_grid(grid) @@ -89,6 +95,7 @@ def nearest_neighbor( center_coord: Union[Tuple, List, np.ndarray], k: int, element: Optional[str] = "nodes", + inverse_indices=False, **kwargs, ): """Subsets an unstructured grid by returning the ``k`` closest @@ -102,10 +109,12 @@ def nearest_neighbor( Number of neighbors to query element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` + inverse_indices : bool + Flag to indicate whether to store the original grids face indices for later use """ grid = self.uxda.uxgrid.subset.nearest_neighbor( - center_coord, k, element, **kwargs + center_coord, k, element, inverse_indices=inverse_indices, **kwargs ) return self.uxda._slice_from_grid(grid) From ae151aacf94ea7ec5ac3fe90aceb9758177dd6d4 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Mon, 6 Jan 2025 13:41:27 -0600 Subject: [PATCH 05/16] Added ability to choose which inverse_indices to store, stores as ds --- test/test_subset.py | 14 ++++--- uxarray/cross_sections/dataarray_accessor.py | 10 +++-- uxarray/cross_sections/grid_accessor.py | 6 +-- uxarray/grid/grid.py | 39 +++++++++++++---- uxarray/grid/slice.py | 44 +++++++++++++++++--- uxarray/subset/dataarray_accessor.py | 10 ++--- uxarray/subset/grid_accessor.py | 8 ++-- 7 files changed, 99 insertions(+), 32 deletions(-) diff --git a/test/test_subset.py b/test/test_subset.py index 2d5452fbe..b5fa2fda7 100644 --- a/test/test_subset.py +++ b/test/test_subset.py @@ -128,16 +128,20 @@ def test_inverse_face_indices(): coord = [0, 0] subset = grid.subset.nearest_neighbor(coord, k=1, element="face centers", inverse_indices=True) - assert subset.inverse_face_indices is not None + assert subset.inverse_indices is not None # Test bounding box subsetting box = [(-10, 10), (-10, 10)] - subset = grid.subset.bounding_box(box[0], box[1], element="edge centers", inverse_indices=True) + subset = grid.subset.bounding_box(box[0], box[1], element="face centers", inverse_indices=True) - assert subset.inverse_face_indices is not None + assert subset.inverse_indices is not None # Test bounding circle subsetting center_coord = [0, 0] - subset = grid.subset.bounding_circle(center_coord, r=10, element="nodes", inverse_indices=True) + subset = grid.subset.bounding_circle(center_coord, r=10, element="face centers", inverse_indices=True) - assert subset.inverse_face_indices is not None + assert subset.inverse_indices is not None + + # Ensure code raises exceptions when the element is edges or nodes + assert pytest.raises(Exception, grid.subset.bounding_circle, center_coord, r=10, element="edge centers", inverse_indices=True) + assert pytest.raises(Exception, grid.subset.bounding_circle, center_coord, r=10, element="node centers", inverse_indices=True) diff --git a/uxarray/cross_sections/dataarray_accessor.py b/uxarray/cross_sections/dataarray_accessor.py index 0c9166fe6..48a864a74 100644 --- a/uxarray/cross_sections/dataarray_accessor.py +++ b/uxarray/cross_sections/dataarray_accessor.py @@ -1,7 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union, List, Set if TYPE_CHECKING: pass @@ -22,7 +22,9 @@ def __repr__(self): return prefix + methods_heading - def constant_latitude(self, lat: float, inverse_indices: bool = False): + def constant_latitude( + self, lat: float, inverse_indices: Union[List[str], Set[str], bool] = False + ): """Extracts a cross-section of the data array by selecting all faces that intersect with a specified line of constant latitude. @@ -64,7 +66,9 @@ def constant_latitude(self, lat: float, inverse_indices: bool = False): return self.uxda.isel(n_face=faces, inverse_indices=inverse_indices) - def constant_longitude(self, lon: float, inverse_indices: bool = False): + def constant_longitude( + self, lon: float, inverse_indices: Union[List[str], Set[str], bool] = False + ): """Extracts a cross-section of the data array by selecting all faces that intersect with a specified line of constant longitude. diff --git a/uxarray/cross_sections/grid_accessor.py b/uxarray/cross_sections/grid_accessor.py index 4b9ce82f2..36b91521a 100644 --- a/uxarray/cross_sections/grid_accessor.py +++ b/uxarray/cross_sections/grid_accessor.py @@ -1,7 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union, List, Set if TYPE_CHECKING: from uxarray.grid import Grid @@ -25,7 +25,7 @@ def constant_latitude( self, lat: float, return_face_indices: bool = False, - inverse_indices: bool = False, + inverse_indices: Union[List[str], Set[str], bool] = False, ): """Extracts a cross-section of the grid by selecting all faces that intersect with a specified line of constant latitude. @@ -82,7 +82,7 @@ def constant_longitude( self, lon: float, return_face_indices: bool = False, - inverse_indices: bool = False, + inverse_indices: Union[List[str], Set[str], bool] = False, ): """Extracts a cross-section of the grid by selecting all faces that intersect with a specified line of constant longitude. diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 36b239151..51a11f94e 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -9,6 +9,8 @@ from typing import ( Optional, Union, + List, + Set, ) # reader and writer imports @@ -161,6 +163,7 @@ def __init__( source_grid_spec: Optional[str] = None, source_dims_dict: Optional[dict] = {}, is_subset=False, + inverse_indices: Optional[xr.Dataset] = None, ): # check if inputted dataset is a minimum representable 2D UGRID unstructured grid if not _validate_minimum_ugrid(grid_ds): @@ -194,6 +197,9 @@ def __init__( self._ds.assign_attrs({"source_grid_spec": self.source_grid_spec}) self.is_subset = is_subset + if inverse_indices is not None: + self.inverse_indices = inverse_indices + # cached parameters for GeoDataFrame conversions self._gdf_cached_parameters = { "gdf": None, @@ -244,9 +250,7 @@ def __init__( cross_section = UncachedAccessor(GridCrossSectionAccessor) @classmethod - def from_dataset( - cls, dataset, use_dual: Optional[bool] = False, is_subset=False, **kwargs - ): + def from_dataset(cls, dataset, use_dual: Optional[bool] = False, **kwargs): """Constructs a ``Grid`` object from a dataset. Parameters @@ -307,7 +311,13 @@ def from_dataset( except TypeError: raise ValueError("Unsupported Grid Format") - return cls(grid_ds, source_grid_spec, source_dims_dict, is_subset=is_subset) + return cls( + grid_ds, + source_grid_spec, + source_dims_dict, + is_subset=kwargs.get("is_subset", False), + inverse_indices=kwargs.get("inverse_indices"), + ) @classmethod def from_file( @@ -1513,15 +1523,20 @@ def global_sphere_coverage(self): return not self.partial_sphere_coverage @property - def inverse_face_indices(self): + def inverse_indices(self): """Indices for a subset that map each face in the subset back to the original grid""" if self.is_subset: - return self._ds["inverse_face_indices"] + return self._inverse_indices else: raise Exception( "Grid is not a subset, therefore no inverse face indices exist" ) + @inverse_indices.setter + def inverse_indices(self, value): + assert isinstance(value, xr.Dataset) + self._inverse_indices = value + def chunk(self, n_node="auto", n_edge="auto", n_face="auto"): """Converts all arrays to dask arrays with given chunks across grid dimensions in-place. @@ -2217,7 +2232,9 @@ def get_dual(self): return dual - def isel(self, inverse_indices=False, **dim_kwargs): + def isel( + self, inverse_indices: Union[List[str], Set[str], bool] = False, **dim_kwargs + ): """Indexes an unstructured grid along a given dimension (``n_node``, ``n_edge``, or ``n_face``) and returns a new grid. @@ -2242,11 +2259,19 @@ def isel(self, inverse_indices=False, **dim_kwargs): raise ValueError("Indexing must be along a single dimension.") if "n_node" in dim_kwargs: + if inverse_indices: + raise Exception( + "Inverse indices are not yet supported for node selection, please use face centers" + ) return _slice_node_indices( self, dim_kwargs["n_node"], inverse_indices=inverse_indices ) elif "n_edge" in dim_kwargs: + if inverse_indices: + raise Exception( + "Inverse indices are not yet supported for edge selection, please use face centers" + ) return _slice_edge_indices( self, dim_kwargs["n_edge"], inverse_indices=inverse_indices ) diff --git a/uxarray/grid/slice.py b/uxarray/grid/slice.py index c748ce588..3f7416d84 100644 --- a/uxarray/grid/slice.py +++ b/uxarray/grid/slice.py @@ -4,13 +4,18 @@ import xarray as xr from uxarray.constants import INT_FILL_VALUE, INT_DTYPE -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union, List, Set if TYPE_CHECKING: pass -def _slice_node_indices(grid, indices, inclusive=True, inverse_indices=False): +def _slice_node_indices( + grid, + indices, + inclusive=True, + inverse_indices: Union[List[str], Set[str], bool] = False, +): """Slices (indexes) an unstructured grid given a list/array of node indices, returning a new Grid composed of elements that contain the nodes specified in the indices. @@ -36,7 +41,12 @@ def _slice_node_indices(grid, indices, inclusive=True, inverse_indices=False): return _slice_face_indices(grid, face_indices, inverse_indices=inverse_indices) -def _slice_edge_indices(grid, indices, inclusive=True, inverse_indices=False): +def _slice_edge_indices( + grid, + indices, + inclusive=True, + inverse_indices: Union[List[str], Set[str], bool] = False, +): """Slices (indexes) an unstructured grid given a list/array of edge indices, returning a new Grid composed of elements that contain the edges specified in the indices. @@ -62,7 +72,12 @@ def _slice_edge_indices(grid, indices, inclusive=True, inverse_indices=False): return _slice_face_indices(grid, face_indices, inverse_indices=inverse_indices) -def _slice_face_indices(grid, indices, inclusive=True, inverse_indices=False): +def _slice_face_indices( + grid, + indices, + inclusive=True, + inverse_indices: Union[List[str], Set[str], bool] = False, +): """Slices (indexes) an unstructured grid given a list/array of face indices, returning a new Grid composed of elements that contain the faces specified in the indices. @@ -132,6 +147,25 @@ def _slice_face_indices(grid, indices, inclusive=True, inverse_indices=False): ds = ds.drop_vars(conn_name) if inverse_indices: - ds["inverse_face_indices"] = indices + inverse_indices_ds = xr.Dataset() + + index_types = { + "face centers": face_indices, + "edge centers": edge_indices, + "nodes": node_indices, + } + if isinstance(inverse_indices, bool): + inverse_indices_ds["face centers"] = face_indices + else: + for index_type in inverse_indices[0]: + if index_type in index_types: + inverse_indices_ds[index_type] = index_types[index_type] + + return Grid.from_dataset( + ds, + source_grid_spec=grid.source_grid_spec, + is_subset=True, + inverse_indices=inverse_indices_ds, + ) return Grid.from_dataset(ds, source_grid_spec=grid.source_grid_spec, is_subset=True) diff --git a/uxarray/subset/dataarray_accessor.py b/uxarray/subset/dataarray_accessor.py index b058505e6..b71e78f08 100644 --- a/uxarray/subset/dataarray_accessor.py +++ b/uxarray/subset/dataarray_accessor.py @@ -2,7 +2,7 @@ import numpy as np -from typing import TYPE_CHECKING, Union, Tuple, List, Optional +from typing import TYPE_CHECKING, Union, Tuple, List, Optional, Set if TYPE_CHECKING: pass @@ -33,7 +33,7 @@ def bounding_box( lat_bounds: Union[Tuple, List, np.ndarray], element: Optional[str] = "nodes", method: Optional[str] = "coords", - inverse_indices=False, + inverse_indices: Union[List[str], Set[str], bool] = False, **kwargs, ): """Subsets an unstructured grid between two latitude and longitude @@ -68,7 +68,7 @@ def bounding_circle( center_coord: Union[Tuple, List, np.ndarray], r: Union[float, int], element: Optional[str] = "nodes", - inverse_indices=False, + inverse_indices: Union[List[str], Set[str], bool] = False, **kwargs, ): """Subsets an unstructured grid by returning all elements within some @@ -86,7 +86,7 @@ def bounding_circle( Flag to indicate whether to store the original grids face indices for later use """ grid = self.uxda.uxgrid.subset.bounding_circle( - center_coord, r, element, inverse_indices=inverse_indices**kwargs + center_coord, r, element, inverse_indices=inverse_indices, **kwargs ) return self.uxda._slice_from_grid(grid) @@ -95,7 +95,7 @@ def nearest_neighbor( center_coord: Union[Tuple, List, np.ndarray], k: int, element: Optional[str] = "nodes", - inverse_indices=False, + inverse_indices: Union[List[str], Set[str], bool] = False, **kwargs, ): """Subsets an unstructured grid by returning the ``k`` closest diff --git a/uxarray/subset/grid_accessor.py b/uxarray/subset/grid_accessor.py index c80686228..2dcb954e2 100644 --- a/uxarray/subset/grid_accessor.py +++ b/uxarray/subset/grid_accessor.py @@ -2,7 +2,7 @@ import numpy as np -from typing import TYPE_CHECKING, Union, Tuple, List, Optional +from typing import TYPE_CHECKING, Union, Tuple, List, Optional, Set if TYPE_CHECKING: from uxarray.grid import Grid @@ -33,7 +33,7 @@ def bounding_box( lat_bounds: Union[Tuple, List, np.ndarray], element: Optional[str] = "nodes", method: Optional[str] = "coords", - inverse_indices=False, + inverse_indices: Union[List[str], Set[str], bool] = False, **kwargs, ): """Subsets an unstructured grid between two latitude and longitude @@ -118,7 +118,7 @@ def bounding_circle( center_coord: Union[Tuple, List, np.ndarray], r: Union[float, int], element: Optional[str] = "nodes", - inverse_indices=False, + inverse_indices: Union[List[str], Set[str], bool] = False, **kwargs, ): """Subsets an unstructured grid by returning all elements within some @@ -154,7 +154,7 @@ def nearest_neighbor( center_coord: Union[Tuple, List, np.ndarray], k: int, element: Optional[str] = "nodes", - inverse_indices=False, + inverse_indices: Union[List[str], Set[str], bool] = False, **kwargs, ): """Subsets an unstructured grid by returning the ``k`` closest From aaf49ab165af5b72dac0f4b34fba904e68ae8422 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Tue, 7 Jan 2025 09:58:54 -0600 Subject: [PATCH 06/16] updated grid, doc strings, api, test cases --- docs/api.rst | 1 + test/test_subset.py | 6 +++++- uxarray/cross_sections/dataarray_accessor.py | 10 ++++++---- uxarray/cross_sections/grid_accessor.py | 10 ++++++---- uxarray/grid/grid.py | 16 ++++++++++------ uxarray/grid/slice.py | 5 +++-- uxarray/subset/dataarray_accessor.py | 15 +++++++++------ uxarray/subset/grid_accessor.py | 15 +++++++++------ 8 files changed, 49 insertions(+), 29 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index f9146e69f..10307a16f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -54,6 +54,7 @@ Indexing :toctree: generated/ Grid.isel + Grid.inverse_indices Dimensions ~~~~~~~~~~ diff --git a/test/test_subset.py b/test/test_subset.py index b5fa2fda7..184d0fcb0 100644 --- a/test/test_subset.py +++ b/test/test_subset.py @@ -121,7 +121,7 @@ def test_uxda_isel_with_coords(): assert len(sub.coords['lon_face']) == 3 -def test_inverse_face_indices(): +def test_inverse_indices(): grid = ux.open_grid(GRID_PATHS[0]) # Test nearest neighbor subsetting @@ -145,3 +145,7 @@ def test_inverse_face_indices(): # Ensure code raises exceptions when the element is edges or nodes assert pytest.raises(Exception, grid.subset.bounding_circle, center_coord, r=10, element="edge centers", inverse_indices=True) assert pytest.raises(Exception, grid.subset.bounding_circle, center_coord, r=10, element="node centers", inverse_indices=True) + + # Test isel directly + subset = grid.isel(n_face=[1], inverse_indices=True) + assert subset.inverse_indices is not None diff --git a/uxarray/cross_sections/dataarray_accessor.py b/uxarray/cross_sections/dataarray_accessor.py index 48a864a74..fd0e81e84 100644 --- a/uxarray/cross_sections/dataarray_accessor.py +++ b/uxarray/cross_sections/dataarray_accessor.py @@ -33,8 +33,9 @@ def constant_latitude( lat : float The latitude at which to extract the cross-section, in degrees. Must be between -90.0 and 90.0 - inverse_indices : bool, optional - If True, stores the original grid indices + inverse_indices : Union[List[str], Set[str], bool], optional + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) Returns ------- @@ -77,8 +78,9 @@ def constant_longitude( lon : float The latitude at which to extract the cross-section, in degrees. Must be between -180.0 and 180.0 - inverse_indices : bool, optional - If True, stores the original grid indices + inverse_indices : Union[List[str], Set[str], bool], optional + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) Returns ------- diff --git a/uxarray/cross_sections/grid_accessor.py b/uxarray/cross_sections/grid_accessor.py index 36b91521a..17fee3f8e 100644 --- a/uxarray/cross_sections/grid_accessor.py +++ b/uxarray/cross_sections/grid_accessor.py @@ -37,8 +37,9 @@ def constant_latitude( Must be between -90.0 and 90.0 return_face_indices : bool, optional If True, also returns the indices of the faces that intersect with the line of constant latitude. - inverse_indices : bool, optional - If True, stores the original grid indices + inverse_indices : Union[List[str], Set[str], bool], optional + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) Returns ------- @@ -94,8 +95,9 @@ def constant_longitude( Must be between -90.0 and 90.0 return_face_indices : bool, optional If True, also returns the indices of the faces that intersect with the line of constant longitude. - inverse_indices : bool, optional - If True, stores the original grid indices + inverse_indices : Union[List[str], Set[str], bool], optional + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) Returns ------- diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 51a11f94e..03a54b191 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -139,6 +139,12 @@ class Grid: source_dims_dict : dict, default={} Mapping of dimensions from the source dataset to their UGRID equivalent (i.e. {nCell : n_face}) + is_subset : bool, default=False + Flag to mark if the grid is a subset or not + + inverse_indices: xr.Dataset, defaul=None + A dataset of indices that correspond to the original grid, if the grid being constructed is a subset + Examples ---------- @@ -198,7 +204,7 @@ def __init__( self.is_subset = is_subset if inverse_indices is not None: - self.inverse_indices = inverse_indices + self._inverse_indices = inverse_indices # cached parameters for GeoDataFrame conversions self._gdf_cached_parameters = { @@ -1532,11 +1538,6 @@ def inverse_indices(self): "Grid is not a subset, therefore no inverse face indices exist" ) - @inverse_indices.setter - def inverse_indices(self, value): - assert isinstance(value, xr.Dataset) - self._inverse_indices = value - def chunk(self, n_node="auto", n_edge="auto", n_face="auto"): """Converts all arrays to dask arrays with given chunks across grid dimensions in-place. @@ -2244,6 +2245,9 @@ def isel( exclusive and clipped indexing is in the works. Parameters + inverse_indices : Union[List[str], Set[str], bool], default=False + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) **dims_kwargs: kwargs Dimension to index, one of ['n_node', 'n_edge', 'n_face'] diff --git a/uxarray/grid/slice.py b/uxarray/grid/slice.py index 3f7416d84..9045e0be7 100644 --- a/uxarray/grid/slice.py +++ b/uxarray/grid/slice.py @@ -14,7 +14,6 @@ def _slice_node_indices( grid, indices, inclusive=True, - inverse_indices: Union[List[str], Set[str], bool] = False, ): """Slices (indexes) an unstructured grid given a list/array of node indices, returning a new Grid composed of elements that contain the nodes @@ -45,7 +44,6 @@ def _slice_edge_indices( grid, indices, inclusive=True, - inverse_indices: Union[List[str], Set[str], bool] = False, ): """Slices (indexes) an unstructured grid given a list/array of edge indices, returning a new Grid composed of elements that contain the edges @@ -91,6 +89,9 @@ def _slice_face_indices( inclusive: bool Whether to perform inclusive (i.e. elements must contain at least one desired feature from a slice) as opposed to exclusive (i.e elements be made up all desired features from a slice) + inverse_indices : Union[List[str], Set[str], bool], optional + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) """ if inclusive is False: raise ValueError("Exclusive slicing is not yet supported.") diff --git a/uxarray/subset/dataarray_accessor.py b/uxarray/subset/dataarray_accessor.py index b71e78f08..23609ba65 100644 --- a/uxarray/subset/dataarray_accessor.py +++ b/uxarray/subset/dataarray_accessor.py @@ -54,8 +54,9 @@ def bounding_box( face centers, or edge centers lie within the bounds. element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` - inverse_indices : bool - Flag to indicate whether to store the original grids face indices for later use + inverse_indices : Union[List[str], Set[str], bool], optional + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) """ grid = self.uxda.uxgrid.subset.bounding_box( lon_bounds, lat_bounds, element, method, inverse_indices=inverse_indices @@ -82,8 +83,9 @@ def bounding_circle( Radius of bounding circle (in degrees) element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` - inverse_indices : bool - Flag to indicate whether to store the original grids face indices for later use + inverse_indices : Union[List[str], Set[str], bool], optional + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) """ grid = self.uxda.uxgrid.subset.bounding_circle( center_coord, r, element, inverse_indices=inverse_indices, **kwargs @@ -109,8 +111,9 @@ def nearest_neighbor( Number of neighbors to query element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` - inverse_indices : bool - Flag to indicate whether to store the original grids face indices for later use + inverse_indices : Union[List[str], Set[str], bool], optional + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) """ grid = self.uxda.uxgrid.subset.nearest_neighbor( diff --git a/uxarray/subset/grid_accessor.py b/uxarray/subset/grid_accessor.py index 2dcb954e2..d0d8a9d08 100644 --- a/uxarray/subset/grid_accessor.py +++ b/uxarray/subset/grid_accessor.py @@ -54,8 +54,9 @@ def bounding_box( face centers, or edge centers lie within the bounds. element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` - inverse_indices : bool - Flag to indicate whether to store the original grids face indices for later use + inverse_indices : Union[List[str], Set[str], bool], optional + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) """ if method == "coords": @@ -132,8 +133,9 @@ def bounding_circle( Radius of bounding circle (in degrees) element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` - inverse_indices : bool - Flag to indicate whether to store the original grids face indices for later use + inverse_indices : Union[List[str], Set[str], bool], optional + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) """ coords = np.asarray(center_coord) @@ -168,8 +170,9 @@ def nearest_neighbor( Number of neighbors to query element: str Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` - inverse_indices : bool - Flag to indicate whether to store the original grids face indices for later use + inverse_indices : Union[List[str], Set[str], bool], optional + Indicates whether to store the original grids indices. Passing `True` stores the original face centers, + other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) """ coords = np.asarray(center_coord) From 00973c72523f712a9d796c741fcc4804441d6370 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Tue, 7 Jan 2025 10:00:05 -0600 Subject: [PATCH 07/16] fixed leftover variables --- uxarray/grid/slice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uxarray/grid/slice.py b/uxarray/grid/slice.py index 9045e0be7..1bfc8a017 100644 --- a/uxarray/grid/slice.py +++ b/uxarray/grid/slice.py @@ -37,7 +37,7 @@ def _slice_node_indices( face_indices = np.unique(grid.node_face_connectivity.values[indices].ravel()) face_indices = face_indices[face_indices != INT_FILL_VALUE] - return _slice_face_indices(grid, face_indices, inverse_indices=inverse_indices) + return _slice_face_indices(grid, face_indices) def _slice_edge_indices( @@ -67,7 +67,7 @@ def _slice_edge_indices( face_indices = np.unique(grid.edge_face_connectivity.values[indices].ravel()) face_indices = face_indices[face_indices != INT_FILL_VALUE] - return _slice_face_indices(grid, face_indices, inverse_indices=inverse_indices) + return _slice_face_indices(grid, face_indices) def _slice_face_indices( From f226da0445c482e3b83108e655ac46981e09fc9e Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Tue, 7 Jan 2025 13:53:49 -0600 Subject: [PATCH 08/16] Fixed failing tests --- uxarray/grid/grid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 03a54b191..5c71f9d42 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -2268,7 +2268,7 @@ def isel( "Inverse indices are not yet supported for node selection, please use face centers" ) return _slice_node_indices( - self, dim_kwargs["n_node"], inverse_indices=inverse_indices + self, dim_kwargs["n_node"] ) elif "n_edge" in dim_kwargs: @@ -2277,7 +2277,7 @@ def isel( "Inverse indices are not yet supported for edge selection, please use face centers" ) return _slice_edge_indices( - self, dim_kwargs["n_edge"], inverse_indices=inverse_indices + self, dim_kwargs["n_edge"] ) elif "n_face" in dim_kwargs: From 1e52e02bdada8e4431210c2d0ba3b413703e5397 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Tue, 7 Jan 2025 13:56:48 -0600 Subject: [PATCH 09/16] Update grid.py --- uxarray/grid/grid.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 5c71f9d42..9f62da303 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -2267,18 +2267,14 @@ def isel( raise Exception( "Inverse indices are not yet supported for node selection, please use face centers" ) - return _slice_node_indices( - self, dim_kwargs["n_node"] - ) + return _slice_node_indices(self, dim_kwargs["n_node"]) elif "n_edge" in dim_kwargs: if inverse_indices: raise Exception( "Inverse indices are not yet supported for edge selection, please use face centers" ) - return _slice_edge_indices( - self, dim_kwargs["n_edge"] - ) + return _slice_edge_indices(self, dim_kwargs["n_edge"]) elif "n_face" in dim_kwargs: return _slice_face_indices( From 129f9c52c30658b81c35fdc13450272377300215 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Wed, 8 Jan 2025 10:18:32 -0600 Subject: [PATCH 10/16] New naming convention, fixed spelling errors --- test/test_subset.py | 2 +- uxarray/cross_sections/dataarray_accessor.py | 4 ++-- uxarray/cross_sections/grid_accessor.py | 4 ++-- uxarray/grid/grid.py | 10 +++++----- uxarray/grid/slice.py | 10 +++++----- uxarray/subset/dataarray_accessor.py | 6 +++--- uxarray/subset/grid_accessor.py | 6 +++--- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/test/test_subset.py b/test/test_subset.py index 184d0fcb0..db2108971 100644 --- a/test/test_subset.py +++ b/test/test_subset.py @@ -144,7 +144,7 @@ def test_inverse_indices(): # Ensure code raises exceptions when the element is edges or nodes assert pytest.raises(Exception, grid.subset.bounding_circle, center_coord, r=10, element="edge centers", inverse_indices=True) - assert pytest.raises(Exception, grid.subset.bounding_circle, center_coord, r=10, element="node centers", inverse_indices=True) + assert pytest.raises(Exception, grid.subset.bounding_circle, center_coord, r=10, element="nodes", inverse_indices=True) # Test isel directly subset = grid.isel(n_face=[1], inverse_indices=True) diff --git a/uxarray/cross_sections/dataarray_accessor.py b/uxarray/cross_sections/dataarray_accessor.py index fd0e81e84..52599c7a4 100644 --- a/uxarray/cross_sections/dataarray_accessor.py +++ b/uxarray/cross_sections/dataarray_accessor.py @@ -35,7 +35,7 @@ def constant_latitude( Must be between -90.0 and 90.0 inverse_indices : Union[List[str], Set[str], bool], optional Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) Returns ------- @@ -80,7 +80,7 @@ def constant_longitude( Must be between -180.0 and 180.0 inverse_indices : Union[List[str], Set[str], bool], optional Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) Returns ------- diff --git a/uxarray/cross_sections/grid_accessor.py b/uxarray/cross_sections/grid_accessor.py index 17fee3f8e..76485fbda 100644 --- a/uxarray/cross_sections/grid_accessor.py +++ b/uxarray/cross_sections/grid_accessor.py @@ -39,7 +39,7 @@ def constant_latitude( If True, also returns the indices of the faces that intersect with the line of constant latitude. inverse_indices : Union[List[str], Set[str], bool], optional Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) Returns ------- @@ -97,7 +97,7 @@ def constant_longitude( If True, also returns the indices of the faces that intersect with the line of constant longitude. inverse_indices : Union[List[str], Set[str], bool], optional Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) Returns ------- diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 9f62da303..bf7f8864f 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -142,7 +142,7 @@ class Grid: is_subset : bool, default=False Flag to mark if the grid is a subset or not - inverse_indices: xr.Dataset, defaul=None + inverse_indices: xr.Dataset, default=None A dataset of indices that correspond to the original grid, if the grid being constructed is a subset Examples @@ -168,7 +168,7 @@ def __init__( grid_ds: xr.Dataset, source_grid_spec: Optional[str] = None, source_dims_dict: Optional[dict] = {}, - is_subset=False, + is_subset: bool = False, inverse_indices: Optional[xr.Dataset] = None, ): # check if inputted dataset is a minimum representable 2D UGRID unstructured grid @@ -1529,7 +1529,7 @@ def global_sphere_coverage(self): return not self.partial_sphere_coverage @property - def inverse_indices(self): + def inverse_indices(self) -> xr.Dataset: """Indices for a subset that map each face in the subset back to the original grid""" if self.is_subset: return self._inverse_indices @@ -2246,8 +2246,8 @@ def isel( Parameters inverse_indices : Union[List[str], Set[str], bool], default=False - Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + Indicates whether to store the original grids indices. Passing `True` stores the original face indices, + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) **dims_kwargs: kwargs Dimension to index, one of ['n_node', 'n_edge', 'n_face'] diff --git a/uxarray/grid/slice.py b/uxarray/grid/slice.py index 1bfc8a017..a85c9f78f 100644 --- a/uxarray/grid/slice.py +++ b/uxarray/grid/slice.py @@ -91,7 +91,7 @@ def _slice_face_indices( to exclusive (i.e elements be made up all desired features from a slice) inverse_indices : Union[List[str], Set[str], bool], optional Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) """ if inclusive is False: raise ValueError("Exclusive slicing is not yet supported.") @@ -151,12 +151,12 @@ def _slice_face_indices( inverse_indices_ds = xr.Dataset() index_types = { - "face centers": face_indices, - "edge centers": edge_indices, - "nodes": node_indices, + "face": face_indices, + "edge": edge_indices, + "node": node_indices, } if isinstance(inverse_indices, bool): - inverse_indices_ds["face centers"] = face_indices + inverse_indices_ds["face"] = face_indices else: for index_type in inverse_indices[0]: if index_type in index_types: diff --git a/uxarray/subset/dataarray_accessor.py b/uxarray/subset/dataarray_accessor.py index 23609ba65..2d966c587 100644 --- a/uxarray/subset/dataarray_accessor.py +++ b/uxarray/subset/dataarray_accessor.py @@ -56,7 +56,7 @@ def bounding_box( Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` inverse_indices : Union[List[str], Set[str], bool], optional Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) """ grid = self.uxda.uxgrid.subset.bounding_box( lon_bounds, lat_bounds, element, method, inverse_indices=inverse_indices @@ -85,7 +85,7 @@ def bounding_circle( Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` inverse_indices : Union[List[str], Set[str], bool], optional Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) """ grid = self.uxda.uxgrid.subset.bounding_circle( center_coord, r, element, inverse_indices=inverse_indices, **kwargs @@ -113,7 +113,7 @@ def nearest_neighbor( Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` inverse_indices : Union[List[str], Set[str], bool], optional Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) """ grid = self.uxda.uxgrid.subset.nearest_neighbor( diff --git a/uxarray/subset/grid_accessor.py b/uxarray/subset/grid_accessor.py index d0d8a9d08..a504179f1 100644 --- a/uxarray/subset/grid_accessor.py +++ b/uxarray/subset/grid_accessor.py @@ -56,7 +56,7 @@ def bounding_box( Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` inverse_indices : Union[List[str], Set[str], bool], optional Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) """ if method == "coords": @@ -135,7 +135,7 @@ def bounding_circle( Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` inverse_indices : Union[List[str], Set[str], bool], optional Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) """ coords = np.asarray(center_coord) @@ -172,7 +172,7 @@ def nearest_neighbor( Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` inverse_indices : Union[List[str], Set[str], bool], optional Indicates whether to store the original grids indices. Passing `True` stores the original face centers, - other reverse indices can be stored by passing any or all of the following: (["face centers", "edge centers", "nodes"], True) + other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True) """ coords = np.asarray(center_coord) From 23a2b9a916e9f0a46533d14568489819b1636d86 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Wed, 8 Jan 2025 13:32:48 -0600 Subject: [PATCH 11/16] Updated subsetting notebook --- docs/user-guide/subset.ipynb | 95 +++++++++++++++++++++++++++++++++++- test/test_subset.py | 3 +- uxarray/grid/slice.py | 3 ++ 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/docs/user-guide/subset.ipynb b/docs/user-guide/subset.ipynb index 07df1e82b..368c34d5e 100644 --- a/docs/user-guide/subset.ipynb +++ b/docs/user-guide/subset.ipynb @@ -131,6 +131,7 @@ "cell_type": "markdown", "metadata": { "collapsed": false, + "jp-MarkdownHeadingCollapsed": true, "jupyter": { "outputs_hidden": false } @@ -553,6 +554,98 @@ "print(\"Bounding Box Mean: \", bbox_subset_nodes.values.mean())\n", "print(\"Bounding Circle Mean: \", bcircle_subset.values.mean())" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Retrieving Orignal Grid Indices" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Sometimes having the original grids' indices is useful. These indices can be stored within the subset with the `inverse_indices` variable. This can be used to store the indices of the original face centers, edge centers, and node indices. This variable can be used within the subset as follows:\n", + "\n", + "* Passing in `True`, which will store the face center indices\n", + "* Passing in a list of which indices to store, along with `True`, to indicate what kind of original grid indices to store.\n", + " * Options for which indices to select include: `face`, `node`, and `edge`\n", + "\n", + "This currently only works when the element is `face centers`. Elements `nodes` and `edge centers` will be supported in the future." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "subset_indices = uxds[\"relhum_200hPa\"][0].subset.bounding_circle(center_coord, r, element=\"face centers\", \n", + " inverse_indices=(['face', 'node', 'edge'], True))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These indices can be retrieve through the grid:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "subset_indices.uxgrid.inverse_indices" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Determining if a Grid is a Subset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To check if a Grid (or dataset using `.uxgrid`) is a subset, we can use `Grid.is_subset`, which will return either `True` or `False`, depending on whether the `Grid` is a subset. Since `subset_indices` is a subset, using this feature we will return `True`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "subset_indices.uxgrid.is_subset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The file we have been using to create these subsets, `uxds`, is not a subset, so using the same call we will return `False:`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "uxds.uxgrid.is_subset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -571,7 +664,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.7" } }, "nbformat": 4, diff --git a/test/test_subset.py b/test/test_subset.py index db2108971..0809320b8 100644 --- a/test/test_subset.py +++ b/test/test_subset.py @@ -142,9 +142,10 @@ def test_inverse_indices(): assert subset.inverse_indices is not None - # Ensure code raises exceptions when the element is edges or nodes + # Ensure code raises exceptions when the element is edges or nodes or inverse_indices is incorrect assert pytest.raises(Exception, grid.subset.bounding_circle, center_coord, r=10, element="edge centers", inverse_indices=True) assert pytest.raises(Exception, grid.subset.bounding_circle, center_coord, r=10, element="nodes", inverse_indices=True) + assert pytest.raises(ValueError, grid.subset.bounding_circle, center_coord, r=10, element="face center", inverse_indices=(['not right'], True)) # Test isel directly subset = grid.isel(n_face=[1], inverse_indices=True) diff --git a/uxarray/grid/slice.py b/uxarray/grid/slice.py index a85c9f78f..66e1bbbfc 100644 --- a/uxarray/grid/slice.py +++ b/uxarray/grid/slice.py @@ -161,6 +161,9 @@ def _slice_face_indices( for index_type in inverse_indices[0]: if index_type in index_types: inverse_indices_ds[index_type] = index_types[index_type] + else: + raise ValueError("Incorrect type of index for `inverse_indices`. Try passing one of the following " + "instead: 'face', 'edge', 'node'") return Grid.from_dataset( ds, From 1a45876a33f11c5089fb9230be79a7e1e34a0c95 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Wed, 8 Jan 2025 13:33:57 -0600 Subject: [PATCH 12/16] Fixed precommit --- docs/user-guide/subset.ipynb | 8 ++++++-- uxarray/grid/slice.py | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/user-guide/subset.ipynb b/docs/user-guide/subset.ipynb index 368c34d5e..8110e98cb 100644 --- a/docs/user-guide/subset.ipynb +++ b/docs/user-guide/subset.ipynb @@ -581,8 +581,12 @@ "metadata": {}, "outputs": [], "source": [ - "subset_indices = uxds[\"relhum_200hPa\"][0].subset.bounding_circle(center_coord, r, element=\"face centers\", \n", - " inverse_indices=(['face', 'node', 'edge'], True))" + "subset_indices = uxds[\"relhum_200hPa\"][0].subset.bounding_circle(\n", + " center_coord,\n", + " r,\n", + " element=\"face centers\",\n", + " inverse_indices=([\"face\", \"node\", \"edge\"], True),\n", + ")" ] }, { diff --git a/uxarray/grid/slice.py b/uxarray/grid/slice.py index 66e1bbbfc..94e8e0eb8 100644 --- a/uxarray/grid/slice.py +++ b/uxarray/grid/slice.py @@ -162,8 +162,10 @@ def _slice_face_indices( if index_type in index_types: inverse_indices_ds[index_type] = index_types[index_type] else: - raise ValueError("Incorrect type of index for `inverse_indices`. Try passing one of the following " - "instead: 'face', 'edge', 'node'") + raise ValueError( + "Incorrect type of index for `inverse_indices`. Try passing one of the following " + "instead: 'face', 'edge', 'node'" + ) return Grid.from_dataset( ds, From 434b3899c1aa35ce45ecba08056311c99f2fe776 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Thu, 9 Jan 2025 12:44:53 -0600 Subject: [PATCH 13/16] Added is_subset property --- docs/user-guide/subset.ipynb | 8 -------- uxarray/grid/grid.py | 6 +++++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/user-guide/subset.ipynb b/docs/user-guide/subset.ipynb index 8110e98cb..c1e7a97df 100644 --- a/docs/user-guide/subset.ipynb +++ b/docs/user-guide/subset.ipynb @@ -131,7 +131,6 @@ "cell_type": "markdown", "metadata": { "collapsed": false, - "jp-MarkdownHeadingCollapsed": true, "jupyter": { "outputs_hidden": false } @@ -643,13 +642,6 @@ "source": [ "uxds.uxgrid.is_subset" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index bf7f8864f..7acd6c6f3 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -201,7 +201,7 @@ def __init__( # initialize attributes self._antimeridian_face_indices = None self._ds.assign_attrs({"source_grid_spec": self.source_grid_spec}) - self.is_subset = is_subset + self._is_subset = is_subset if inverse_indices is not None: self._inverse_indices = inverse_indices @@ -1538,6 +1538,10 @@ def inverse_indices(self) -> xr.Dataset: "Grid is not a subset, therefore no inverse face indices exist" ) + @property + def is_subset(self): + return self._is_subset + def chunk(self, n_node="auto", n_edge="auto", n_face="auto"): """Converts all arrays to dask arrays with given chunks across grid dimensions in-place. From 702736b3981c58e842e102cbc7f7db401ea9b9c2 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick <95507181+aaronzedwick@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:04:42 -0600 Subject: [PATCH 14/16] Update uxarray/grid/grid.py Co-authored-by: Philip Chmielowiec <67855069+philipc2@users.noreply.github.com> --- uxarray/grid/grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 7acd6c6f3..3f1611713 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -1539,7 +1539,7 @@ def inverse_indices(self) -> xr.Dataset: ) @property - def is_subset(self): + def is_subset(self) -> bool: return self._is_subset def chunk(self, n_node="auto", n_edge="auto", n_face="auto"): From 4ea26e430c4a1a36327118563bd54b772ede60fe Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Thu, 9 Jan 2025 13:15:07 -0600 Subject: [PATCH 15/16] Added doc string, updated test case --- test/test_subset.py | 2 +- uxarray/grid/grid.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_subset.py b/test/test_subset.py index 0809320b8..f13ae4919 100644 --- a/test/test_subset.py +++ b/test/test_subset.py @@ -149,4 +149,4 @@ def test_inverse_indices(): # Test isel directly subset = grid.isel(n_face=[1], inverse_indices=True) - assert subset.inverse_indices is not None + assert subset.inverse_indices.face.values == 1 diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 7acd6c6f3..8bf752b34 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -1540,6 +1540,7 @@ def inverse_indices(self) -> xr.Dataset: @property def is_subset(self): + """Returns `True` if the Grid is a subset, and 'False' otherwise.""" return self._is_subset def chunk(self, n_node="auto", n_edge="auto", n_face="auto"): From a97870652169094c3b4627d01372545a37593c32 Mon Sep 17 00:00:00 2001 From: Aaron Zedwick Date: Thu, 9 Jan 2025 13:19:27 -0600 Subject: [PATCH 16/16] Update grid.py --- uxarray/grid/grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py index 8bf752b34..c975ebff8 100644 --- a/uxarray/grid/grid.py +++ b/uxarray/grid/grid.py @@ -1540,7 +1540,7 @@ def inverse_indices(self) -> xr.Dataset: @property def is_subset(self): - """Returns `True` if the Grid is a subset, and 'False' otherwise.""" + """Returns `True` if the Grid is a subset, 'False' otherwise.""" return self._is_subset def chunk(self, n_node="auto", n_edge="auto", n_face="auto"):