Skip to content

Commit 0e45eda

Browse files
Add Inverse Face Indices to Subsetted Grids (#1122)
* Initial Work * Addressed review comments * Updated doc string * Added inverse_indices support for data arrays and cross sections * Added ability to choose which inverse_indices to store, stores as ds * updated grid, doc strings, api, test cases * fixed leftover variables * Fixed failing tests * Update grid.py * New naming convention, fixed spelling errors * Updated subsetting notebook * Fixed precommit * Added is_subset property * Update uxarray/grid/grid.py Co-authored-by: Philip Chmielowiec <[email protected]> * Added doc string, updated test case * Update grid.py --------- Co-authored-by: Philip Chmielowiec <[email protected]>
1 parent c568d6b commit 0e45eda

File tree

10 files changed

+304
-38
lines changed

10 files changed

+304
-38
lines changed

docs/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Indexing
5454
:toctree: generated/
5555

5656
Grid.isel
57+
Grid.inverse_indices
5758

5859
Dimensions
5960
~~~~~~~~~~

docs/user-guide/subset.ipynb

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,95 @@
553553
"print(\"Bounding Box Mean: \", bbox_subset_nodes.values.mean())\n",
554554
"print(\"Bounding Circle Mean: \", bcircle_subset.values.mean())"
555555
]
556+
},
557+
{
558+
"cell_type": "markdown",
559+
"metadata": {},
560+
"source": [
561+
"## Retrieving Orignal Grid Indices"
562+
]
563+
},
564+
{
565+
"cell_type": "markdown",
566+
"metadata": {},
567+
"source": [
568+
"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",
569+
"\n",
570+
"* Passing in `True`, which will store the face center indices\n",
571+
"* Passing in a list of which indices to store, along with `True`, to indicate what kind of original grid indices to store.\n",
572+
" * Options for which indices to select include: `face`, `node`, and `edge`\n",
573+
"\n",
574+
"This currently only works when the element is `face centers`. Elements `nodes` and `edge centers` will be supported in the future."
575+
]
576+
},
577+
{
578+
"cell_type": "code",
579+
"execution_count": null,
580+
"metadata": {},
581+
"outputs": [],
582+
"source": [
583+
"subset_indices = uxds[\"relhum_200hPa\"][0].subset.bounding_circle(\n",
584+
" center_coord,\n",
585+
" r,\n",
586+
" element=\"face centers\",\n",
587+
" inverse_indices=([\"face\", \"node\", \"edge\"], True),\n",
588+
")"
589+
]
590+
},
591+
{
592+
"cell_type": "markdown",
593+
"metadata": {},
594+
"source": [
595+
"These indices can be retrieve through the grid:"
596+
]
597+
},
598+
{
599+
"cell_type": "code",
600+
"execution_count": null,
601+
"metadata": {},
602+
"outputs": [],
603+
"source": [
604+
"subset_indices.uxgrid.inverse_indices"
605+
]
606+
},
607+
{
608+
"cell_type": "markdown",
609+
"metadata": {},
610+
"source": [
611+
"## Determining if a Grid is a Subset"
612+
]
613+
},
614+
{
615+
"cell_type": "markdown",
616+
"metadata": {},
617+
"source": [
618+
"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`:"
619+
]
620+
},
621+
{
622+
"cell_type": "code",
623+
"execution_count": null,
624+
"metadata": {},
625+
"outputs": [],
626+
"source": [
627+
"subset_indices.uxgrid.is_subset"
628+
]
629+
},
630+
{
631+
"cell_type": "markdown",
632+
"metadata": {},
633+
"source": [
634+
"The file we have been using to create these subsets, `uxds`, is not a subset, so using the same call we will return `False:`"
635+
]
636+
},
637+
{
638+
"cell_type": "code",
639+
"execution_count": null,
640+
"metadata": {},
641+
"outputs": [],
642+
"source": [
643+
"uxds.uxgrid.is_subset"
644+
]
556645
}
557646
],
558647
"metadata": {
@@ -571,7 +660,7 @@
571660
"name": "python",
572661
"nbconvert_exporter": "python",
573662
"pygments_lexer": "ipython3",
574-
"version": "3.11.9"
663+
"version": "3.12.7"
575664
}
576665
},
577666
"nbformat": 4,

test/test_subset.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,19 +104,49 @@ def test_grid_bounding_box_subset():
104104
bbox_antimeridian[0], bbox_antimeridian[1], element=element)
105105

106106

107-
108-
109107
def test_uxda_isel():
110108
uxds = ux.open_dataset(GRID_PATHS[0], DATA_PATHS[0])
111109

112110
sub = uxds['bottomDepth'].isel(n_face=[1, 2, 3])
113111

114112
assert len(sub) == 3
115113

114+
116115
def test_uxda_isel_with_coords():
117116
uxds = ux.open_dataset(GRID_PATHS[0], DATA_PATHS[0])
118117
uxds = uxds.assign_coords({"lon_face": uxds.uxgrid.face_lon})
119118
sub = uxds['bottomDepth'].isel(n_face=[1, 2, 3])
120119

121120
assert "lon_face" in sub.coords
122121
assert len(sub.coords['lon_face']) == 3
122+
123+
124+
def test_inverse_indices():
125+
grid = ux.open_grid(GRID_PATHS[0])
126+
127+
# Test nearest neighbor subsetting
128+
coord = [0, 0]
129+
subset = grid.subset.nearest_neighbor(coord, k=1, element="face centers", inverse_indices=True)
130+
131+
assert subset.inverse_indices is not None
132+
133+
# Test bounding box subsetting
134+
box = [(-10, 10), (-10, 10)]
135+
subset = grid.subset.bounding_box(box[0], box[1], element="face centers", inverse_indices=True)
136+
137+
assert subset.inverse_indices is not None
138+
139+
# Test bounding circle subsetting
140+
center_coord = [0, 0]
141+
subset = grid.subset.bounding_circle(center_coord, r=10, element="face centers", inverse_indices=True)
142+
143+
assert subset.inverse_indices is not None
144+
145+
# Ensure code raises exceptions when the element is edges or nodes or inverse_indices is incorrect
146+
assert pytest.raises(Exception, grid.subset.bounding_circle, center_coord, r=10, element="edge centers", inverse_indices=True)
147+
assert pytest.raises(Exception, grid.subset.bounding_circle, center_coord, r=10, element="nodes", inverse_indices=True)
148+
assert pytest.raises(ValueError, grid.subset.bounding_circle, center_coord, r=10, element="face center", inverse_indices=(['not right'], True))
149+
150+
# Test isel directly
151+
subset = grid.isel(n_face=[1], inverse_indices=True)
152+
assert subset.inverse_indices.face.values == 1

uxarray/core/dataarray.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,7 +1035,7 @@ def _edge_centered(self) -> bool:
10351035
"n_edge" dimension)"""
10361036
return "n_edge" in self.dims
10371037

1038-
def isel(self, ignore_grid=False, *args, **kwargs):
1038+
def isel(self, ignore_grid=False, inverse_indices=False, *args, **kwargs):
10391039
"""Grid-informed implementation of xarray's ``isel`` method, which
10401040
enables indexing across grid dimensions.
10411041
@@ -1069,11 +1069,17 @@ def isel(self, ignore_grid=False, *args, **kwargs):
10691069
raise ValueError("Only one grid dimension can be sliced at a time")
10701070

10711071
if "n_node" in kwargs:
1072-
sliced_grid = self.uxgrid.isel(n_node=kwargs["n_node"])
1072+
sliced_grid = self.uxgrid.isel(
1073+
n_node=kwargs["n_node"], inverse_indices=inverse_indices
1074+
)
10731075
elif "n_edge" in kwargs:
1074-
sliced_grid = self.uxgrid.isel(n_edge=kwargs["n_edge"])
1076+
sliced_grid = self.uxgrid.isel(
1077+
n_edge=kwargs["n_edge"], inverse_indices=inverse_indices
1078+
)
10751079
else:
1076-
sliced_grid = self.uxgrid.isel(n_face=kwargs["n_face"])
1080+
sliced_grid = self.uxgrid.isel(
1081+
n_face=kwargs["n_face"], inverse_indices=inverse_indices
1082+
)
10771083

10781084
return self._slice_from_grid(sliced_grid)
10791085

uxarray/cross_sections/dataarray_accessor.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33

4-
from typing import TYPE_CHECKING
4+
from typing import TYPE_CHECKING, Union, List, Set
55

66
if TYPE_CHECKING:
77
pass
@@ -22,7 +22,9 @@ def __repr__(self):
2222

2323
return prefix + methods_heading
2424

25-
def constant_latitude(self, lat: float):
25+
def constant_latitude(
26+
self, lat: float, inverse_indices: Union[List[str], Set[str], bool] = False
27+
):
2628
"""Extracts a cross-section of the data array by selecting all faces that
2729
intersect with a specified line of constant latitude.
2830
@@ -31,6 +33,9 @@ def constant_latitude(self, lat: float):
3133
lat : float
3234
The latitude at which to extract the cross-section, in degrees.
3335
Must be between -90.0 and 90.0
36+
inverse_indices : Union[List[str], Set[str], bool], optional
37+
Indicates whether to store the original grids indices. Passing `True` stores the original face centers,
38+
other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True)
3439
3540
Returns
3641
-------
@@ -60,9 +65,11 @@ def constant_latitude(self, lat: float):
6065

6166
faces = self.uxda.uxgrid.get_faces_at_constant_latitude(lat)
6267

63-
return self.uxda.isel(n_face=faces)
68+
return self.uxda.isel(n_face=faces, inverse_indices=inverse_indices)
6469

65-
def constant_longitude(self, lon: float):
70+
def constant_longitude(
71+
self, lon: float, inverse_indices: Union[List[str], Set[str], bool] = False
72+
):
6673
"""Extracts a cross-section of the data array by selecting all faces that
6774
intersect with a specified line of constant longitude.
6875
@@ -71,6 +78,9 @@ def constant_longitude(self, lon: float):
7178
lon : float
7279
The latitude at which to extract the cross-section, in degrees.
7380
Must be between -180.0 and 180.0
81+
inverse_indices : Union[List[str], Set[str], bool], optional
82+
Indicates whether to store the original grids indices. Passing `True` stores the original face centers,
83+
other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True)
7484
7585
Returns
7686
-------
@@ -102,7 +112,7 @@ def constant_longitude(self, lon: float):
102112
lon,
103113
)
104114

105-
return self.uxda.isel(n_face=faces)
115+
return self.uxda.isel(n_face=faces, inverse_indices=inverse_indices)
106116

107117
def gca(self, *args, **kwargs):
108118
raise NotImplementedError

uxarray/cross_sections/grid_accessor.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33

4-
from typing import TYPE_CHECKING
4+
from typing import TYPE_CHECKING, Union, List, Set
55

66
if TYPE_CHECKING:
77
from uxarray.grid import Grid
@@ -25,6 +25,7 @@ def constant_latitude(
2525
self,
2626
lat: float,
2727
return_face_indices: bool = False,
28+
inverse_indices: Union[List[str], Set[str], bool] = False,
2829
):
2930
"""Extracts a cross-section of the grid by selecting all faces that
3031
intersect with a specified line of constant latitude.
@@ -36,6 +37,9 @@ def constant_latitude(
3637
Must be between -90.0 and 90.0
3738
return_face_indices : bool, optional
3839
If True, also returns the indices of the faces that intersect with the line of constant latitude.
40+
inverse_indices : Union[List[str], Set[str], bool], optional
41+
Indicates whether to store the original grids indices. Passing `True` stores the original face centers,
42+
other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True)
3943
4044
Returns
4145
-------
@@ -66,7 +70,9 @@ def constant_latitude(
6670
if len(faces) == 0:
6771
raise ValueError(f"No intersections found at lat={lat}.")
6872

69-
grid_at_constant_lat = self.uxgrid.isel(n_face=faces)
73+
grid_at_constant_lat = self.uxgrid.isel(
74+
n_face=faces, inverse_indices=inverse_indices
75+
)
7076

7177
if return_face_indices:
7278
return grid_at_constant_lat, faces
@@ -77,6 +83,7 @@ def constant_longitude(
7783
self,
7884
lon: float,
7985
return_face_indices: bool = False,
86+
inverse_indices: Union[List[str], Set[str], bool] = False,
8087
):
8188
"""Extracts a cross-section of the grid by selecting all faces that
8289
intersect with a specified line of constant longitude.
@@ -88,6 +95,9 @@ def constant_longitude(
8895
Must be between -90.0 and 90.0
8996
return_face_indices : bool, optional
9097
If True, also returns the indices of the faces that intersect with the line of constant longitude.
98+
inverse_indices : Union[List[str], Set[str], bool], optional
99+
Indicates whether to store the original grids indices. Passing `True` stores the original face centers,
100+
other reverse indices can be stored by passing any or all of the following: (["face", "edge", "node"], True)
91101
92102
Returns
93103
-------
@@ -117,7 +127,9 @@ def constant_longitude(
117127
if len(faces) == 0:
118128
raise ValueError(f"No intersections found at lon={lon}")
119129

120-
grid_at_constant_lon = self.uxgrid.isel(n_face=faces)
130+
grid_at_constant_lon = self.uxgrid.isel(
131+
n_face=faces, inverse_indices=inverse_indices
132+
)
121133

122134
if return_face_indices:
123135
return grid_at_constant_lon, faces

0 commit comments

Comments
 (0)