Skip to content

Commit 94f3c8c

Browse files
authored
feat(grid): add get_node() method to all grid types (#2680)
Add a get_node() method to VertexGrid and UnstructuredGrid, consistent with StructuredGrid, for cell ID -> node number conversions. Rename the lrc_list parameter on StructuredGrid.get_node() to cellids and deprecate lrc_list. Add an optional node2d parameter to determine whether to get the 2D or 3D node number, useful for structured and vertex grids, ignored for unstructured. The cell ID -> node number conversion was done ad hoc in a few places in the codebase, this allows some simplification. Maybe in 4.x we can standardize the signature and pull an abstract method into the base Grid class.
1 parent cd4e6e9 commit 94f3c8c

7 files changed

Lines changed: 242 additions & 37 deletions

File tree

autotest/test_grid.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1761,3 +1761,94 @@ def test_structured_grid_intersect_edge_case(simple_structured_grid):
17611761
assert lay is None, f"Expected lay=None, got {lay}"
17621762
assert np.isnan(row), f"Expected row=nan, got {row}"
17631763
assert np.isnan(col), f"Expected col=nan, got {col}"
1764+
1765+
1766+
def test_structured_grid_get_node():
1767+
sg = StructuredGrid(nlay=3, nrow=4, ncol=5, delr=np.ones(5), delc=np.ones(4))
1768+
1769+
# 3D node numbers
1770+
assert sg.get_node((0, 0, 0)) == [0]
1771+
assert sg.get_node((1, 0, 0)) == [20]
1772+
assert sg.get_node((0, 1, 2)) == [7]
1773+
assert sg.get_node([(0, 0, 0), (1, 0, 0)]) == [0, 20]
1774+
assert sg.get_node([(0, 0, 0), (2, 3, 4)]) == [0, 59]
1775+
1776+
# 2D node numbers (layer ignored)
1777+
assert sg.get_node((0, 1, 2), node2d=True) == [7]
1778+
assert sg.get_node((1, 1, 2), node2d=True) == [7]
1779+
assert sg.get_node((2, 1, 2), node2d=True) == [7]
1780+
assert sg.get_node([(0, 0, 0), (1, 0, 0), (2, 1, 2)], node2d=True) == [0, 0, 7]
1781+
1782+
1783+
def test_vertex_grid_get_node():
1784+
nlay = 3
1785+
ncpl = 100
1786+
vertices = [[i, float(i % 10), float(i // 10)] for i in range(121)]
1787+
cell2d = []
1788+
for i in range(ncpl):
1789+
# Simple quad cells
1790+
iv = [i, i + 1, i + 11, i + 10]
1791+
cell2d.append([i, 0.0, 0.0, 4] + iv)
1792+
vg = VertexGrid(
1793+
vertices=vertices,
1794+
cell2d=cell2d,
1795+
nlay=nlay,
1796+
ncpl=ncpl,
1797+
)
1798+
1799+
# 3D node number
1800+
assert vg.get_node((0, 5)) == [5]
1801+
assert vg.get_node((1, 5)) == [105]
1802+
assert vg.get_node((2, 99)) == [299]
1803+
assert vg.get_node([(0, 5), (1, 5)]) == [5, 105]
1804+
1805+
# 2D node number
1806+
assert vg.get_node((0, 5), node2d=True) == [5]
1807+
assert vg.get_node((1, 5), node2d=True) == [5]
1808+
assert vg.get_node((2, 5), node2d=True) == [5]
1809+
assert vg.get_node([(0, 10), (1, 20), (2, 30)], node2d=True) == [10, 20, 30]
1810+
1811+
with pytest.raises(ValueError, match="VertexGrid cellid must be"):
1812+
vg.get_node((0, 1, 2))
1813+
1814+
with pytest.raises(IndexError, match=r"Layer .* out of range"):
1815+
vg.get_node((5, 10))
1816+
1817+
with pytest.raises(IndexError, match=r"Cell2d .* out of range"):
1818+
vg.get_node((0, 200))
1819+
1820+
1821+
def test_unstructured_grid_get_node():
1822+
ncpl = [100]
1823+
vertices = [[i, float(i % 10), float(i // 10)] for i in range(121)]
1824+
iverts = [[i, i + 1, i + 11, i + 10] for i in range(100)]
1825+
ug = UnstructuredGrid(
1826+
vertices=vertices,
1827+
iverts=iverts,
1828+
ncpl=ncpl,
1829+
)
1830+
1831+
# Test plain integers
1832+
assert ug.get_node(5) == [5]
1833+
assert ug.get_node(10) == [10]
1834+
1835+
# Test tuples
1836+
assert ug.get_node((5,)) == [5]
1837+
assert ug.get_node((10,)) == [10]
1838+
1839+
# Test list of integers
1840+
assert ug.get_node([5, 10, 99]) == [5, 10, 99]
1841+
1842+
# Test list of tuples
1843+
assert ug.get_node([(5,), (10,), (99,)]) == [5, 10, 99]
1844+
1845+
# node2d parameter should have no effect
1846+
assert ug.get_node(5, node2d=True) == [5]
1847+
assert ug.get_node((5,), node2d=True) == [5]
1848+
assert ug.get_node([5, 10], node2d=True) == [5, 10]
1849+
1850+
with pytest.raises(ValueError, match="UnstructuredGrid cellid"):
1851+
ug.get_node((0, 1))
1852+
1853+
with pytest.raises(IndexError, match=r"Node .* out of range"):
1854+
ug.get_node(200)

flopy/discretization/structuredgrid.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,7 +1272,7 @@ def get_lrc(self, nodes):
12721272
shape = tuple(dim or 1 for dim in shape)
12731273
return list(zip(*np.unravel_index(nodes, shape)))
12741274

1275-
def get_node(self, lrc_list):
1275+
def get_node(self, lrc_list, cellids=None, node2d=False):
12761276
"""
12771277
Get node number from a list of zero-based MODFLOW
12781278
layer, row, column tuples.
@@ -1282,6 +1282,15 @@ def get_node(self, lrc_list):
12821282
lrc_list : tuple of int or list of tuple of int
12831283
Zero-based layer, row, column tuples
12841284
1285+
.. deprecated:: 3.10
1286+
This parameter is deprecated and will be
1287+
removed in FloPy 3.12. Use cellids instead.
1288+
cellids : tuple of int or list of tuple of int
1289+
Zero-based layer, row, column tuples
1290+
node2d : bool, optional
1291+
If True, return 2D node numbers (ignore layer).
1292+
If False (default), return 3D node numbers.
1293+
12851294
Returns
12861295
-------
12871296
list
@@ -1297,10 +1306,26 @@ def get_node(self, lrc_list):
12971306
>>> sg.get_node([(0, 2, 20), (0, 25, 0), (8, 10, 0)])
12981307
[100, 1000, 10000]
12991308
"""
1300-
if not isinstance(lrc_list, list):
1301-
lrc_list = [lrc_list]
1302-
multi_index = tuple(np.array(lrc_list).T)
1303-
shape = self.shape
1309+
1310+
if lrc_list is not None:
1311+
if cellids is not None:
1312+
raise TypeError("lrc_list and cellids are mutually exclusive")
1313+
cellids = lrc_list
1314+
1315+
if cellids is None:
1316+
raise ValueError("Expected a value for cellids")
1317+
1318+
if not isinstance(cellids, list):
1319+
cellids = [cellids]
1320+
1321+
if node2d:
1322+
rc_list = [(row, col) for lay, row, col in cellids]
1323+
multi_index = tuple(np.array(rc_list).T)
1324+
shape = (self.nrow, self.ncol)
1325+
else:
1326+
multi_index = tuple(np.array(cellids).T)
1327+
shape = self.shape
1328+
13041329
if shape[0] is None:
13051330
shape = tuple(dim or 1 for dim in shape)
13061331
return np.ravel_multi_index(multi_index, shape).tolist()

flopy/discretization/unstructuredgrid.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,63 @@ def get_cell_vertices(self, cellid=None, node=None):
898898
self._copy_cache = True
899899
return cell_vert
900900

901+
def get_node(self, cellids, node2d=False):
902+
"""
903+
Get node number from cellids.
904+
905+
For DISU grids, cellid IS the node number. The node2d
906+
parameter is accepted for API consistency but has no effect.
907+
908+
Parameters
909+
----------
910+
cellid_list : int, tuple of int, or list of int/tuple
911+
DISU cellid(s). Can be a plain integer, a tuple (node,),
912+
or a list of integers or tuples.
913+
node2d : bool, optional
914+
Accepted for API consistency. Has no effect for
915+
unstructured grids (no layer concept).
916+
917+
Returns
918+
-------
919+
list
920+
list of MODFLOW node numbers
921+
922+
Examples
923+
--------
924+
>>> import flopy
925+
>>> ug = flopy.discretization.UnstructuredGrid(ncpl=[100], ...)
926+
>>> ug.get_node(5)
927+
[5]
928+
>>> ug.get_node((5,))
929+
[5]
930+
>>> ug.get_node([5, 10])
931+
[5, 10]
932+
>>> ug.get_node([(5,), (10,)])
933+
[5, 10]
934+
"""
935+
if not isinstance(cellids, list):
936+
cellids = [cellids]
937+
938+
nodes = []
939+
for cellid in cellids:
940+
# Accept both plain integers and tuples
941+
if isinstance(cellid, (int, np.integer)):
942+
node = int(cellid)
943+
elif isinstance(cellid, (tuple, list)):
944+
if len(cellid) != 1:
945+
raise ValueError(
946+
"UnstructuredGrid cellid tuple must have 1 element"
947+
)
948+
node = cellid[0]
949+
else:
950+
raise TypeError(f"Expected int or tuple, got {type(cellid).__name__}")
951+
952+
if node < 0 or node >= self.nnodes:
953+
raise IndexError(f"Node {node} out of range [0, {self.nnodes})")
954+
nodes.append(node)
955+
956+
return nodes
957+
901958
def plot(self, **kwargs):
902959
"""
903960
Plot the grid lines.

flopy/discretization/vertexgrid.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,56 @@ def get_cell_vertices(self, cellid=None, node=None):
544544
self._copy_cache = True
545545
return cell_verts
546546

547+
def get_node(self, cellids, node2d=False):
548+
"""
549+
Get node number from a list of zero-based MODFLOW
550+
(layer, cell2d) tuples.
551+
552+
Parameters
553+
----------
554+
cellid_list : tuple of int or list of tuple of int
555+
Zero-based (layer, cell2d) tuples
556+
node2d : bool, optional
557+
If True, return 2D node numbers (cell2d values).
558+
If False (default), return 3D node numbers.
559+
560+
Returns
561+
-------
562+
list
563+
list of MODFLOW nodes for each (layer, cell2d) tuple
564+
in the input list
565+
566+
Examples
567+
--------
568+
>>> import flopy
569+
>>> vg = flopy.discretization.VertexGrid(nlay=3, ncpl=100, ...)
570+
>>> vg.get_node((0, 5))
571+
[5]
572+
>>> vg.get_node((1, 5))
573+
[105]
574+
>>> vg.get_node([(0, 5), (1, 5)], node2d=True)
575+
[5, 5]
576+
"""
577+
if not isinstance(cellids, list):
578+
cellids = [cellids]
579+
580+
# Validate
581+
for cellid in cellids:
582+
if len(cellid) != 2:
583+
raise ValueError("VertexGrid cellid must be (layer, cell2d) tuple")
584+
585+
if node2d:
586+
return [cell2d for lay, cell2d in cellids]
587+
else:
588+
nodes = []
589+
for lay, cell2d in cellids:
590+
if lay < 0 or lay >= self.nlay:
591+
raise IndexError(f"Layer {lay} out of range [0, {self.nlay})")
592+
if cell2d < 0 or cell2d >= self.ncpl:
593+
raise IndexError(f"Cell2d {cell2d} out of range [0, {self.ncpl})")
594+
nodes.append(lay * self.ncpl + cell2d)
595+
return nodes
596+
547597
def plot(self, **kwargs):
548598
"""
549599
Plot the grid lines.

flopy/export/utils.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -924,13 +924,12 @@ def mflist_export(f: Union[str, PathLike, NetCdf], mfl, **kwargs):
924924
cell_index_name = (
925925
"cellid_cell" if "cellid_cell" in df.columns else "cell"
926926
)
927+
cellids = list(
928+
zip(df[layer_index_name].values, df[cell_index_name].values)
929+
)
930+
nodes = modelgrid.get_node(cellids)
927931
verts = np.array(
928-
[
929-
modelgrid.get_cell_vertices(layer * modelgrid.ncpl + cell)
930-
for layer, cell in zip(
931-
df[layer_index_name].values, df[cell_index_name].values
932-
)
933-
]
932+
[modelgrid.get_cell_vertices(node) for node in nodes]
934933
)
935934
else:
936935
row_index_name = "i" if "i" in df.columns else "cellid_row"

flopy/plot/crosssection.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -795,17 +795,7 @@ def _cellid_to_node(self, cellid):
795795
int
796796
Node number
797797
"""
798-
if len(cellid) == 3:
799-
# Structured grid: (layer, row, col)
800-
layer, row, col = cellid
801-
return layer * self.mg.nrow * self.mg.ncol + row * self.mg.ncol + col
802-
elif len(cellid) == 2:
803-
# Vertex grid: (layer, cell2d)
804-
layer, cell2d = cellid
805-
return layer * self._ncpl + cell2d
806-
else:
807-
# Unstructured grid: (node,)
808-
return cellid[0]
798+
return self.mg.get_node([cellid])[0]
809799

810800
def _plot_vertical_hfb_lines(self, color=None, **kwargs):
811801
"""
@@ -835,12 +825,9 @@ def _plot_vertical_hfb_lines(self, color=None, **kwargs):
835825

836826
for cellid1, cellid2 in self._vertical_hfbs_to_plot:
837827
# Get the 2D cell identifier (row, col for DIS or cell2d for DISV)
838-
if len(cellid1) == 3:
839-
# Structured grid
840-
node_2d = cellid1[1] * self.mg.ncol + cellid1[2]
841-
elif len(cellid1) == 2:
842-
# Vertex grid
843-
node_2d = cellid1[1]
828+
if len(cellid1) == 3 or len(cellid1) == 2:
829+
# Structured or vertex grid
830+
node_2d = self.mg.get_node([cellid1], node2d=True)[0]
844831
else:
845832
# Unstructured - skip for now
846833
continue

flopy/utils/binaryfile/__init__.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1878,15 +1878,11 @@ def _cellids(self, idx):
18781878
return cellid
18791879

18801880
def _cellid_to_node(self, cellids) -> list[int]:
1881-
if self.modelgrid.grid_type == "structured":
1882-
return [
1883-
k * (self.nrow * self.ncol) + i * self.ncol + j + 1
1884-
for k, i, j in cellids
1885-
]
1886-
elif self.modelgrid.grid_type == "vertex":
1887-
return [k * self.modelgrid.ncpl + cell + 1 for k, cell in cellids]
1888-
else:
1889-
return [node + 1 for node in cellids]
1881+
"""Convert 0-based cellids to 1-based MODFLOW node numbers."""
1882+
# Get 0-based nodes from grid, then convert to 1-based (vectorized)
1883+
# UnstructuredGrid.get_node() accepts both plain ints and tuples
1884+
nodes_0based = self.modelgrid.get_node(cellids)
1885+
return (np.array(nodes_0based) + 1).tolist()
18901886

18911887
def get_record(self, idx, full3D=False):
18921888
"""

0 commit comments

Comments
 (0)