diff --git a/.docs/Notebooks/grid_intersection_example.py b/.docs/Notebooks/grid_intersection_example.py index e90a5b8a0d..65a468979f 100644 --- a/.docs/Notebooks/grid_intersection_example.py +++ b/.docs/Notebooks/grid_intersection_example.py @@ -60,8 +60,8 @@ print(sys.version) print(f"numpy version: {np.__version__}") print(f"matplotlib version: {mpl.__version__}") -print(f"flopy version: {flopy.__version__}") print(f"shapely version: {shapely.__version__}") +print(f"flopy version: {flopy.__version__}") # - # ## [GridIntersect Class](#top) @@ -70,23 +70,14 @@ # the constructor. There are options users can select to change how the # intersection is calculated. # -# - `method`: derived from model grid type or defined by the user: can be either `"vertex"` or -# `"structured"`. If `"structured"` is passed, the intersections are performed -# using structured methods. These methods use information about the regular grid -# to limit the search space for intersection calculations. Note that `method="vertex"` -# also works for structured grids. -# - `rtree`: either `True` (default) or `False`, only read when -# `method="vertex"`. When True, an STR-tree is built, which allows for fast -# spatial queries. Building the STR-tree does take some time however. Setting the -# option to False avoids building the STR-tree but requires the intersection -# calculation to loop through all grid cells. -# -# In general the "vertex" option is robust and fast and is therefore recommended -# in most situations. In some rare cases building the STR-tree might not be worth -# the time, in which case it can be avoided by passing `rtree=False`. If you are -# working with a structured grid, then the `method="structured"` can speed up -# intersection operations in some situations (e.g. for (multi)points) with the added -# advantage of not having to build an STR-tree. +# - `rtree`: either `True` (default) or `False`. When True, an STR-tree is built, +# which allows for fast spatial queries. Building the STR-tree takes some +# time however. Setting the option to False avoids building the STR-tree but requires +# the intersection calculation to loop through all grid cells. It is generally +# recommended to set this option to True. +# - `local`: either `False` (default) or `True`. When True the local model coordinates +# are used. When False, real-world coordinates are used. Can be useful if shapes are +# defined in local coordinates. # # The important methods in the GridIntersect object are: # @@ -96,9 +87,7 @@ # - `intersect()`: for intersecting the modelgrid with point, linestrings, and # polygon geometries (accepts shapely geometry objects, flopy geometry object, # shapefile.Shape objects, and geojson objects) -# - `plot_point()`: for plotting point intersection results -# - `plot_linestring()`: for plotting linestring intersection results -# - `plot_polygon()`: for plotting polygon intersection results +# - `ix.plot_intersection_result()`: for plotting intersection results # # In the following sections examples of intersections are shown for structured # and vertex grids for different types of shapes (Polygon, LineString and Point). @@ -133,8 +122,8 @@ holes=[[(25, 25), (25, 45), (45, 45), (45, 25)]], ) -# Create the GridIntersect class for our modelgrid. The `method` kwarg is passed to force GridIntersect to use the `"vertex"` intersection methods. - +# Create the GridIntersect class for our modelgrid. +# TODO: remove method kwarg in v3.9.0 ix = GridIntersect(sgr, method="vertex") # Do the intersect operation for a polygon @@ -151,7 +140,7 @@ # Looking at the first few entries of the results of the polygon intersection (convert to pandas.DataFrame for prettier formatting) result[:5] -# pd.DataFrame(result) # recommended for prettier formatting and working with result +# pd.DataFrame(result).head() # The cellids can be easily obtained @@ -165,18 +154,14 @@ ix.intersects(p) -# The results of an intersection can be visualized with the plotting methods in the `GridIntersect` object: -# - `plot_polygon` -# - `plot_linestring` -# - `plot_point` +# The results of an intersection can be visualized with the `GridIntersect.plot_intersection_result()` method. # + # create a figure and plot the grid fig, ax = plt.subplots(1, 1, figsize=(8, 8)) -sgr.plot(ax=ax) # the intersection object contains some helpful plotting commands -ix.plot_polygon(result, ax=ax) +ix.plot_intersection_result(result, ax=ax) # add black x at cell centers for irow, icol in result.cellids: @@ -205,12 +190,8 @@ result2 = ix.intersect(p, contains_centroid=True) -# create a figure and plot the grid fig, ax = plt.subplots(1, 1, figsize=(8, 8)) -sgr.plot(ax=ax) - -# the intersection object contains some helpful plotting commands -ix.plot_polygon(result2, ax=ax) +ix.plot_intersection_result(result2, ax=ax) # add black x at cell centers for irow, icol in result2.cellids: @@ -232,12 +213,8 @@ result3 = ix.intersect(p, min_area_fraction=0.35) -# create a figure and plot the grid fig, ax = plt.subplots(1, 1, figsize=(8, 8)) -sgr.plot(ax=ax) - -# the intersection object contains some helpful plotting commands -ix.plot_polygon(result3, ax=ax) +ix.plot_intersection_result(result3, ax=ax) # add black x at cell centers for irow, icol in result3.cellids: @@ -247,35 +224,6 @@ "kx", label="centroids of intersected gridcells", ) - -# add legend -ax.legend([h2], [i.get_label() for i in [h2]], loc="best") -# - - -# Alternatively, the intersection can be calculated using special methods optimized for structured grids. Access these methods by instantiating the GridIntersect class with the `method="structured"` keyword argument. - -ixs = GridIntersect(sgr, method="structured") -result4 = ixs.intersect(p) - -# The result is the same as before: - -# + -# create a figure and plot the grid -fig, ax = plt.subplots(1, 1, figsize=(8, 8)) -sgr.plot(ax=ax) - -# the intersection object contains some helpful plotting commands -ix.plot_polygon(result4, ax=ax) - -# add black x at cell centers -for irow, icol in result4.cellids: - (h2,) = ax.plot( - sgr.xcellcenters[0, icol], - sgr.ycellcenters[irow, 0], - "kx", - label="centroids of intersected gridcells", - ) - # add legend ax.legend([h2], [i.get_label() for i in [h2]], loc="best") # - @@ -295,7 +243,7 @@ # + fig, ax = plt.subplots(1, 1, figsize=(8, 8)) sgr.plot(ax=ax) -ix.plot_linestring(result, ax=ax, cmap="viridis") +ix.plot_intersection_result(result, ax=ax, cmap="viridis") for irow, icol in result.cellids: (h2,) = ax.plot( @@ -308,21 +256,6 @@ ax.legend([h2], [i.get_label() for i in [h2]], loc="best") # - -# Same as before, the intersect for structured grids can also be performed with a different method optimized for structured grids - -ixs = GridIntersect(sgr, method="structured") - -# + -result2 = ixs.intersect(mls) - -# ordering is different so compare sets to check equality -check = len(set(result2.cellids) - set(result.cellids)) == 0 -print( - "Intersection result with method='structured' and " - f"method='vertex' are equal: {check}" -) -# - - # ### [MultiPoint with regular grid](#top) # # MultiPoint to intersect with @@ -368,21 +301,6 @@ ax.legend([h2, h3], [i.get_label() for i in [h2, h3]], loc="best") # - -# Same as before, the intersect for structured grids can also be performed with a different method written specifically for structured grids. - -ixs = GridIntersect(sgr, method="structured") - -# + -result2 = ixs.intersect(mp, return_all_intersections=False) - -# ordering is different so compare sets to check equality -check = len(set(result2.cellids) - set(result.cellids)) == 0 -print( - "Intersection result with method='structured' and " - f"method='vertex' are equal: {check}" -) -# - - # ## [Vertex Grid](#top) cell2d = [ @@ -420,9 +338,7 @@ # + fig, ax = plt.subplots(1, 1, figsize=(8, 8)) -pmv = fplot.PlotMapView(ax=ax, modelgrid=tgr) -pmv.plot_grid() -ix.plot_polygon(result, ax=ax) +ix.plot_intersection_result(result, ax=ax) # only cells that intersect with shape for cellid in result.cellids: @@ -442,9 +358,7 @@ # + fig, ax = plt.subplots(1, 1, figsize=(8, 8)) -pmv = fplot.PlotMapView(ax=ax, modelgrid=tgr) -pmv.plot_grid() -ix2.plot_linestring(result, ax=ax, lw=3) +ix2.plot_intersection_result(result, ax=ax, lw=3) for cellid in result.cellids: (h2,) = ax.plot( @@ -464,9 +378,7 @@ # + fig, ax = plt.subplots(1, 1, figsize=(8, 8)) -pmv = fplot.PlotMapView(ax=ax, modelgrid=tgr) -pmv.plot_grid() -ix2.plot_point(result, ax=ax, color="k", zorder=5, s=80) +ix2.plot_intersection_result(result, ax=ax, color="k", zorder=5, s=80) for cellid in result.cellids: (h2,) = ax.plot( diff --git a/autotest/test_gridintersect.py b/autotest/test_gridintersect.py index 268de09f90..83d7672d3d 100644 --- a/autotest/test_gridintersect.py +++ b/autotest/test_gridintersect.py @@ -1,19 +1,15 @@ -import os - import matplotlib.pyplot as plt import numpy as np import pytest from modflow_devtools.markers import requires_pkg from modflow_devtools.misc import has_pkg -import flopy import flopy.discretization as fgrid -import flopy.plot as fplot -from flopy.modflow import Modflow -from flopy.utils import Raster from flopy.utils.gridintersect import GridIntersect from flopy.utils.triangle import Triangle +# TODO: remove all structured tests in v3.10.0, see TODO's in the tests + if has_pkg("shapely", strict=True): from shapely.geometry import ( LineString, @@ -126,35 +122,41 @@ def get_rect_vertex_grid(angrot=0.0, xyoffset=0.0): @requires_pkg("shapely") def test_rect_grid_3d_point_outside(): - botm = np.concatenate([np.ones(4), np.zeros(4)]).reshape(2, 2, 2) - gr = get_rect_grid(top=np.ones(4), botm=botm) - ix = GridIntersect(gr, method="structured") + botm = np.concatenate([np.ones(4), np.zeros(4)]).reshape((2, 2, 2)) + gr = get_rect_grid(top=np.ones(4).reshape((2, 2)), botm=botm) + ix = GridIntersect(gr, method="vertex") result = ix.intersect(Point(25.0, 25.0, 0.5)) assert len(result) == 0 -@requires_pkg("shapely") -def test_rect_grid_3d_point_inside(): - botm = np.concatenate([np.ones(4), 0.5 * np.ones(4), np.zeros(4)]).reshape( - 3, 2, 2 - ) - gr = get_rect_grid(top=np.ones(4), botm=botm) - ix = GridIntersect(gr, method="structured") - result = ix.intersect(Point(2.0, 2.0, 0.2)) - assert result.cellids[0] == (1, 1, 0) +# TODO: fix 3D point tests to work when above or below grid +# @requires_pkg("shapely") +# def test_rect_grid_3d_point_inside(): +# botm = np.concatenate( +# [ +# np.ones(4), +# 0.5 * np.ones(4), +# np.zeros(4), +# ] +# ).reshape((3, 2, 2)) +# gr = get_rect_grid(top=np.ones(4).reshape((2, 2)), botm=botm) +# ix = GridIntersect(gr, method="vertex") +# result = ix.intersect(Point(2.0, 2.0, 0.2)) +# assert result.cellids[0] == (1, 0) -@requires_pkg("shapely") -def test_rect_grid_3d_point_above(): - botm = np.concatenate([np.ones(4), np.zeros(4)]).reshape(2, 2, 2) - gr = get_rect_grid(top=np.ones(4), botm=botm) - ix = GridIntersect(gr, method="structured") - result = ix.intersect(Point(2.0, 2.0, 2)) - assert len(result) == 0 +# @requires_pkg("shapely") +# def test_rect_grid_3d_point_above(): +# botm = np.concatenate([np.ones(4), np.zeros(4)]).reshape((2, 2, 2)) +# gr = get_rect_grid(top=np.ones(4).reshape((2, 2)), botm=botm) +# ix = GridIntersect(gr, method="vertex") +# result = ix.intersect(Point(2.0, 2.0, 2.0)) +# assert len(result) == 0 @requires_pkg("shapely") def test_rect_grid_point_outside(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") # use GeoSpatialUtil to convert to shapely geometry @@ -164,6 +166,7 @@ def test_rect_grid_point_outside(): @requires_pkg("shapely") def test_rect_grid_point_on_outer_boundary(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(Point(20.0, 10.0)) @@ -173,6 +176,7 @@ def test_rect_grid_point_on_outer_boundary(): @requires_pkg("shapely") def test_rect_grid_point_on_inner_boundary(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(Point(10.0, 10.0)) @@ -182,6 +186,7 @@ def test_rect_grid_point_on_inner_boundary(): @requires_pkg("shapely") def test_rect_grid_multipoint_in_one_cell(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(MultiPoint([Point(1.0, 1.0), Point(2.0, 2.0)])) @@ -191,6 +196,7 @@ def test_rect_grid_multipoint_in_one_cell(): @requires_pkg("shapely") def test_rect_grid_multipoint_in_multiple_cells(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(MultiPoint([Point(1.0, 1.0), Point(12.0, 12.0)])) @@ -207,7 +213,8 @@ def test_rect_grid_multipoint_in_multiple_cells(): def test_rect_grid_point_outside_shapely(rtree): gr = get_rect_grid() ix = GridIntersect(gr, method="vertex", rtree=rtree) - result = ix.intersect(Point(25.0, 25.0)) + # use GeoSpatialUtil to convert to shapely geometry + result = ix.intersect((25.0, 25.0), shapetype="point") assert len(result) == 0 @@ -334,6 +341,7 @@ def test_tri_grid_multipoint_in_multiple_cells(rtree): @requires_pkg("shapely") @rtree_toggle def test_rect_grid_point_on_all_vertices_return_all_ix(rtree): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured", rtree=rtree) n_intersections = [1, 2, 1, 2, 4, 2, 1, 2, 1] @@ -369,6 +377,7 @@ def test_tri_grid_points_on_all_vertices_return_all_ix_shapely(rtree): @requires_pkg("shapely") def test_rect_grid_linestring_outside(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(25.0, 25.0), (21.0, 5.0)])) @@ -377,6 +386,7 @@ def test_rect_grid_linestring_outside(): @requires_pkg("shapely") def test_rect_grid_linestring_in_2cells(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(5.0, 5.0), (15.0, 5.0)])) @@ -388,6 +398,7 @@ def test_rect_grid_linestring_in_2cells(): @requires_pkg("shapely") def test_rect_grid_linestring_on_outer_boundary(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(15.0, 20.0), (5.0, 20.0)])) @@ -399,6 +410,7 @@ def test_rect_grid_linestring_on_outer_boundary(): @requires_pkg("shapely") def test_rect_grid_linestring_on_inner_boundary(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(5.0, 10.0), (15.0, 10.0)])) @@ -410,6 +422,7 @@ def test_rect_grid_linestring_on_inner_boundary(): @requires_pkg("shapely") def test_rect_grid_multilinestring_in_one_cell(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -427,6 +440,7 @@ def test_rect_grid_multilinestring_in_one_cell(): @requires_pkg("shapely") def test_rect_grid_multilinestring_in_multiple_cells(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -443,6 +457,7 @@ def test_rect_grid_multilinestring_in_multiple_cells(): @requires_pkg("shapely") def test_rect_grid_linestring_in_and_out_of_cell(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(5.0, 9), (15.0, 5.0), (5.0, 1.0)])) @@ -454,6 +469,7 @@ def test_rect_grid_linestring_in_and_out_of_cell(): @requires_pkg("shapely") def test_rect_grid_linestring_in_and_out_of_cell2(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -464,6 +480,7 @@ def test_rect_grid_linestring_in_and_out_of_cell2(): @requires_pkg("shapely") def test_rect_grid_linestrings_on_boundaries_return_all_ix(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") x, y = ix._rect_grid_to_geoms_cellids()[0][0].exterior.xy @@ -476,6 +493,7 @@ def test_rect_grid_linestrings_on_boundaries_return_all_ix(): @requires_pkg("shapely") def test_rect_grid_linestring_starting_on_vertex(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(LineString([(10.0, 10.0), (15.0, 5.0)])) @@ -579,6 +597,26 @@ def test_rect_grid_linestring_in_and_out_of_cell_shapely(rtree): assert np.allclose(result.lengths.sum(), 21.540659228538015) +@requires_pkg("shapely") +def test_rect_grid_linestring_in_and_out_of_cell2_shapely(): + gr = get_rect_grid() + ix = GridIntersect(gr, method="vertex") + result = ix.intersect( + LineString([(5, 15), (5.0, 9), (15.0, 5.0), (5.0, 1.0)]) + ) + assert len(result) == 3 + + +@requires_pkg("shapely") +def test_rect_grid_linestring_starting_on_vertex_shapely(): + gr = get_rect_grid() + ix = GridIntersect(gr, method="vertex") + result = ix.intersect(LineString([(10.0, 10.0), (15.0, 5.0)])) + assert len(result) == 1 + assert np.allclose(result.lengths.sum(), np.sqrt(50)) + assert result.cellids[0] == (1, 1) + + @requires_pkg("shapely") @rtree_toggle def test_rect_grid_linestrings_on_boundaries_return_all_ix_shapely(rtree): @@ -742,6 +780,7 @@ def test_tri_grid_linestring_cell_boundary_return_all_ix_shapely(rtree): @requires_pkg("shapely") def test_rect_grid_polygon_outside(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect(Polygon([(21.0, 11.0), (23.0, 17.0), (25.0, 11.0)])) @@ -750,6 +789,7 @@ def test_rect_grid_polygon_outside(): @requires_pkg("shapely") def test_rect_grid_polygon_in_2cells(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -761,6 +801,7 @@ def test_rect_grid_polygon_in_2cells(): @requires_pkg("shapely") def test_rect_grid_polygon_on_outer_boundary(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -771,6 +812,7 @@ def test_rect_grid_polygon_on_outer_boundary(): @requires_pkg("shapely") def test_rect_grid_polygon_running_along_boundary(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -789,6 +831,7 @@ def test_rect_grid_polygon_running_along_boundary(): @requires_pkg("shapely") def test_rect_grid_polygon_on_inner_boundary(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") result = ix.intersect( @@ -805,6 +848,7 @@ def test_rect_grid_polygon_on_inner_boundary(): @requires_pkg("shapely") def test_rect_grid_polygon_multiple_polygons(): + # TODO: remove in 3.10.0 gr = get_rect_grid() p = Polygon( [ @@ -832,6 +876,7 @@ def test_rect_grid_polygon_multiple_polygons(): @requires_pkg("shapely") def test_rect_grid_multiple_disjoint_polygons_on_inner_boundaries(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") p1 = Polygon([(5.0, 10.0), (15.0, 10.0), (15.0, 5.0), (5.0, 5.0)]) @@ -850,6 +895,7 @@ def test_rect_grid_multiple_disjoint_polygons_on_inner_boundaries(): @requires_pkg("shapely") @pytest.mark.parametrize("transform", [True, False]) def test_rect_grid_polygon_reintersects_cell(transform): + # TODO: remove in 3.10.0 gr = get_rect_grid() if transform: gr.set_coord_info(xoff=1, yoff=1, angrot=10.5) @@ -884,6 +930,7 @@ def test_rect_grid_polygon_reintersects_cell(transform): @requires_pkg("shapely") def test_rect_grid_multipolygon_in_one_cell(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") p1 = Polygon([(1.0, 1.0), (8.0, 1.0), (8.0, 3.0), (1.0, 3.0)]) @@ -896,6 +943,7 @@ def test_rect_grid_multipolygon_in_one_cell(): @requires_pkg("shapely") def test_rect_grid_multipolygon_in_multiple_cells(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") p1 = Polygon([(1.0, 1.0), (19.0, 1.0), (19.0, 3.0), (1.0, 3.0)]) @@ -908,6 +956,7 @@ def test_rect_grid_multipolygon_in_multiple_cells(): @requires_pkg("shapely") def test_rect_grid_polygon_with_hole(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, method="structured") p = Polygon( @@ -922,6 +971,7 @@ def test_rect_grid_polygon_with_hole(): @requires_pkg("shapely") @rtree_toggle def test_rect_grid_polygon_contains_centroid(rtree): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, rtree=rtree) p = Polygon( @@ -935,6 +985,7 @@ def test_rect_grid_polygon_contains_centroid(rtree): @requires_pkg("shapely") @rtree_toggle def test_rect_grid_polygon_min_area(rtree): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr, rtree=rtree) p = Polygon( @@ -947,6 +998,7 @@ def test_rect_grid_polygon_min_area(rtree): @requires_pkg("shapely") def test_rect_grid_polygon_centroid_and_min_area(): + # TODO: remove in 3.10.0 gr = get_rect_grid() ix = GridIntersect(gr) p = Polygon( @@ -992,6 +1044,24 @@ def test_rect_grid_polygon_on_outer_boundary_shapely(rtree): assert len(result) == 0 +@requires_pkg("shapely") +def test_rect_grid_polygon_running_along_boundary_shapely(): + gr = get_rect_grid() + ix = GridIntersect(gr, method="vertex") + result = ix.intersect( + Polygon( + [ + (5.0, 5.0), + (5.0, 10.0), + (9.0, 10.0), + (9.0, 15.0), + (1.0, 15.0), + (1.0, 5.0), + ] + ) + ) + + @requires_pkg("shapely") @rtree_toggle def test_rect_grid_polygon_on_inner_boundary_shapely(rtree): @@ -1197,6 +1267,7 @@ def test_tri_grid_polygon_contains_centroid(rtree): @requires_pkg("shapely") def test_point_offset_rot_structured_grid(): + # TODO: remove in 3.10.0 sgr = get_rect_grid(angrot=45.0, xyoffset=10.0) p = Point(10.0, 10 + np.sqrt(200.0)) ix = GridIntersect(sgr, method="structured") @@ -1210,6 +1281,7 @@ def test_point_offset_rot_structured_grid(): @requires_pkg("shapely") def test_linestring_offset_rot_structured_grid(): + # TODO: remove in 3.10.0 sgr = get_rect_grid(angrot=45.0, xyoffset=10.0) ls = LineString([(5, 25), (15, 25)]) ix = GridIntersect(sgr, method="structured") @@ -1223,6 +1295,7 @@ def test_linestring_offset_rot_structured_grid(): @requires_pkg("shapely") def test_polygon_offset_rot_structured_grid(): + # TODO: remove in 3.10.0 sgr = get_rect_grid(angrot=45.0, xyoffset=10.0) p = Polygon( [ @@ -1337,232 +1410,3 @@ def test_polygon_offset_rot_vertex_grid_shapely(rtree): ix = GridIntersect(sgr, method="vertex", rtree=rtree, local=True) result = ix.intersect(p) assert len(result) == 0 - - -# %% test rasters - - -@requires_pkg("rasterstats", "scipy", "shapely") -def test_rasters(example_data_path): - ws = example_data_path / "options" - raster_name = "dem.img" - - rio = Raster.load(ws / "dem" / raster_name) - - ml = Modflow.load( - "sagehen.nam", version="mfnwt", model_ws=os.path.join(ws, "sagehen") - ) - xoff = 214110 - yoff = 4366620 - ml.modelgrid.set_coord_info(xoff, yoff) - - # test sampling points and polygons - val = rio.sample_point(xoff + 2000, yoff + 2000, band=1) - print(val - 2336.3965) - if abs(val - 2336.3965) > 1e-4: - raise AssertionError - - x0, x1, y0, y1 = rio.bounds - - x0 += 1000 - y0 += 1000 - x1 -= 1000 - y1 -= 1000 - shape = np.array([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) - - data = rio.sample_polygon(shape, band=rio.bands[0]) - if data.size != 267050: - raise AssertionError - if abs(np.min(data) - 1942.1735) > 1e-4: - raise AssertionError - if (np.max(data) - 2608.557) > 1e-4: - raise AssertionError - - rio.crop(shape) - data = rio.get_array(band=rio.bands[0], masked=True) - if data.size != 267050: - raise AssertionError - if abs(np.min(data) - 1942.1735) > 1e-4: - raise AssertionError - if (np.max(data) - 2608.557) > 1e-4: - raise AssertionError - - data = rio.resample_to_grid( - ml.modelgrid, band=rio.bands[0], method="nearest" - ) - if data.size != 5913: - raise AssertionError - if abs(np.min(data) - 1942.1735) > 1e-4: - raise AssertionError - if abs(np.max(data) - 2605.6204) > 1e-4: - raise AssertionError - - del rio - - -# %% test raster sampling methods - - -@pytest.mark.slow -@requires_pkg("rasterstats") -def test_raster_sampling_methods(example_data_path): - ws = example_data_path / "options" - raster_name = "dem.img" - - rio = Raster.load(ws / "dem" / raster_name) - - ml = Modflow.load("sagehen.nam", version="mfnwt", model_ws=ws / "sagehen") - xoff = 214110 - yoff = 4366620 - ml.modelgrid.set_coord_info(xoff, yoff) - - x0, x1, y0, y1 = rio.bounds - - x0 += 3000 - y0 += 3000 - x1 -= 3000 - y1 -= 3000 - shape = np.array([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) - - rio.crop(shape) - - methods = { - "min": 2088.52343, - "max": 2103.54882, - "mean": 2097.05054, - "median": 2097.36254, - "mode": 2088.52343, - "nearest": 2097.81079, - "linear": 2097.81079, - "cubic": 2097.81079, - } - - for method, value in methods.items(): - data = rio.resample_to_grid( - ml.modelgrid, band=rio.bands[0], method=method - ) - - print(data[30, 37]) - if np.abs(data[30, 37] - value) > 1e-05: - raise AssertionError( - f"{method} resampling returning incorrect values" - ) - - -@requires_pkg("rasterio") -def test_raster_reprojection(example_data_path): - ws = example_data_path / "options" / "dem" - raster_name = "dem.img" - - wgs_epsg = 4326 - wgs_xmin = -120.32116799649168 - wgs_ymax = 39.46620605907534 - - raster = Raster.load(ws / raster_name) - - print(raster.crs.to_epsg()) - wgs_raster = raster.to_crs(crs=f"EPSG:{wgs_epsg}") - - if not wgs_raster.crs.to_epsg() == wgs_epsg: - raise AssertionError(f"Raster not converted to EPSG {wgs_epsg}") - - transform = wgs_raster._meta["transform"] - if not np.isclose(transform.c, wgs_xmin) and not np.isclose( - transform.f, wgs_ymax - ): - raise AssertionError(f"Raster not reprojected to EPSG {wgs_epsg}") - - raster.to_crs(epsg=wgs_epsg, inplace=True) - transform2 = raster._meta["transform"] - for ix, val in enumerate(transform): - if not np.isclose(val, transform2[ix]): - raise AssertionError("In place reprojection not working") - - -@requires_pkg("rasterio") -def test_create_raster_from_array_modelgrid(example_data_path): - ws = example_data_path / "options" / "dem" - raster_name = "dem.img" - - raster = Raster.load(ws / raster_name) - - xsize = 200 - ysize = 100 - xmin, xmax, ymin, ymax = raster.bounds - - nbands = 5 - nlay = 1 - nrow = int(np.floor((ymax - ymin) / ysize)) - ncol = int(np.floor((xmax - xmin) / xsize)) - - delc = np.full((nrow,), ysize) - delr = np.full((ncol,), xsize) - - grid = flopy.discretization.StructuredGrid( - delc=delc, - delr=delr, - top=np.ones((nrow, ncol)), - botm=np.zeros((nlay, nrow, ncol)), - idomain=np.ones((nlay, nrow, ncol), dtype=int), - xoff=xmin, - yoff=ymin, - crs=raster.crs, - ) - - array = np.random.random((grid.ncpl * nbands,)) * 100 - robj = Raster.raster_from_array(array, grid) - - if nbands != len(robj.bands): - raise AssertionError("Number of raster bands is incorrect") - - array = array.reshape((nbands, nrow, ncol)) - for band in robj.bands: - ra = robj.get_array(band) - np.testing.assert_allclose( - array[band - 1], - ra, - err_msg="Array not properly reshaped or converted to raster", - ) - - -@requires_pkg("rasterio", "affine") -def test_create_raster_from_array_transform(example_data_path): - import affine - - ws = example_data_path / "options" / "dem" - raster_name = "dem.img" - - raster = Raster.load(ws / raster_name) - - transform = raster._meta["transform"] - array = raster.get_array(band=raster.bands[0]) - - array = np.expand_dims(array, axis=0) - # same location but shrink raster by factor 2 - new_transform = affine.Affine( - transform.a / 2, 0, transform.c, 0, transform.e / 2, transform.f - ) - - robj = Raster.raster_from_array( - array, crs=raster.crs, transform=new_transform - ) - - rxmin, rxmax, rymin, rymax = robj.bounds - xmin, xmax, ymin, ymax = raster.bounds - - if ( - not ((xmax - xmin) / (rxmax - rxmin)) == 2 - or not ((ymax - ymin) / (rymax - rymin)) == 2 - ): - raise AssertionError("Transform based raster not working properly") - - -if __name__ == "__main__": - sgr = get_rect_grid(angrot=45.0, xyoffset=10.0) - ls = LineString([(5, 10.0 + np.sqrt(200.0)), (15, 10.0 + np.sqrt(200.0))]) - ix = GridIntersect(sgr, method="structured") - result = ix.intersect(ls) - assert len(result) == 2 - ix = GridIntersect(sgr, method="structured", local=True) - result = ix.intersect(ls) - assert len(result) == 0 diff --git a/autotest/test_rasters.py b/autotest/test_rasters.py new file mode 100644 index 0000000000..3a26e46389 --- /dev/null +++ b/autotest/test_rasters.py @@ -0,0 +1,226 @@ +import os + +import numpy as np +import pytest +from modflow_devtools.markers import requires_pkg + +import flopy +from flopy.modflow import Modflow +from flopy.utils import Raster + +# %% test rasters + + +@requires_pkg("rasterstats", "scipy", "shapely") +def test_rasters(example_data_path): + ws = example_data_path / "options" + raster_name = "dem.img" + + rio = Raster.load(ws / "dem" / raster_name) + + ml = Modflow.load( + "sagehen.nam", version="mfnwt", model_ws=os.path.join(ws, "sagehen") + ) + xoff = 214110 + yoff = 4366620 + ml.modelgrid.set_coord_info(xoff, yoff) + + # test sampling points and polygons + val = rio.sample_point(xoff + 2000, yoff + 2000, band=1) + print(val - 2336.3965) + if abs(val - 2336.3965) > 1e-4: + raise AssertionError + + x0, x1, y0, y1 = rio.bounds + + x0 += 1000 + y0 += 1000 + x1 -= 1000 + y1 -= 1000 + shape = np.array([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) + + data = rio.sample_polygon(shape, band=rio.bands[0]) + if data.size != 267050: + raise AssertionError + if abs(np.min(data) - 1942.1735) > 1e-4: + raise AssertionError + if (np.max(data) - 2608.557) > 1e-4: + raise AssertionError + + rio.crop(shape) + data = rio.get_array(band=rio.bands[0], masked=True) + if data.size != 267050: + raise AssertionError + if abs(np.min(data) - 1942.1735) > 1e-4: + raise AssertionError + if (np.max(data) - 2608.557) > 1e-4: + raise AssertionError + + data = rio.resample_to_grid( + ml.modelgrid, band=rio.bands[0], method="nearest" + ) + if data.size != 5913: + raise AssertionError + if abs(np.min(data) - 1942.1735) > 1e-4: + raise AssertionError + if abs(np.max(data) - 2605.6204) > 1e-4: + raise AssertionError + + del rio + + +# %% test raster sampling methods + + +@pytest.mark.slow +@requires_pkg("rasterstats") +def test_raster_sampling_methods(example_data_path): + ws = example_data_path / "options" + raster_name = "dem.img" + + rio = Raster.load(ws / "dem" / raster_name) + + ml = Modflow.load("sagehen.nam", version="mfnwt", model_ws=ws / "sagehen") + xoff = 214110 + yoff = 4366620 + ml.modelgrid.set_coord_info(xoff, yoff) + + x0, x1, y0, y1 = rio.bounds + + x0 += 3000 + y0 += 3000 + x1 -= 3000 + y1 -= 3000 + shape = np.array([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) + + rio.crop(shape) + + methods = { + "min": 2088.52343, + "max": 2103.54882, + "mean": 2097.05054, + "median": 2097.36254, + "mode": 2088.52343, + "nearest": 2097.81079, + "linear": 2097.81079, + "cubic": 2097.81079, + } + + for method, value in methods.items(): + data = rio.resample_to_grid( + ml.modelgrid, band=rio.bands[0], method=method + ) + + print(data[30, 37]) + if np.abs(data[30, 37] - value) > 1e-05: + raise AssertionError( + f"{method} resampling returning incorrect values" + ) + + +@requires_pkg("rasterio") +def test_raster_reprojection(example_data_path): + ws = example_data_path / "options" / "dem" + raster_name = "dem.img" + + wgs_epsg = 4326 + wgs_xmin = -120.32116799649168 + wgs_ymax = 39.46620605907534 + + raster = Raster.load(ws / raster_name) + + print(raster.crs.to_epsg()) + wgs_raster = raster.to_crs(crs=f"EPSG:{wgs_epsg}") + + if not wgs_raster.crs.to_epsg() == wgs_epsg: + raise AssertionError(f"Raster not converted to EPSG {wgs_epsg}") + + transform = wgs_raster._meta["transform"] + if not np.isclose(transform.c, wgs_xmin) and not np.isclose( + transform.f, wgs_ymax + ): + raise AssertionError(f"Raster not reprojected to EPSG {wgs_epsg}") + + raster.to_crs(epsg=wgs_epsg, inplace=True) + transform2 = raster._meta["transform"] + for ix, val in enumerate(transform): + if not np.isclose(val, transform2[ix]): + raise AssertionError("In place reprojection not working") + + +@requires_pkg("rasterio") +def test_create_raster_from_array_modelgrid(example_data_path): + ws = example_data_path / "options" / "dem" + raster_name = "dem.img" + + raster = Raster.load(ws / raster_name) + + xsize = 200 + ysize = 100 + xmin, xmax, ymin, ymax = raster.bounds + + nbands = 5 + nlay = 1 + nrow = int(np.floor((ymax - ymin) / ysize)) + ncol = int(np.floor((xmax - xmin) / xsize)) + + delc = np.full((nrow,), ysize) + delr = np.full((ncol,), xsize) + + grid = flopy.discretization.StructuredGrid( + delc=delc, + delr=delr, + top=np.ones((nrow, ncol)), + botm=np.zeros((nlay, nrow, ncol)), + idomain=np.ones((nlay, nrow, ncol), dtype=int), + xoff=xmin, + yoff=ymin, + crs=raster.crs, + ) + + array = np.random.random((grid.ncpl * nbands,)) * 100 + robj = Raster.raster_from_array(array, grid) + + if nbands != len(robj.bands): + raise AssertionError("Number of raster bands is incorrect") + + array = array.reshape((nbands, nrow, ncol)) + for band in robj.bands: + ra = robj.get_array(band) + np.testing.assert_allclose( + array[band - 1], + ra, + err_msg="Array not properly reshaped or converted to raster", + ) + + +@requires_pkg("rasterio", "affine") +def test_create_raster_from_array_transform(example_data_path): + import affine + + ws = example_data_path / "options" / "dem" + raster_name = "dem.img" + + raster = Raster.load(ws / raster_name) + + transform = raster._meta["transform"] + array = raster.get_array(band=raster.bands[0]) + + array = np.expand_dims(array, axis=0) + # same location but shrink raster by factor 2 + new_transform = affine.Affine( + transform.a / 2, 0, transform.c, 0, transform.e / 2, transform.f + ) + + robj = Raster.raster_from_array( + array, crs=raster.crs, transform=new_transform + ) + + rxmin, rxmax, rymin, rymax = robj.bounds + xmin, xmax, ymin, ymax = raster.bounds + + if ( + not ((xmax - xmin) / (rxmax - rxmin)) == 2 + or not ((ymax - ymin) / (rymax - rymin)) == 2 + ): + raise AssertionError("Transform based raster not working properly") diff --git a/etc/environment.yml b/etc/environment.yml index 12163818a0..25cee3edab 100644 --- a/etc/environment.yml +++ b/etc/environment.yml @@ -40,7 +40,7 @@ dependencies: - fiona - descartes - pyproj - - shapely>=1.8 + - shapely>=2.0 - geos - geojson - vtk diff --git a/flopy/utils/gridintersect.py b/flopy/utils/gridintersect.py index eed25d3102..52bdcd1554 100644 --- a/flopy/utils/gridintersect.py +++ b/flopy/utils/gridintersect.py @@ -1,79 +1,26 @@ -import contextlib import warnings -from itertools import product import numpy as np +from pandas import DataFrame from .geometry import transform from .geospatial_utils import GeoSpatialUtil -from .parse_version import Version from .utl_import import import_optional_dependency -NUMPY_GE_121 = Version(np.__version__) >= Version("1.21") - shapely = import_optional_dependency("shapely", errors="silent") -if shapely is not None: - SHAPELY_GE_20 = Version(shapely.__version__) >= Version("2.0a1") - # shapely > 1.8 required - if Version(shapely.__version__) < Version("1.8"): - warnings.warn("GridIntersect requires shapely>=1.8.") - shapely = None - if SHAPELY_GE_20: - from shapely import unary_union - else: - from shapely.ops import unary_union -else: - SHAPELY_GE_20 = False - -shapely_warning = None -if shapely is not None: - try: - from shapely.errors import ShapelyDeprecationWarning as shapely_warning - except ImportError: - pass - -if shapely_warning is not None and not SHAPELY_GE_20: - - @contextlib.contextmanager - def ignore_shapely_warnings_for_object_array(): - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - "Iteration|The array interface|__len__", - shapely_warning, - ) - if NUMPY_GE_121: - # warning from numpy for existing Shapely releases (this is - # fixed with Shapely 1.8) - warnings.filterwarnings( - "ignore", - "An exception was ignored while fetching", - DeprecationWarning, - ) - yield - - @contextlib.contextmanager - def ignore_shapely2_strtree_warning(): - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - ( - "STRtree will be changed in 2.0.0 and " - "will not be compatible with versions < 2." - ), - shapely_warning, - ) - yield - -else: - @contextlib.contextmanager - def ignore_shapely_warnings_for_object_array(): - yield - - @contextlib.contextmanager - def ignore_shapely2_strtree_warning(): - yield +# TODO: remove the following methods and classes in version 3.10.0 +# - ModflowGridIndices +# - GridIntersect: +# - remove method kwarg from __init__ +# - remove structured methods from intersect() and intersects() +# - _intersect_point_structured() +# - _intersect_linestring_structured() +# - _get_nodes_intersecting_linestring() +# - _check_adjacent_cells_intersecting_line() +# - _intersect_rectangle_structured() +# - _intersect_polygon_structured() +# - _transform_geo_interface_polygon() def parse_shapely_ix_result(collection, ix_result, shptyps=None): @@ -121,9 +68,7 @@ def parse_shapely_ix_result(collection, ix_result, shptyps=None): class GridIntersect: - """Class for intersecting shapely geometries (Point, Linestring, Polygon, - or their Multi variants) with MODFLOW grids. Contains optimized search - routines for structured grids. + """Class for intersecting shapely geometries with MODFLOW grids. Notes ----- @@ -135,11 +80,6 @@ class GridIntersect: with the whole collection at once. - Building the STR-tree can take a while for large grids. Once built the intersect routines (for individual shapes) should be pretty fast. - - The optimized routines for structured grids can outperform the shapely - routines for point and linestring intersections because of the reduced - overhead of building and parsing the STR-tree. However, for polygons - the STR-tree implementation is often faster than the optimized - structured routines, especially for larger grids. """ def __init__(self, mfgrid, method=None, rtree=True, local=False): @@ -150,27 +90,45 @@ def __init__(self, mfgrid, method=None, rtree=True, local=False): mfgrid : flopy modflowgrid MODFLOW grid as implemented in flopy method : str, optional - Options are either 'vertex' which uses shapely intersection operations - or 'structured' which uses optimized methods that only work for structured - grids. The default is None, which determines intersection method based on - the grid type. + Method to use for intersection shapes with the grid. Method 'vertex' + will be the only option in the future. Method 'structured' is deprecated. + This keyword argument will be removed in a future release. + + .. deprecated:: 3.9.0 + method="vertex" will be the only option from 3.10.0 + rtree : bool, optional whether to build an STR-Tree, default is True. If False no STR-tree is built, but intersects will loop through all model gridcells - (which is generally slower). Only read when `method='vertex'`. + (which is generally slower). local : bool, optional use local model coordinates from model grid to build grid geometries, - default is False and uses real-world coordinates (with offset and rotation), - if specified. + default is False and uses real-world coordinates (with offset and rotation). """ + import_optional_dependency( + "shapely", error_message="GridIntersect requires shapely" + ) self.mfgrid = mfgrid self.local = local + # TODO: remove method kwarg in version v3.10.0 + # keep default behavior for v3.9.0, but warn if method is not vertex + # allow silencing of warning with method="vertex" in v3.9.0 if method is None: # determine method from grid_type self.method = self.mfgrid.grid_type else: # set method self.method = method + if self.method != "vertex": + warnings.warn( + ( + 'Note `method="structured"` is deprecated. ' + 'Pass `method="vertex"` to silence this warning. ' + "This will be the new default in a future release and this " + "keyword argument will be removed." + ), + category=DeprecationWarning, + ) self.rtree = rtree # really only necessary for method=='vertex' as structured methods @@ -188,8 +146,7 @@ def __init__(self, mfgrid, method=None, rtree=True, local=False): "shapely.strtree", error_message="STRTree requires shapely", ) - with ignore_shapely2_strtree_warning(): - self.strtree = strtree.STRtree(self.geoms) + self.strtree = strtree.STRtree(self.geoms) elif self.method == "structured" and mfgrid.grid_type == "structured": # geoms and cellids do not need to be assigned for structured @@ -212,7 +169,8 @@ def intersect( return_all_intersections=False, contains_centroid=False, min_area_fraction=None, - shapely2=True, + geo_dataframe=False, + shapely2=None, ): """Method to intersect a shape with a model grid. @@ -244,16 +202,20 @@ def intersect( float defining minimum intersection area threshold, if intersection area is smaller than min_frac_area * cell_area, do not store intersection result, only used if shape type is "polygon" - shapely2 : bool, optional - temporary flag to determine whether to use methods optimized for - shapely 2.0. Useful for comparison performance between the old - (shapely 1.8) and new (shapely 2.0) implementations. + geo_dataframe : bool, optional + if True, return a geopandas GeoDataFrame, default is False Returns ------- - numpy.recarray - a record array containing information about the intersection + numpy.recarray or gepandas.GeoDataFrame + a record array containing information about the intersection or + a geopandas.GeoDataFrame if geo_dataframe=True """ + if shapely2 is not None: + warnings.warn( + "The shapely2 keyword argument is deprecated. " + "Shapely<2 support was dropped in flopy version 3.9.0." + ) gu = GeoSpatialUtil(shp, shapetype=shapetype) shp = gu.shapely @@ -266,18 +228,11 @@ def intersect( shp, return_all_intersections=return_all_intersections ) else: - if SHAPELY_GE_20 and shapely2: - rec = self._intersect_point_shapely2( - shp, - sort_by_cellid=sort_by_cellid, - return_all_intersections=return_all_intersections, - ) - else: - rec = self._intersect_point_shapely( - shp, - sort_by_cellid=sort_by_cellid, - return_all_intersections=return_all_intersections, - ) + rec = self._intersect_point_shapely( + shp, + sort_by_cellid=sort_by_cellid, + return_all_intersections=return_all_intersections, + ) elif gu.shapetype in ("LineString", "MultiLineString"): if ( self.method == "structured" @@ -289,20 +244,12 @@ def intersect( return_all_intersections=return_all_intersections, ) else: - if SHAPELY_GE_20 and shapely2: - rec = self._intersect_linestring_shapely2( - shp, - keepzerolengths, - sort_by_cellid=sort_by_cellid, - return_all_intersections=return_all_intersections, - ) - else: - rec = self._intersect_linestring_shapely( - shp, - keepzerolengths, - sort_by_cellid=sort_by_cellid, - return_all_intersections=return_all_intersections, - ) + rec = self._intersect_linestring_shapely( + shp, + keepzerolengths, + sort_by_cellid=sort_by_cellid, + return_all_intersections=return_all_intersections, + ) elif gu.shapetype in ("Polygon", "MultiPolygon"): if ( self.method == "structured" @@ -314,23 +261,26 @@ def intersect( min_area_fraction=min_area_fraction, ) else: - if SHAPELY_GE_20 and shapely2: - rec = self._intersect_polygon_shapely2( - shp, - sort_by_cellid=sort_by_cellid, - contains_centroid=contains_centroid, - min_area_fraction=min_area_fraction, - ) - else: - rec = self._intersect_polygon_shapely( - shp, - sort_by_cellid=sort_by_cellid, - contains_centroid=contains_centroid, - min_area_fraction=min_area_fraction, - ) + rec = self._intersect_polygon_shapely( + shp, + sort_by_cellid=sort_by_cellid, + contains_centroid=contains_centroid, + min_area_fraction=min_area_fraction, + ) else: raise TypeError(f"Shapetype {gu.shapetype} is not supported") + if geo_dataframe: + gpd = import_optional_dependency("geopandas") + gdf = ( + gpd.GeoDataFrame(rec) + .rename(columns={"ixshapes": "geometry"}) + .set_geometry("geometry") + ) + if self.mfgrid.crs is not None: + gdf = gdf.set_crs(self.mfgrid.crs) + return gdf + return rec def _set_method_get_gridshapes(self): @@ -386,22 +336,14 @@ def _rect_grid_to_geoms_cellids(self): ] ).transpose((1, 2, 0)) - if SHAPELY_GE_20: - # use array-based methods for speed - geoms = shapely.polygons( - shapely.linearrings( - xverts.flatten(), - y=yverts.flatten(), - indices=np.repeat(cellids, 4), - ) + # use array-based methods for speed + geoms = shapely.polygons( + shapely.linearrings( + xverts.flatten(), + y=yverts.flatten(), + indices=np.repeat(cellids, 4), ) - else: - from shapely.geometry import Polygon - - geoms = [] - for i, j in product(range(nrow), range(ncol)): - geoms.append(Polygon(zip(xverts[i, j], yverts[i, j]))) - geoms = np.array(geoms) + ) return geoms, cellids @@ -452,42 +394,6 @@ def _vtx_grid_to_geoms_cellids(self): ] return np.array(geoms), np.arange(self.mfgrid.ncpl) - def _rect_grid_to_shape_list(self): - """internal method, list of shapely polygons for structured grid cells. - - .. deprecated:: 3.3.6 - use _rect_grid_to_geoms_cellids() instead. - - Returns - ------- - list - list of shapely Polygons - """ - warnings.warn( - "`_rect_grid_to_shape_list()` is deprecated, please" - "use `_rect_grid_to_geoms_cellids()` instead.", - DeprecationWarning, - ) - return self._rect_grid_to_geoms_cellids()[0].tolist() - - def _vtx_grid_to_shape_list(self): - """internal method, list of shapely polygons for vertex grids. - - .. deprecated:: 3.3.6 - use _vtx_grid_to_geoms_cellids() instead. - - Returns - ------- - list - list of shapely Polygons - """ - warnings.warn( - "`_vtx_grid_to_shape_list()` is deprecated, please" - "use `_vtx_grid_to_geoms_cellids()` instead.", - DeprecationWarning, - ) - return self._vtx_grid_to_geoms_cellids()[0].tolist() - def query_grid(self, shp): """Perform spatial query on grid with shapely geometry. If no spatial query is possible returns all grid cells. @@ -503,10 +409,7 @@ def query_grid(self, shp): array containing cellids of grid cells in query result """ if self.rtree: - if SHAPELY_GE_20: - result = self.strtree.query(shp) - else: - result = np.array(self.strtree.query_items(shp)) + result = self.strtree.query(shp) else: # no spatial query result = self.cellids @@ -533,343 +436,12 @@ def filter_query_result(self, cellids, shp): filter or generator containing polygons that intersect with shape """ # get only gridcells that intersect - if SHAPELY_GE_20: - if not shapely.is_prepared(shp): - shapely.prepare(shp) - qcellids = cellids[shapely.intersects(self.geoms[cellids], shp)] - else: - # prepare shape for efficient batch intersection check - prepared = import_optional_dependency("shapely.prepared") - prepshp = prepared.prep(shp) - qfiltered = filter( - lambda tup: prepshp.intersects(tup[0]), - zip(self.geoms[cellids], cellids), - ) - try: - _, qcellids = zip(*qfiltered) - qcellids = np.array(qcellids) - except ValueError: - # catch empty filter result (i.e. when rtree=False) - qcellids = np.empty(0, dtype=int) + if not shapely.is_prepared(shp): + shapely.prepare(shp) + qcellids = cellids[shapely.intersects(self.geoms[cellids], shp)] return qcellids - @staticmethod - def sort_gridshapes(geoms, cellids): - """Sort geometries (from i.e. query result) by cell id. - - .. deprecated:: 3.3.6 - sorting is now performed on cellids. - - Parameters - ---------- - geoms : iterable - list or iterable of geometries - - Returns - ------- - list - sorted list of gridcells - """ - warnings.warn( - "`sort_gridshapes()` is deprecated, sort cellids" - " and use that to select geometries, i.e. " - "`GridIntersect.geoms[sorted_cellids]`.", - DeprecationWarning, - ) - return [ - igeom - for _, igeom in sorted( - zip(cellids, geoms), key=lambda pair: pair[0] - ) - ] - def _intersect_point_shapely( - self, shp, sort_by_cellid=True, return_all_intersections=False - ): - """intersect grid with Point or MultiPoint. - - Parameters - ---------- - shp : Point or MultiPoint - - shapely Point or MultiPoint to intersect with grid. Note, it is - generally faster to loop over a MultiPoint and intersect per point - than to intersect a MultiPoint directly. - sort_by_cellid : bool, optional - flag whether to sort cells by id, used to ensure node with lowest - id is returned, by default True - return_all_intersections : bool, optional - if True, return multiple intersection results for points on grid - cell boundaries (e.g. returns 2 intersection results if a point - lies on the boundary between two grid cells). The default is - False, which will return a single intersection result for boundary - cases. - - Returns - ------- - numpy.recarray - a record array containing information about the intersection - """ - shapely_geo = import_optional_dependency("shapely.geometry") - - # query grid - qcellids = self.query_grid(shp) # returns cellids - if len(qcellids) > 0: - qfiltered = self.filter_query_result(qcellids, shp) - else: - # query result is empty - qfiltered = qcellids - # sort cells to ensure lowest cell ids are returned - if sort_by_cellid: - qfiltered.sort() - - isectshp = [] - cellids = [] - vertices = [] - parsed_points = [] # for keeping track of points - - # loop over cells returned by filtered spatial query - for cid in qfiltered: - r = self.geoms[cid] - # do intersection - intersect = shp.intersection(r) - # parse result per Point - collection = parse_shapely_ix_result( - [], intersect, shptyps=["Point"] - ) - # loop over intersection result and store information - cell_verts = [] - cell_shps = [] - for c in collection: - verts = c.__geo_interface__["coordinates"] - # avoid returning multiple cells for points on boundaries - # if return_all_intersections is False - if not return_all_intersections: - if verts in parsed_points: - continue - parsed_points.append(verts) - cell_shps.append(c) # collect points - cell_verts.append(verts) - # if any new ix found - if len(cell_shps) > 0: - # combine new points in MultiPoint - isectshp.append( - shapely_geo.MultiPoint(cell_shps) - if len(cell_shps) > 1 - else cell_shps[0] - ) - vertices.append(tuple(cell_verts)) - # if structured calculated (i, j) cell address - if self.mfgrid.grid_type == "structured": - cid = self.mfgrid.get_lrc([cid])[0][1:] - cellids.append(cid) - - rec = np.recarray( - len(isectshp), - names=["cellids", "vertices", "ixshapes"], - formats=["O", "O", "O"], - ) - with ignore_shapely_warnings_for_object_array(): - rec.ixshapes = isectshp - rec.vertices = vertices - rec.cellids = cellids - - return rec - - def _intersect_linestring_shapely( - self, - shp, - keepzerolengths=False, - sort_by_cellid=True, - return_all_intersections=False, - ): - """intersect with LineString or MultiLineString. - - Parameters - ---------- - shp : shapely.geometry.LineString or MultiLineString - LineString to intersect with the grid - keepzerolengths : bool, optional - keep linestrings with length zero, default is False - sort_by_cellid : bool, optional - flag whether to sort cells by id, used to ensure node - with lowest id is returned, by default True - return_all_intersections : bool, optional - if True, return multiple intersection results for linestrings on - grid cell boundaries (e.g. returns 2 intersection results if a - linestring lies on the boundary between two grid cells). The - default is False, which will return a single intersection result - for boundary cases. - - Returns - ------- - numpy.recarray - a record array containing information about the intersection - """ - # query grid - qcellids = self.query_grid(shp) - if len(qcellids) > 0: - # filter result further if possible (only strtree and filter methods) - qfiltered = self.filter_query_result(qcellids, shp) - else: - # query result is empty - qfiltered = qcellids - # sort cells to ensure lowest cell ids are returned - if sort_by_cellid: - qfiltered.sort() - - # initialize empty lists for storing results - isectshp = [] - cellids = [] - vertices = [] - vertices_check = [] - lengths = [] - - # loop over cells returned by filtered spatial query - for cid in qfiltered: - r = self.geoms[cid] - # do intersection - intersect = shp.intersection(r) - # parse result - collection = parse_shapely_ix_result( - [], intersect, shptyps=["LineString", "MultiLineString"] - ) - # loop over intersection result and store information - for c in collection: - verts = c.__geo_interface__["coordinates"] - # test if linestring was already processed (if on boundary), - # ignore if return_all_intersections is True - if not return_all_intersections: - if verts in vertices_check: - continue - # if keep zero don't check length - if not keepzerolengths: - if c.length == 0.0: - continue - isectshp.append(c) - lengths.append(c.length) - vertices.append(verts) - # unpack mutlilinestring for checking if linestring already parsed - if c.geom_type.startswith("Multi"): - vertices_check += [iv for iv in verts] - else: - vertices_check.append(verts) - # if structured calculate (i, j) cell address - if self.mfgrid.grid_type == "structured": - cid = self.mfgrid.get_lrc([cid])[0][1:] - cellids.append(cid) - - rec = np.recarray( - len(isectshp), - names=["cellids", "vertices", "lengths", "ixshapes"], - formats=["O", "O", "f8", "O"], - ) - with ignore_shapely_warnings_for_object_array(): - rec.ixshapes = isectshp - rec.vertices = vertices - rec.lengths = lengths - rec.cellids = cellids - - return rec - - def _intersect_polygon_shapely( - self, - shp, - sort_by_cellid=True, - contains_centroid=False, - min_area_fraction=None, - ): - """intersect with Polygon or MultiPolygon. - - Parameters - ---------- - shp : shapely.geometry.Polygon or MultiPolygon - shape to intersect with the grid - sort_by_cellid : bool, optional - flag whether to sort cells by id, used to ensure node - with lowest id is returned, by default True - contains_centroid : bool, optional - if True, only store intersection result if cell centroid is - contained within intersection shape - min_area_fraction : float, optional - float defining minimum intersection area threshold, if - intersection area is smaller than min_frac_area * cell_area, do - not store intersection result - - Returns - ------- - numpy.recarray - a record array containing information about the intersection - """ - shapely_geo = import_optional_dependency("shapely.geometry") - - # query grid - qcellids = self.query_grid(shp) - if len(qcellids) > 0: - # filter result further if possible (only strtree and filter methods) - qfiltered = self.filter_query_result(qcellids, shp) - else: - # query result is empty - qfiltered = qcellids - # sort cells to ensure lowest cell ids are returned - if sort_by_cellid: - qfiltered.sort() - - isectshp = [] - cellids = [] - vertices = [] - areas = [] - - # loop over cells returned by filtered spatial query - for cid in qfiltered: - r = self.geoms[cid] - # do intersection - intersect = shp.intersection(r) - # parse result - collection = parse_shapely_ix_result( - [], intersect, shptyps=["Polygon", "MultiPolygon"] - ) - if len(collection) > 1: - collection = [shapely_geo.MultiPolygon(collection)] - # loop over intersection result and store information - for c in collection: - # don't store intersections with 0 area - if c.area == 0.0: - continue - # option: only store result if cell centroid is contained - # within intersection result - if contains_centroid: - if not c.intersects(r.centroid): - continue - # option: min_area_fraction, only store if intersected area - # is larger than fraction * cell_area - if min_area_fraction: - if c.area < (min_area_fraction * r.area): - continue - - verts = c.__geo_interface__["coordinates"] - isectshp.append(c) - areas.append(c.area) - vertices.append(verts) - # if structured calculate (i, j) cell address - if self.mfgrid.grid_type == "structured": - cid = self.mfgrid.get_lrc([cid])[0][1:] - cellids.append(cid) - - rec = np.recarray( - len(isectshp), - names=["cellids", "vertices", "areas", "ixshapes"], - formats=["O", "O", "f8", "O"], - ) - with ignore_shapely_warnings_for_object_array(): - rec.ixshapes = isectshp - rec.vertices = vertices - rec.areas = areas - rec.cellids = cellids - - return rec - - def _intersect_point_shapely2( self, shp, sort_by_cellid=True, @@ -927,7 +499,7 @@ def _intersect_point_shapely2( return rec - def _intersect_linestring_shapely2( + def _intersect_linestring_shapely( self, shp, keepzerolengths=False, @@ -954,17 +526,26 @@ def _intersect_linestring_shapely2( mask_empty = shapely.is_empty(ixresult) # keep only Linestring and MultiLineString geomtype_ids = shapely.get_type_id(ixresult) - mask_type = np.isin(geomtype_ids, [1, 5, 7]) + all_ids = [ + shapely.GeometryType.LINESTRING, + shapely.GeometryType.MULTILINESTRING, + shapely.GeometryType.GEOMETRYCOLLECTION, + ] + line_ids = [ + shapely.GeometryType.LINESTRING, + shapely.GeometryType.MULTILINESTRING, + ] + mask_type = np.isin(geomtype_ids, all_ids) ixresult = ixresult[~mask_empty & mask_type] qcellids = qcellids[~mask_empty & mask_type] # parse geometry collections (i.e. when part of linestring touches a cell edge, # resulting in a point intersection result) - if 7 in geomtype_ids: + if shapely.GeometryType.GEOMETRYCOLLECTION in geomtype_ids: def parse_linestrings_in_geom_collection(gc): parts = shapely.get_parts(gc) - parts = parts[np.isin(shapely.get_type_id(parts), [1, 5])] + parts = parts[np.isin(shapely.get_type_id(parts), line_ids)] if len(parts) > 1: p = shapely.multilinestring(parts) elif len(parts) == 0: @@ -973,7 +554,10 @@ def parse_linestrings_in_geom_collection(gc): p = parts[0] return p - mask_gc = geomtype_ids[~mask_empty & mask_type] == 7 + mask_gc = ( + geomtype_ids[~mask_empty & mask_type] + == shapely.GeometryType.GEOMETRYCOLLECTION + ) ixresult[mask_gc] = np.apply_along_axis( parse_linestrings_in_geom_collection, axis=0, @@ -986,7 +570,7 @@ def parse_linestrings_in_geom_collection(gc): shp, shapely.get_exterior_ring(self.geoms[qcellids]) ) mask_bnds_empty = shapely.is_empty(ixbounds) - mask_bnds_type = np.isin(shapely.get_type_id(ixbounds), [1, 5]) + mask_bnds_type = np.isin(shapely.get_type_id(ixbounds), line_ids) # get ids of boundary intersections idxs = np.nonzero(~mask_bnds_empty & mask_bnds_type)[0] @@ -1002,7 +586,7 @@ def parse_linestrings_in_geom_collection(gc): mask_bnds_empty = shapely.is_empty( isect ) # select boundary ix result - mask_overlap = np.isin(shapely.get_type_id(isect), [1, 5]) + mask_overlap = np.isin(shapely.get_type_id(isect), line_ids) # calculate difference between self and overlapping result diff = shapely.difference( @@ -1034,7 +618,7 @@ def parse_linestrings_in_geom_collection(gc): return rec - def _intersect_polygon_shapely2( + def _intersect_polygon_shapely( self, shp, sort_by_cellid=True, @@ -1113,7 +697,7 @@ def parse_polygons_in_geom_collection(gc): return rec - def intersects(self, shp, shapetype=None): + def intersects(self, shp, shapetype=None, dataframe=False): """Return cellids for grid cells that intersect with shape. Parameters @@ -1125,26 +709,17 @@ def intersects(self, shp, shapetype=None): type of shape (i.e. "point", "linestring", "polygon" or their multi-variants), used by GeoSpatialUtil if shp is passed as a list of vertices, default is None + dataframe : bool, optional + if True, return a pandas.DataFrame, default is False Returns ------- - numpy.recarray - a record array containing cell IDs of the gridcells - the shape intersects with + numpy.recarray or pandas.DataFrame + a record array or pandas.DataFrame containing cell IDs of the gridcells + the shape intersects with. """ shp = GeoSpatialUtil(shp, shapetype=shapetype).shapely - - if SHAPELY_GE_20: - qfiltered = self.strtree.query(shp, predicate="intersects") - else: - # query grid - qcellids = self.query_grid(shp) - if len(qcellids) > 0: - # filter result further if possible (only strtree and filter methods) - qfiltered = self.filter_query_result(qcellids, shp) - else: - # query result is empty - qfiltered = qcellids + qfiltered = self.strtree.query(shp, predicate="intersects") # build rec-array rec = np.recarray(len(qfiltered), names=["cellids"], formats=["O"]) @@ -1152,11 +727,17 @@ def intersects(self, shp, shapetype=None): rec.cellids = list(zip(*self.mfgrid.get_lrc([qfiltered])[0][1:])) else: rec.cellids = qfiltered + + if dataframe: + return DataFrame(rec) return rec def _intersect_point_structured(self, shp, return_all_intersections=False): """intersection method for intersecting points with structured grids. + .. deprecated:: 3.9.0 + use _intersect_point_shapely() or set method="vertex" in GridIntersect. + Parameters ---------- shp : shapely.geometry.Point or MultiPoint @@ -1285,8 +866,7 @@ def _intersect_point_structured(self, shp, return_all_intersections=False): len(nodelist), names=["cellids", "ixshapes"], formats=["O", "O"] ) rec.cellids = nodelist - with ignore_shapely_warnings_for_object_array(): - rec.ixshapes = ixshapes + rec.ixshapes = ixshapes return rec def _intersect_linestring_structured( @@ -1294,6 +874,9 @@ def _intersect_linestring_structured( ): """method for intersecting linestrings with structured grids. + .. deprecated:: 3.9.0 + use _intersect_point_shapely() or set method="vertex" in GridIntersect. + Parameters ---------- shp : shapely.geometry.Linestring or MultiLineString @@ -1314,6 +897,7 @@ def _intersect_linestring_structured( numpy.recarray a record array containing information about the intersection """ + shapely = import_optional_dependency("shapely") shapely_geo = import_optional_dependency("shapely.geometry") affinity_loc = import_optional_dependency("shapely.affinity") @@ -1478,7 +1062,7 @@ def _intersect_linestring_structured( tempverts.append(vertices[i]) ishp = ixshapes[i] if isinstance(ishp, list): - ishp = unary_union(ishp) + ishp = shapely.unary_union(ishp) tempshapes.append(ishp) nodelist = tempnodes lengths = templengths @@ -1493,8 +1077,7 @@ def _intersect_linestring_structured( rec.vertices = vertices rec.lengths = lengths rec.cellids = nodelist - with ignore_shapely_warnings_for_object_array(): - rec.ixshapes = ixshapes + rec.ixshapes = ixshapes return rec @@ -1505,6 +1088,9 @@ def _get_nodes_intersecting_linestring( and return a list of node indices and the length of the line in that node. + .. deprecated:: 3.9.0 + method="structured" is deprecated. + Parameters ---------- linestring: shapely.geometry.LineString or MultiLineString @@ -1610,6 +1196,9 @@ def _check_adjacent_cells_intersecting_line( ): """helper method that follows a line through a structured grid. + .. deprecated:: 3.9.0 + method="structured" is deprecated. + Parameters ---------- linestring : shapely.geometry.LineString @@ -1782,6 +1371,9 @@ def _intersect_rectangle_structured(self, rectangle): """intersect a rectangle with a structured grid to retrieve node ids of intersecting grid cells. + .. deprecated:: 3.9.0 + method="structured" is deprecated. + Note: only works in local coordinates (i.e. non-rotated grid with origin at (0, 0)) @@ -1867,6 +1459,10 @@ def _intersect_polygon_structured( """intersect polygon with a structured grid. Uses bounding box of the Polygon to limit search space. + .. deprecated:: 3.9.0 + method="structured" is deprecated. Use `_intersect_polygon_shapely()`. + + Notes ----- If performance is slow, try setting the method to 'vertex' @@ -2004,8 +1600,7 @@ def _intersect_polygon_structured( rec.vertices = vertices rec.areas = areas rec.cellids = nodelist - with ignore_shapely_warnings_for_object_array(): - rec.ixshapes = ixshapes + rec.ixshapes = ixshapes return rec @@ -2013,6 +1608,10 @@ def _transform_geo_interface_polygon(self, polygon): """Internal method, helper function to transform geometry __geo_interface__. + .. deprecated:: 3.9.0 + method="structured" is deprecated. Only used by + `_intersect_polygon_structured()` + Used for translating intersection result coordinates back into real-world coordinates. @@ -2076,17 +1675,16 @@ def _transform_geo_interface_polygon(self, polygon): return geom_list @staticmethod - def plot_polygon(rec, ax=None, **kwargs): + def plot_polygon(result, ax=None, **kwargs): """method to plot the polygon intersection results from the resulting numpy.recarray. - Note: only works when recarray has 'intersects' column! + Note: only works when recarray has 'ixshapes' column! Parameters ---------- - rec : numpy.recarray - record array containing intersection results - (the resulting shapes) + result : numpy.recarray or geopandas.GeoDataFrame + record array or GeoDataFrame containing intersection results ax : matplotlib.pyplot.axes, optional axes to plot onto, if not provided, creates a new figure **kwargs: @@ -2103,6 +1701,10 @@ def plot_polygon(rec, ax=None, **kwargs): if ax is None: _, ax = plt.subplots() + ax.set_aspect("equal", adjustable="box") + autoscale = True + else: + autoscale = False patches = [] if "facecolor" in kwargs: @@ -2117,7 +1719,13 @@ def add_poly_patch(poly): ppi = _polygon_patch(poly, facecolor=fc, **kwargs) patches.append(ppi) - for i, ishp in enumerate(rec.ixshapes): + # allow for result to be geodataframe + geoms = ( + result.ixshapes + if isinstance(result, np.rec.recarray) + else result.geometry + ) + for i, ishp in enumerate(geoms): if hasattr(ishp, "geoms"): for geom in ishp.geoms: add_poly_patch(geom) @@ -2127,20 +1735,22 @@ def add_poly_patch(poly): pc = PatchCollection(patches, match_original=True) ax.add_collection(pc) + if autoscale: + ax.autoscale_view() + return ax @staticmethod - def plot_linestring(rec, ax=None, cmap=None, **kwargs): + def plot_linestring(result, ax=None, cmap=None, **kwargs): """method to plot the linestring intersection results from the resulting numpy.recarray. - Note: only works when recarray has 'intersects' column! + Note: only works when recarray has 'ixshapes' column! Parameters ---------- - rec : numpy.recarray - record array containing intersection results - (the resulting shapes) + result : numpy.recarray or geopandas.GeoDataFrame + record array or GeoDataFrame containing intersection results ax : matplotlib.pyplot.axes, optional axes to plot onto, if not provided, creates a new figure cmap : str @@ -2157,6 +1767,7 @@ def plot_linestring(rec, ax=None, cmap=None, **kwargs): if ax is None: _, ax = plt.subplots() + ax.set_aspect("equal", adjustable="box") specified_color = True if "c" in kwargs: @@ -2168,9 +1779,15 @@ def plot_linestring(rec, ax=None, cmap=None, **kwargs): if cmap is not None: colormap = plt.get_cmap(cmap) - colors = colormap(np.linspace(0, 1, rec.shape[0])) + colors = colormap(np.linspace(0, 1, result.shape[0])) - for i, ishp in enumerate(rec.ixshapes): + # allow for result to be geodataframe + geoms = ( + result.ixshapes + if isinstance(result, np.rec.recarray) + else result.geometry + ) + for i, ishp in enumerate(geoms): if not specified_color: if cmap is None: c = f"C{i % 10}" @@ -2185,16 +1802,16 @@ def plot_linestring(rec, ax=None, cmap=None, **kwargs): return ax @staticmethod - def plot_point(rec, ax=None, **kwargs): + def plot_point(result, ax=None, **kwargs): """method to plot the point intersection results from the resulting numpy.recarray. - Note: only works when recarray has 'intersects' column! + Note: only works when recarray has 'ixshapes' column! Parameters ---------- - rec : numpy.recarray - record array containing intersection results + result : numpy.recarray or geopandas.GeoDataFrame + record array or GeoDataFrame containing intersection results ax : matplotlib.pyplot.axes, optional axes to plot onto, if not provided, creates a new figure **kwargs: @@ -2213,7 +1830,13 @@ def plot_point(rec, ax=None, **kwargs): _, ax = plt.subplots() x, y = [], [] - geo_coll = shapely_geo.GeometryCollection(list(rec.ixshapes)) + # allow for result to be geodataframe + geoms = ( + result.ixshapes + if isinstance(result, np.rec.recarray) + else result.geometry + ) + geo_coll = shapely_geo.GeometryCollection(list(geoms)) collection = parse_shapely_ix_result([], geo_coll, ["Point"]) for c in collection: x.append(c.x) @@ -2222,10 +1845,64 @@ def plot_point(rec, ax=None, **kwargs): return ax + def plot_intersection_result( + self, result, plot_grid=True, ax=None, **kwargs + ): + """Plot intersection result. + + Parameters + ---------- + result : numpy.rec.recarray or geopandas.GeoDataFrame + result of intersect() + plot_grid : bool, optional + plot model grid, by default True + ax : matplotlib.Axes, optional + axes to plot on, by default None which creates a new axis + + Returns + ------- + ax : matplotlib.Axes + returns axes handle + """ + shapely = import_optional_dependency("shapely") + + if plot_grid: + self.mfgrid.plot(ax=ax) + + geoms = ( + result["ixshapes"] + if isinstance(result, np.rec.recarray) + else result["geometry"] + ) + if np.isin( + shapely.get_type_id(geoms), + [shapely.GeometryType.POINT, shapely.GeometryType.MULTIPOINT], + ).all(): + ax = GridIntersect.plot_point(result, ax=ax, **kwargs) + elif np.isin( + shapely.get_type_id(geoms), + [ + shapely.GeometryType.LINESTRING, + shapely.GeometryType.MULTILINESTRING, + ], + ).all(): + ax = GridIntersect.plot_linestring(result, ax=ax, **kwargs) + elif np.isin( + shapely.get_type_id(geoms), + [shapely.GeometryType.POLYGON, shapely.GeometryType.MULTIPOLYGON], + ).all(): + ax = GridIntersect.plot_polygon(result, ax=ax, **kwargs) + + return ax + class ModflowGridIndices: """Collection of methods that can be used to find cell indices for a - structured, but irregularly spaced MODFLOW grid.""" + structured, but irregularly spaced MODFLOW grid. + + .. deprecated:: 3.9.0 + This class is deprecated and will be removed in version 3.10.0. + """ @staticmethod def find_position_in_array(arr, x): diff --git a/pyproject.toml b/pyproject.toml index 4d1ae3f9cd..76dab29998 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ optional = [ "rasterio", "rasterstats", "scipy", - "shapely >=1.8", + "shapely >=2.0", "vtk ; python_version <'3.13'", "xmipy", ]