11"""
2- Unified geospatial indexing for FloPy grids.
2+ Geospatial indexing for FloPy vertex and unstructured grids.
33
4- Provides efficient spatial queries for all grid types using KD-tree
5- with cell centroids AND vertices for robust edge case handling,
6- plus pre-computed ConvexHull equations for fast point-in-polygon testing.
4+ Provides efficient spatial queries using KD-tree with cell centroids
5+ AND vertices for robust edge case handling, plus pre-computed ConvexHull
6+ equations for fast point-in-polygon testing.
7+
8+ Note: StructuredGrid has its own optimized spatial methods and does not
9+ use this index.
710"""
811
912import numpy as np
1215
1316class GeospatialIndex :
1417 """
15- Geospatial index for efficient geometric queries on model grids.
18+ Geospatial index for efficient geometric queries on vertex/unstructured grids.
1619
1720 Uses KD-tree indexing with cell centroids + vertices to find candidate cells,
1821 then pre-computed ConvexHull hyperplane equations for fast vectorized
@@ -22,10 +25,13 @@ class GeospatialIndex:
2225 - Points near cell boundaries
2326 - Points in thin/sliver cells where centroid is far from the cell
2427
28+ Note: StructuredGrid has its own optimized spatial methods and should not
29+ use this index.
30+
2531 Parameters
2632 ----------
27- grid : Grid
28- Any FloPy grid object (StructuredGrid, VertexGrid, UnstructuredGrid)
33+ grid : VertexGrid or UnstructuredGrid
34+ A FloPy vertex or unstructured grid object
2935 epsilon : float, optional
3036 Tolerance for point-in-cell tests. Used for both bounding box
3137 expansion and ConvexHull hyperplane distance tests. Ensures boundary
@@ -52,10 +58,11 @@ class GeospatialIndex:
5258
5359 Examples
5460 --------
55- >>> from flopy.discretization import StructuredGrid
61+ >>> from flopy.discretization import VertexGrid
5662 >>> from flopy.utils.geospatial_index import GeospatialIndex
5763 >>>
58- >>> grid = StructuredGrid(delr=np.ones(10), delc=np.ones(10))
64+ >>> # Create a simple triangular grid
65+ >>> grid = VertexGrid(vertices, cell2d)
5966 >>> index = GeospatialIndex(grid)
6067 >>>
6168 >>> # Single point query
@@ -67,12 +74,12 @@ class GeospatialIndex:
6774
6875 def __init__ (self , grid , epsilon = 1e-3 ):
6976 """
70- Build geospatial index for a model grid.
77+ Build geospatial index for a vertex or unstructured grid.
7178
7279 Parameters
7380 ----------
74- grid : Grid
75- Any FloPy grid (StructuredGrid, VertexGrid, UnstructuredGrid)
81+ grid : VertexGrid or UnstructuredGrid
82+ A FloPy vertex or unstructured grid
7683 epsilon : float, optional
7784 Tolerance for point-in-cell tests.
7885 Used for bounding box expansion and ConvexHull tests.
@@ -97,37 +104,37 @@ def _build_index(self):
97104 points = []
98105 point_to_cell = []
99106
100- # Get grid dimensions
101- if self .grid .grid_type == "structured" :
102- self .ncells = self .grid .nrow * self .grid .ncol
103- self ._build_structured_index (points , point_to_cell )
104- elif self .grid .grid_type in ("vertex" , "unstructured" ):
105- if self .is_3d :
106- # 3D indexing: index all nnodes with x,y,z coordinates
107- self .ncells = self .grid .nnodes
108- self ._build_3d_index (points , point_to_cell )
109- else :
110- # 2D indexing: index only 2D cells with x,y coordinates
111- if hasattr (self .grid , "ncpl" ):
112- ncpl = self .grid .ncpl
113- if ncpl is None :
114- # ncpl not set, fall back to xcellcenters
115- self .ncells = len (self .grid .xcellcenters )
116- elif np .isscalar (ncpl ):
117- # VertexGrid: ncpl is scalar
118- self .ncells = ncpl
119- else :
120- # UnstructuredGrid: ncpl is array
121- # Use first layer's cell count for 2D spatial indexing
122- if len (ncpl ) > 0 :
123- self .ncells = ncpl [0 ]
124- else :
125- self .ncells = len (self .grid .xcellcenters )
126- else :
127- self .ncells = len (self .grid .xcellcenters )
128- self ._build_vertex_index (points , point_to_cell )
107+ # Get grid dimensions for vertex/unstructured grids
108+ if self .grid .grid_type not in ("vertex" , "unstructured" ):
109+ raise ValueError (
110+ f"GeospatialIndex only supports vertex and unstructured grids, "
111+ f"got: { self .grid .grid_type } "
112+ )
113+
114+ if self .is_3d :
115+ # 3D indexing: index all nnodes with x,y,z coordinates
116+ self .ncells = self .grid .nnodes
117+ self ._build_3d_index (points , point_to_cell )
129118 else :
130- raise ValueError (f"Unknown grid type: { self .grid .grid_type } " )
119+ # 2D indexing: index only 2D cells with x,y coordinates
120+ if hasattr (self .grid , "ncpl" ):
121+ ncpl = self .grid .ncpl
122+ if ncpl is None :
123+ # ncpl not set, fall back to xcellcenters
124+ self .ncells = len (self .grid .xcellcenters )
125+ elif np .isscalar (ncpl ):
126+ # VertexGrid: ncpl is scalar
127+ self .ncells = ncpl
128+ else :
129+ # UnstructuredGrid: ncpl is array
130+ # Use first layer's cell count for 2D spatial indexing
131+ if len (ncpl ) > 0 :
132+ self .ncells = ncpl [0 ]
133+ else :
134+ self .ncells = len (self .grid .xcellcenters )
135+ else :
136+ self .ncells = len (self .grid .xcellcenters )
137+ self ._build_vertex_index (points , point_to_cell )
131138
132139 # Build KD-tree from centroids + vertices
133140 self .points = np .array (points )
@@ -141,26 +148,6 @@ def _build_index(self):
141148 # For 3D, store z-bounds for each cell
142149 self ._precompute_3d_bounds ()
143150
144- def _build_structured_index (self , points , point_to_cell ):
145- """Build index for structured grid - centroids + vertices."""
146- xc = self .grid .xcellcenters
147- yc = self .grid .ycellcenters
148- xv = self .grid .xvertices
149- yv = self .grid .yvertices
150-
151- for i in range (self .grid .nrow ):
152- for j in range (self .grid .ncol ):
153- cellid = i * self .grid .ncol + j
154-
155- # Add centroid
156- points .append ([xc [i , j ], yc [i , j ]])
157- point_to_cell .append (cellid )
158-
159- # Add 4 corner vertices
160- for vi , vj in [(i , j ), (i , j + 1 ), (i + 1 , j + 1 ), (i + 1 , j )]:
161- points .append ([xv [vi , vj ], yv [vi , vj ]])
162- point_to_cell .append (cellid )
163-
164151 def _build_vertex_index (self , points , point_to_cell ):
165152 """Build index for vertex/unstructured grid - centroids + vertices."""
166153 # Disable copy cache to avoid expensive deepcopy during index build
@@ -284,24 +271,8 @@ def _precompute_3d_bounds(self):
284271
285272 def _get_cell_vertices (self , cellid ):
286273 """Get vertices for a cell."""
287- if self .grid .grid_type == "structured" :
288- i = cellid // self .grid .ncol
289- j = cellid % self .grid .ncol
290- xv = self .grid .xvertices
291- yv = self .grid .yvertices
292- return np .array (
293- [
294- [xv [i , j ], yv [i , j ]],
295- [xv [i , j + 1 ], yv [i , j + 1 ]],
296- [xv [i + 1 , j + 1 ], yv [i + 1 , j + 1 ]],
297- [xv [i + 1 , j ], yv [i + 1 , j ]],
298- ]
299- )
300- elif self .grid .grid_type in ("vertex" , "unstructured" ):
301- xv , yv , _ = self .grid .xyzvertices
302- return np .column_stack ([xv [cellid ], yv [cellid ]])
303- else :
304- raise ValueError (f"Unknown grid type: { self .grid .grid_type } " )
274+ xv , yv , _ = self .grid .xyzvertices
275+ return np .column_stack ([xv [cellid ], yv [cellid ]])
305276
306277 def query_point (self , x , y , z = None , k = None ):
307278 """
@@ -540,11 +511,9 @@ def _point_in_cell_3d(self, point, cellid):
540511
541512 def _apply_tiebreaker (self , matching_cells ):
542513 """
543- Apply grid-specific tie-breaking when multiple cells match.
514+ Apply tie-breaking when multiple cells match.
544515
545- Implements the same tie-breaking logic as the original grid implementations:
546- - StructuredGrid: cell with lowest row, then lowest column
547- - VertexGrid/UnstructuredGrid: cell with lowest cell ID
516+ For vertex/unstructured grids: choose cell with lowest cell ID.
548517
549518 Parameters
550519 ----------
@@ -559,21 +528,7 @@ def _apply_tiebreaker(self, matching_cells):
559528 if len (matching_cells ) == 1 :
560529 return matching_cells [0 ]
561530
562- if self .grid .grid_type == "structured" :
563- # For structured grids: choose cell with lowest row, then lowest column
564- # Convert cell indices to (row, col) tuples for comparison
565- rows_cols = []
566- for cellid in matching_cells :
567- row = cellid // self .grid .ncol
568- col = cellid % self .grid .ncol
569- rows_cols .append ((row , col , cellid ))
570-
571- # Sort by row first, then column
572- rows_cols .sort (key = lambda x : (x [0 ], x [1 ]))
573- return rows_cols [0 ][2 ] # Return the cellid
574- else :
575- # For vertex/unstructured grids: choose cell with lowest cell ID
576- return min (matching_cells )
531+ return min (matching_cells )
577532
578533 def _find_layer_for_z (self , icell2d , z ):
579534 """
0 commit comments