Skip to content

Commit 8660978

Browse files
committed
feat(Gridintersect): support handling z-coordinates for points
- add handle_z options: "ignore", "drop" and "return" to intersect() and points_to_cellids() - add method to compute layer position for z coordinates - improve _intersect_point() method by using intersects as starting point.
1 parent 4107bff commit 8660978

1 file changed

Lines changed: 109 additions & 9 deletions

File tree

flopy/utils/gridintersect.py

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from matplotlib.collections import PatchCollection
66
from matplotlib.patches import PathPatch
77
from matplotlib.path import Path
8+
from numpy.lib import recfunctions as nprecfns
89
from pandas import DataFrame
910

10-
from .geometry import transform
1111
from .geospatial_utils import GeoSpatialUtil
1212
from .utl_import import import_optional_dependency
1313

@@ -156,6 +156,7 @@ def intersect(
156156
return_all_intersections=False,
157157
contains_centroid=False,
158158
min_area_fraction=None,
159+
handle_z="ignore",
159160
geo_dataframe=False,
160161
):
161162
"""Method to intersect a shape with a model grid.
@@ -185,6 +186,13 @@ def intersect(
185186
float defining minimum intersection area threshold, if intersection
186187
area is smaller than min_frac_area * cell_area, do not store
187188
intersection result, only used if shape type is "polygon"
189+
handle_z : str, optional
190+
Method for handling z dimension in intersection results for point
191+
intersections. Default is "ignore" which ignores z-dimension. Other
192+
options are "drop" which only returns results for points within grid
193+
top and bottom, or "return" which returns the computed layer position
194+
for each point. Points above the grid are returned as +np.inf and below
195+
the grid as -np.inf.
188196
geo_dataframe : bool, optional
189197
if True, return a geopandas GeoDataFrame, default is False
190198
@@ -214,6 +222,31 @@ def intersect(
214222
sort_by_cellid=sort_by_cellid,
215223
return_all_intersections=return_all_intersections,
216224
)
225+
226+
# handle elevation data for points
227+
# if handle_z is "drop" or "return"
228+
# if shp has z information
229+
# if there are intersection results
230+
if (
231+
(handle_z != "ignore")
232+
and shapely.has_z(shp).any()
233+
and len(rec.cellids) > 0
234+
):
235+
laypos = self.get_layer_from_z(shp, rec.cellids)
236+
if handle_z == "drop":
237+
mask_z = np.isfinite(laypos)
238+
rec = rec[mask_z]
239+
elif handle_z == "return":
240+
# copy data to new array to include layer position
241+
rec = nprecfns.append_fields(
242+
rec,
243+
names="layer",
244+
data=laypos,
245+
dtypes="f8",
246+
usemask=False,
247+
asrecarray=True,
248+
)
249+
217250
elif shapetype in {
218251
"LineString",
219252
"MultiLineString",
@@ -430,10 +463,8 @@ def _intersect_point(
430463
sort_by_cellid=True,
431464
return_all_intersections=False,
432465
):
433-
if self.rtree:
434-
qcellids = self.strtree.query(shp, predicate="intersects")
435-
else:
436-
qcellids = self.filter_query_result(self.cellids, shp)
466+
r = self.intersects(shp, return_nodenumbers=True)
467+
qcellids = r.cellids[np.isfinite(r.cellids)].astype(int)
437468

438469
if sort_by_cellid:
439470
qcellids = np.sort(qcellids)
@@ -442,7 +473,10 @@ def _intersect_point(
442473
# discard empty intersection results
443474
mask_empty = shapely.is_empty(ixresult)
444475
# keep only Point and MultiPoint
445-
mask_type = np.isin(shapely.get_type_id(ixresult), [0, 4])
476+
mask_type = np.isin(
477+
shapely.get_type_id(ixresult),
478+
[shapely.GeometryType.POINT, shapely.GeometryType.MULTIPOINT],
479+
)
446480
ixresult = ixresult[~mask_empty & mask_type]
447481
qcellids = qcellids[~mask_empty & mask_type]
448482

@@ -801,6 +835,7 @@ def _nodenumbers_to_rowcol(self, nodes):
801835
def points_to_cellids(
802836
self,
803837
pts,
838+
handle_z="ignore",
804839
dataframe=False,
805840
return_nodenumbers=False,
806841
):
@@ -813,6 +848,13 @@ def points_to_cellids(
813848
points shape to intersect with the grid
814849
dataframe : bool, optional
815850
if True, return a pandas.DataFrame, default is False
851+
handle_z : str, optional
852+
Method for handling z dimension in intersection results for point
853+
intersections. Default is "ignore" which ignores z-dimension. Other
854+
options are "drop" which only returns results for points within grid
855+
top and bottom, or "return" which returns the computed layer position
856+
for each point. Points above the grid are returned as +np.inf and below
857+
the grid as -np.inf.
816858
return_nodenumbers : bool, optional
817859
if False (default), return cellids of intersected grid cells.
818860
If True, return grid node numbers, i.e. index of entry in
@@ -873,6 +915,22 @@ def points_to_cellids(
873915
if self.mfgrid.grid_type == "structured" and not return_nodenumbers:
874916
rec.cellids = self._nodenumbers_to_rowcol(rec.cellids)
875917

918+
if handle_z != "ignore":
919+
laypos = self.get_layer_from_z(pts, rec.cellids)
920+
if handle_z == "drop":
921+
mask_z = np.isfinite(laypos)
922+
rec = rec[mask_z]
923+
elif handle_z == "return":
924+
# copy data to new array to include layer position
925+
rec = nprecfns.append_fields(
926+
rec,
927+
names="layer",
928+
data=laypos,
929+
dtypes="f8",
930+
usemask=False,
931+
asrecarray=True,
932+
)
933+
876934
if dataframe:
877935
return DataFrame(rec).set_index("shp_ids")
878936
return rec
@@ -1080,11 +1138,53 @@ def plot_intersection_result(self, result, plot_grid=True, ax=None, **kwargs):
10801138

10811139
return ax
10821140

1141+
def get_layer_from_z(self, pts, cellids):
1142+
"""Method to handle z values for points.
10831143
1084-
def _polygon_patch(polygon, **kwargs):
1085-
from matplotlib.patches import PathPatch
1086-
from matplotlib.path import Path
1144+
Parameters
1145+
----------
1146+
pts : shapely.geometry
1147+
points geometry
1148+
cellids : array_like
1149+
array of cellids
10871150
1151+
Returns
1152+
-------
1153+
cellids : array_like
1154+
array of cellids with z values handled
1155+
"""
1156+
1157+
def valid_mask(v):
1158+
if isinstance(v, tuple):
1159+
return True
1160+
else:
1161+
return not np.isnan(v)
1162+
1163+
z_arr = np.atleast_1d(shapely.get_z(pts))
1164+
if self.mfgrid.grid_type == "structured":
1165+
mask_valid = list(map(valid_mask, cellids))
1166+
row, col = list(zip(*cellids[mask_valid]))
1167+
surface_elevations = self.mfgrid.top_botm[:, row, col]
1168+
elif self.mfgrid.grid_type == "vertex":
1169+
mask_valid = ~np.isnan(cellids)
1170+
surface_elevations = self.mfgrid.top_botm[:, cellids[mask_valid]]
1171+
else:
1172+
raise NotImplementedError(
1173+
"get_layer_from_z() is only implemented for "
1174+
"structured and vertex grids."
1175+
)
1176+
zb = surface_elevations < z_arr[mask_valid]
1177+
mask_above = zb.all(axis=0)
1178+
mask_below = (~zb).all(axis=0)
1179+
laypos = (np.nanargmax(zb, axis=0) - 1).astype(float)
1180+
laypos[mask_above] = np.inf
1181+
laypos[mask_below] = -np.inf
1182+
laypos_full = np.full_like(z_arr, np.nan, dtype=float)
1183+
laypos_full[mask_valid] = laypos
1184+
return laypos_full
1185+
1186+
1187+
def _polygon_patch(polygon, **kwargs):
10881188
patch = PathPatch(
10891189
Path.make_compound_path(
10901190
Path(np.asarray(polygon.exterior.coords)[:, :2]),

0 commit comments

Comments
 (0)