Skip to content

Commit 47af097

Browse files
adacovskclaude
andcommitted
fix(geospatial): improve ncpl handling and use cKDTree for performance
This commit improves the GeospatialIndex class to better handle different grid types: - Use cKDTree instead of KDTree for better performance in spatial queries - Add proper handling for VertexGrid (scalar ncpl) vs UnstructuredGrid (array ncpl) - Handle grid_varies_by_layer case for unstructured grids where each layer has different geometry - Fix line length issues in error messages for code style compliance Note: There is a known test failure in test_intersection where structured grids using searchsorted() return different results than DISV grids using the KD-tree approach for edge cases. This will be addressed in a separate commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 42511e0 commit 47af097

File tree

3 files changed

+28
-16
lines changed

3 files changed

+28
-16
lines changed

flopy/discretization/structuredgrid.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1050,7 +1050,8 @@ def intersect(self, x, y, z=None, local=False, forgive=False):
10501050
if np.any(invalid_mask):
10511051
idx = np.where(invalid_mask)[0][0]
10521052
raise ValueError(
1053-
f"x, y point given is outside of the model area: ({x[idx]}, {y[idx]})"
1053+
f"x, y point given is outside of the model area: "
1054+
f"({x[idx]}, {y[idx]})"
10541055
)
10551056

10561057
# If either row or col is NaN, set both to NaN

flopy/discretization/unstructuredgrid.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,10 @@ def intersect(self, x, y, z=None, local=False, forgive=False):
819819
results[i] = np.nan
820820
else:
821821
xi, yi = x[i], y[i]
822-
raise ValueError(f"point ({xi}, {yi}, {zi}) is outside of the model area")
822+
raise ValueError(
823+
f"point ({xi}, {yi}, {zi}) is outside of the "
824+
f"model area"
825+
)
823826

824827
# Return scalar if input was scalar, otherwise return array
825828
if is_scalar_input:

flopy/utils/geospatial_index.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"""
88

99
import numpy as np
10-
from scipy.spatial import KDTree, ConvexHull
10+
from scipy.spatial import ConvexHull, cKDTree
1111

1212

1313
class GeospatialIndex:
@@ -69,10 +69,8 @@ def __init__(self, grid):
6969

7070
def _build_index(self):
7171
"""
72-
Build KD-tree with centroids + vertices and pre-compute ConvexHull equations.
73-
74-
Strategy: Include both centroids and vertices in KD-tree to handle edge cases,
75-
but use smart querying to ensure we get k unique cells.
72+
Build KD-tree with centroids + vertices and pre-compute
73+
ConvexHull equations.
7674
"""
7775
points = []
7876
point_to_cell = []
@@ -83,7 +81,22 @@ def _build_index(self):
8381
self._build_structured_index(points, point_to_cell)
8482
elif self.grid.grid_type in ('vertex', 'unstructured'):
8583
if hasattr(self.grid, 'ncpl'):
86-
self.ncells = self.grid.ncpl
84+
ncpl = self.grid.ncpl
85+
if np.isscalar(ncpl):
86+
# VertexGrid: ncpl is scalar
87+
self.ncells = ncpl
88+
else:
89+
# UnstructuredGrid: ncpl is array
90+
if (hasattr(self.grid, 'grid_varies_by_layer')
91+
and self.grid.grid_varies_by_layer):
92+
# Each layer has different geometry
93+
self.ncells = self.grid.nnodes
94+
else:
95+
# Same 2D grid for all layers
96+
if len(ncpl) > 0:
97+
self.ncells = ncpl[0]
98+
else:
99+
self.ncells = len(self.grid.xcellcenters)
87100
else:
88101
self.ncells = len(self.grid.xcellcenters)
89102
self._build_vertex_index(points, point_to_cell)
@@ -93,7 +106,7 @@ def _build_index(self):
93106
# Build KD-tree from centroids + vertices
94107
self.points = np.array(points)
95108
self.point_to_cell = np.array(point_to_cell, dtype=int)
96-
self.tree = KDTree(self.points)
109+
self.tree = cKDTree(self.points)
97110

98111
# Pre-compute ConvexHull equations and bounding boxes
99112
self._precompute_hulls()
@@ -144,11 +157,7 @@ def _build_vertex_index(self, points, point_to_cell):
144157
self.grid._copy_cache = original_copy_cache
145158

146159
def _precompute_hulls(self):
147-
"""
148-
Pre-compute ConvexHull equations and bounding boxes for all cells.
149-
150-
This is the key optimization - compute once, reuse many times.
151-
"""
160+
"""Pre-compute ConvexHull equations and bounding boxes for all cells."""
152161
# Disable copy cache to avoid expensive deepcopy during precomputation
153162
original_copy_cache = getattr(self.grid, '_copy_cache', True)
154163
self.grid._copy_cache = False
@@ -291,8 +300,7 @@ def _get_k_unique_cells(self, point, k):
291300
unique_cells : ndarray
292301
Array of up to k unique cell indices, ordered by distance
293302
"""
294-
# Start by querying k*5 points (centroid + ~4 vertices per cell)
295-
# This should give us at least k unique cells in most cases
303+
# Query k*5 points to get k unique cells (accounting for vertices)
296304
k_points = min(k * 5, len(self.points))
297305

298306
distances, indices = self.tree.query(point, k=k_points)

0 commit comments

Comments
 (0)