Skip to content

Commit aaa38e7

Browse files
authored
refactor(grid): consistent API for get_cell_vertices (#2678)
Previously the Grid.get_cell_vertices() method looked/worked different depending on the grid type. Make it consistent across all three grid types by supporting both cellid and node keyword arguments and being lenient about what's passed to cellid i.e. accept a node number, a fully-sized tuple, or an "abbreviated" tuple lacking layer index. Cell ID and node number are conceptually distinct but til now VertexGrid and UnstructuredGrid methods had a single parameter cellid supporting (for vertex) or expecting (for unstructured) a node number, which we don't want to break. And continue also supporting i and j for StructuredGrid.
1 parent 8d8097a commit aaa38e7

File tree

4 files changed

+239
-24
lines changed

4 files changed

+239
-24
lines changed

autotest/test_grid.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
centroid_of_polygon,
2626
to_cvfd,
2727
)
28+
from flopy.utils.gridutil import get_disu_kwargs, get_disv_kwargs
2829
from flopy.utils.lgrutil import Lgr
2930
from flopy.utils.triangle import Triangle
3031
from flopy.utils.voronoi import VoronoiGrid
@@ -190,6 +191,92 @@ def test_get_cell_vertices():
190191
mg.get_cell_vertices(nn=0)
191192

192193

194+
def test_structured_grid_get_cell_vertices():
195+
"""Test StructuredGrid.get_cell_vertices() with various input forms"""
196+
delr, delc = np.array([10.0] * 3), np.array([10.0] * 4)
197+
sg = StructuredGrid(delr=delr, delc=delc)
198+
199+
# Test node kwarg
200+
v1 = sg.get_cell_vertices(node=0)
201+
expected = [
202+
(np.float64(0.0), np.float64(40.0)),
203+
(np.float64(10.0), np.float64(40.0)),
204+
(np.float64(10.0), np.float64(30.0)),
205+
(np.float64(0.0), np.float64(30.0)),
206+
]
207+
assert v1 == expected
208+
209+
# Test positional args (i, j)
210+
v2 = sg.get_cell_vertices(3, 0)
211+
212+
# Test cellid as tuple (i, j)
213+
v3 = sg.get_cell_vertices((3, 0))
214+
assert v2 == v3, "Positional and tuple forms should match"
215+
216+
# Test cellid as 3-element tuple (layer, i, j) - layer ignored
217+
v4 = sg.get_cell_vertices(cellid=(0, 3, 0))
218+
assert v2 == v4, "2-element and 3-element forms should match"
219+
220+
# Test named i, j kwargs (backward compatibility)
221+
v5 = sg.get_cell_vertices(i=3, j=0)
222+
assert v2 == v5, "Named i,j should match"
223+
224+
225+
def test_vertex_grid_get_cell_vertices():
226+
"""Test VertexGrid.get_cell_vertices() with various input forms"""
227+
disv_props = get_disv_kwargs(2, 10, 10, 10.0, 10.0, 100.0, [50.0, 0.0])
228+
# Remove nvert which is not needed for VertexGrid constructor
229+
disv_props.pop("nvert", None)
230+
vg = VertexGrid(**disv_props)
231+
232+
# Test cell2d index as positional arg
233+
v1 = vg.get_cell_vertices(5)
234+
235+
# Test (layer, cell2d) tuple - layer ignored for 2D vertices
236+
v2 = vg.get_cell_vertices((0, 5))
237+
assert v1 == v2, "cell2d and (layer, cell2d) should match"
238+
239+
# Test named cellid kwarg
240+
v3 = vg.get_cell_vertices(cellid=5)
241+
assert v1 == v3, "Positional and kwarg should match"
242+
243+
# Test node number (>= ncpl, should be converted to cell2d)
244+
# Node 105 = layer 1, cell2d 5 (ncpl=100)
245+
v4 = vg.get_cell_vertices(node=105)
246+
247+
# Verify it's the same as (1, 5)
248+
v5 = vg.get_cell_vertices((1, 5))
249+
assert v4 == v5, "Node and (layer, cell2d) should match"
250+
251+
252+
def test_unstructured_grid_get_cell_vertices():
253+
"""Test UnstructuredGrid.get_cell_vertices() with various input forms"""
254+
disu_props = get_disu_kwargs(
255+
1, 10, 10, 10.0, 10.0, 100.0, [0.0], return_vertices=True
256+
)
257+
# Extract only the parameters needed for UnstructuredGrid
258+
ug = UnstructuredGrid(
259+
vertices=disu_props["vertices"],
260+
cell2d=disu_props["cell2d"],
261+
top=disu_props["top"],
262+
)
263+
264+
# Test node as positional arg
265+
v1 = ug.get_cell_vertices(5)
266+
267+
# Test (node,) single-element tuple
268+
v2 = ug.get_cell_vertices((5,))
269+
assert v1 == v2, "Int and tuple forms should match"
270+
271+
# Test node kwarg
272+
v3 = ug.get_cell_vertices(node=5)
273+
assert v1 == v3, "cellid and node should match"
274+
275+
# Test cellid kwarg
276+
v4 = ug.get_cell_vertices(cellid=5)
277+
assert v1 == v4, "Positional and kwarg should match"
278+
279+
193280
def test_get_lrc_get_node():
194281
nlay, nrow, ncol = 3, 4, 5
195282
nnodes = nlay * nrow * ncol

flopy/discretization/structuredgrid.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,15 +1148,20 @@ def get_cell_vertices(self, *args, **kwargs):
11481148
11491149
Parameters
11501150
----------
1151+
cellid : int or tuple, optional
1152+
Cell identifier. Can be:
1153+
- node number (int)
1154+
- (row, col) tuple
1155+
- (layer, row, col) tuple (layer is ignored, vertices are 2D)
11511156
node : int, optional
1152-
Node index, mutually exclusive with i and j
1157+
Node index, mutually exclusive with cellid, i, and j
11531158
i, j : int, optional
1154-
Row and column index, mutually exclusive with node
1159+
Row and column index, mutually exclusive with cellid and node
11551160
11561161
Returns
11571162
-------
11581163
list
1159-
list of tuples with x,y coordinates to cell vertices
1164+
list of (x, y) cell vertex coordinates
11601165
11611166
Examples
11621167
--------
@@ -1168,26 +1173,61 @@ def get_cell_vertices(self, *args, **kwargs):
11681173
[(0.0, 40.0), (10.0, 40.0), (10.0, 30.0), (0.0, 30.0)]
11691174
>>> sg.get_cell_vertices(3, 0)
11701175
[(0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)]
1176+
>>> sg.get_cell_vertices((3, 0))
1177+
[(0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)]
1178+
>>> sg.get_cell_vertices(cellid=(0, 3, 0))
1179+
[(0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)]
11711180
"""
11721181
if kwargs:
11731182
if args:
11741183
raise TypeError("mixed positional and keyword arguments not supported")
1184+
elif "cellid" in kwargs:
1185+
cellid = kwargs.pop("cellid")
1186+
# Handle cellid as int, tuple of 2, or tuple of 3
1187+
if isinstance(cellid, (tuple, list)):
1188+
if len(cellid) == 2:
1189+
i, j = cellid
1190+
elif len(cellid) == 3:
1191+
_, i, j = cellid # ignore layer
1192+
else:
1193+
raise ValueError(
1194+
f"cellid tuple must have 2 or 3 elements, got {len(cellid)}"
1195+
)
1196+
else:
1197+
_, i, j = self.get_lrc(cellid)[0]
11751198
elif "node" in kwargs:
11761199
_, i, j = self.get_lrc(kwargs.pop("node"))[0]
11771200
elif "i" in kwargs and "j" in kwargs:
11781201
i = kwargs.pop("i")
11791202
j = kwargs.pop("j")
1203+
else:
1204+
raise TypeError(
1205+
"expected cellid, node, or i and j as keyword arguments"
1206+
)
11801207
if kwargs:
11811208
unused = ", ".join(kwargs.keys())
11821209
raise TypeError(f"unused keyword arguments: {unused}")
11831210
elif len(args) == 0:
11841211
raise TypeError("expected one or more arguments")
1185-
1186-
if len(args) == 1:
1187-
_, i, j = self.get_lrc(args[0])[0]
1212+
elif len(args) == 1:
1213+
# Single arg could be node number or (row, col) or (layer, row, col) tuple
1214+
arg = args[0]
1215+
if isinstance(arg, (tuple, list)):
1216+
if len(arg) == 2:
1217+
i, j = arg
1218+
elif len(arg) == 3:
1219+
# (layer, row, col) - ignore layer
1220+
_, i, j = arg
1221+
else:
1222+
raise ValueError(
1223+
f"cellid tuple must have 2 or 3 elements, got {len(arg)}"
1224+
)
1225+
else:
1226+
# Node number
1227+
_, i, j = self.get_lrc(arg)[0]
11881228
elif len(args) == 2:
11891229
i, j = args
1190-
elif len(args) > 2:
1230+
else:
11911231
raise TypeError("too many arguments")
11921232

11931233
self._copy_cache = False

flopy/discretization/unstructuredgrid.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -847,16 +847,54 @@ def top_botm(self):
847847
new_botm = np.expand_dims(self._botm, 0)
848848
return np.concatenate((new_top, new_botm), axis=0)
849849

850-
def get_cell_vertices(self, cellid):
850+
def get_cell_vertices(self, cellid=None, node=None):
851851
"""
852-
Method to get a set of cell vertices for a single cell
853-
used in the Shapefile export utilities
854-
:param cellid: (int) cellid number
852+
Get a set of cell vertices for a single cell.
853+
854+
Parameters
855+
----------
856+
cellid : int or tuple, optional
857+
Cell identifier. Can be:
858+
- node number (int)
859+
- (node,) single-element tuple
860+
node : int, optional
861+
Node number, mutually exclusive with cellid
862+
855863
Returns
856-
------- list of x,y cell vertices
864+
-------
865+
list
866+
list of (x, y) cell vertex coordinates
867+
868+
Examples
869+
--------
870+
>>> import flopy
871+
>>> from flopy.utils.gridutil import get_disu_kwargs
872+
>>> disu_props = get_disu_kwargs(1, 10, 10, 1.0, 1.0, 1.0, [0.0])
873+
>>> ug = flopy.discretization.UnstructuredGrid(**disu_props)
874+
>>> ug.get_cell_vertices(5) # node number
875+
>>> ug.get_cell_vertices((5,)) # (node,) tuple
876+
>>> ug.get_cell_vertices(node=5) # explicit node kwarg
877+
>>> ug.get_cell_vertices(cellid=5) # explicit cellid kwarg
857878
"""
879+
880+
if cellid is not None and node is not None:
881+
raise ValueError("cellid and node are mutually exclusive")
882+
883+
if cellid is None and node is None:
884+
raise TypeError("expected cellid or node argument")
885+
886+
idx = node if node is not None else cellid
887+
if isinstance(idx, (tuple, list)):
888+
if len(idx) == 1:
889+
idx = idx[0]
890+
else:
891+
raise ValueError(
892+
f"cellid tuple must have 1 element for "
893+
f"unstructured grids, got {len(idx)}"
894+
)
895+
858896
self._copy_cache = False
859-
cell_vert = list(zip(self.xvertices[cellid], self.yvertices[cellid]))
897+
cell_vert = list(zip(self.xvertices[idx], self.yvertices[idx]))
860898
self._copy_cache = True
861899
return cell_vert
862900

flopy/discretization/vertexgrid.py

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -474,23 +474,73 @@ def intersect(self, x, y, z=None, local=False, forgive=False):
474474
results.astype(int) if np.all(valid_mask) else results,
475475
)
476476

477-
def get_cell_vertices(self, cellid):
477+
def get_cell_vertices(self, cellid=None, node=None):
478478
"""
479-
Method to get a set of cell vertices for a single cell
480-
used in the Shapefile export utilities
481-
:param cellid: (int) cellid number
479+
Get a set of cell vertices for a single cell.
480+
481+
Parameters
482+
----------
483+
cellid : int or tuple, optional
484+
Cell identifier. Can be:
485+
- cell2d index (int, 0 to ncpl-1)
486+
- node number (int, >= ncpl) - will be converted to cell2d
487+
- (cell2d,) single-element tuple
488+
- (layer, cell2d) tuple (layer is ignored, vertices are 2D)
489+
node : int, optional
490+
Node number, mutually exclusive with cellid
491+
482492
Returns
483-
------- list of x,y cell vertices
493+
-------
494+
list
495+
list of (x, y) cell vertex coordinates
496+
497+
Examples
498+
--------
499+
>>> import flopy
500+
>>> from flopy.utils.gridutil import get_disv_kwargs
501+
>>> disv_props = get_disv_kwargs(1, 10, 10, 1.0, 1.0, 1.0, [0.0])
502+
>>> vg = flopy.discretization.VertexGrid(**disv_props)
503+
>>> vg.get_cell_vertices(5) # cell2d index
504+
>>> vg.get_cell_vertices((0, 5)) # (layer, cell2d) tuple
505+
>>> vg.get_cell_vertices(node=105) # node number
506+
>>> vg.get_cell_vertices(cellid=(1, 5)) # explicit cellid kwarg
484507
"""
485-
while cellid >= self.ncpl:
486-
if cellid > self.nnodes:
487-
err = f"cellid {cellid} out of index for size {self.nnodes}"
488-
raise IndexError(err)
508+
# Handle arguments
509+
if cellid is not None and node is not None:
510+
raise ValueError("cellid and node are mutually exclusive")
511+
512+
if cellid is None and node is None:
513+
raise TypeError("expected cellid or node argument")
489514

490-
cellid -= self.ncpl
515+
# Use cellid if provided, otherwise use node
516+
if node is not None:
517+
idx = node
518+
else:
519+
idx = cellid
520+
521+
# Handle tuple forms
522+
if isinstance(idx, (tuple, list)):
523+
if len(idx) == 1:
524+
# (cell2d,) or (node,)
525+
idx = idx[0]
526+
elif len(idx) == 2:
527+
# (layer, cell2d) - ignore layer since vertices are 2D
528+
_, idx = idx
529+
else:
530+
raise ValueError(
531+
f"cellid tuple must have 1 or 2 elements, got {len(idx)}"
532+
)
533+
534+
# Convert node to cell2d if necessary
535+
while idx >= self.ncpl:
536+
if idx > self.nnodes:
537+
raise IndexError(
538+
f"node number {idx} exceeds grid node count {self.nnodes}"
539+
)
540+
idx -= self.ncpl
491541

492542
self._copy_cache = False
493-
cell_verts = list(zip(self.xvertices[cellid], self.yvertices[cellid]))
543+
cell_verts = list(zip(self.xvertices[idx], self.yvertices[idx]))
494544
self._copy_cache = True
495545
return cell_verts
496546

0 commit comments

Comments
 (0)