Skip to content

Commit a7779f7

Browse files
authored
Merge pull request #678 from martinfleis/voronoi
ENH: geometry agnostic Voronoi based on shapely
2 parents 24f2e72 + 018f1e2 commit a7779f7

File tree

6 files changed

+510
-103
lines changed

6 files changed

+510
-103
lines changed

libpysal/cg/tests/test_voronoi.py

Lines changed: 195 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import geopandas as gpd
12
import numpy as np
3+
import pytest
4+
import shapely
5+
from geopandas.testing import assert_geoseries_equal
6+
from packaging.version import Version
27

3-
from ..shapes import Polygon, asShape
48
from ..voronoi import voronoi, voronoi_frames
59

610

711
class TestVoronoi:
812
def setup_method(self):
913
self.points = [(10.2, 5.1), (4.7, 2.2), (5.3, 5.7), (2.7, 5.3)]
14+
self.points2 = [(10, 5), (4, 2), (5, 5)]
1015

1116
self.vertices = [
1217
[4.21783295711061, 4.084085778781038],
@@ -21,13 +26,197 @@ def setup_method(self):
2126
[-9.226913414477298, -4.58994413837245],
2227
]
2328

29+
p1 = shapely.Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])
30+
p2 = shapely.Polygon([(0, 1), (0, 2), (1, 2), (1, 1)])
31+
p3 = shapely.Polygon([(1, 1), (1, 2), (2, 2), (2, 1)])
32+
p4 = shapely.Polygon([(1, 0), (1, 1), (2, 1), (2, 0)])
33+
self.polygons = gpd.GeoSeries([p1, p2, p3, p4], crs="EPSG:3857")
34+
35+
self.lines = gpd.GeoSeries(
36+
[
37+
shapely.LineString([(0, 0), (0, 1)]),
38+
shapely.LineString([(1, 1), (1, 0)]),
39+
],
40+
crs="EPSG:3857",
41+
)
42+
2443
def test_voronoi(self):
25-
regions, vertices = voronoi(self.points)
44+
with pytest.warns(
45+
FutureWarning, match="The 'voronoi' function is considered private"
46+
):
47+
regions, vertices = voronoi(self.points)
2648
assert regions == [[1, 3, 2], [4, 5, 1, 0], [0, 1, 7, 6], [9, 0, 8]]
2749

2850
np.testing.assert_array_almost_equal(vertices, self.vertices)
2951

30-
def test_voronoi_frames(self):
31-
r_df, p_df = voronoi_frames(self.points)
32-
region = r_df.iloc[0]["geometry"]
33-
assert isinstance(asShape(region), Polygon)
52+
def test_from_array(self):
53+
geoms = voronoi_frames(self.points2, as_gdf=False, return_input=False)
54+
expected = gpd.GeoSeries.from_wkt(
55+
[
56+
"POLYGON ((7.5 2.5, 7.5 5, 10 5, 10 2, 7.75 2, 7.5 2.5))",
57+
"POLYGON ((7.5 2.5, 7.75 2, 4 2, 4 3.66666666, 7.5 2.5))",
58+
"POLYGON ((7.5 2.5, 4 3.66666666, 4 5, 7.5 5, 7.5 2.5))",
59+
],
60+
)
61+
assert_geoseries_equal(
62+
shapely.normalize(geoms),
63+
shapely.normalize(expected),
64+
check_less_precise=True,
65+
)
66+
67+
def test_from_polygons(self):
68+
geoms = voronoi_frames(
69+
self.polygons, as_gdf=False, return_input=False, shrink=0.1
70+
)
71+
expected = gpd.GeoSeries.from_wkt(
72+
[
73+
"POLYGON ((0.5 1, 1 1, 1 0.5, 1 0.1, 0.1 0.1, 0.1 1, 0.5 1))",
74+
"POLYGON ((1 1.5, 1 1, 0.5 1, 0.1 1, 0.1 1.9, 1 1.9, 1 1.5))",
75+
"POLYGON ((1 1.5, 1 1.9, 1.9 1.9, 1.9 1, 1.5 1, 1 1, 1 1.5))",
76+
"POLYGON ((1 0.5, 1 1, 1.5 1, 1.9 1, 1.9 0.1, 1 0.1, 1 0.5))",
77+
],
78+
crs="EPSG:3857",
79+
)
80+
assert_geoseries_equal(
81+
shapely.normalize(geoms),
82+
shapely.normalize(expected),
83+
check_less_precise=True,
84+
)
85+
86+
@pytest.mark.skipif(
87+
Version(gpd.__version__) < Version("0.13.0"),
88+
reason="requires geopandas>=0.13.0",
89+
)
90+
def test_from_lines(self):
91+
geoms = voronoi_frames(
92+
self.lines, as_gdf=False, return_input=False, segment=0.1
93+
)
94+
expected = gpd.GeoSeries.from_wkt(
95+
[
96+
"POLYGON ((0.5 0.95, 0.5 0, 0 0, 0 1, 0.5 0.95))",
97+
"POLYGON ((0.5 0.05, 0.5 1, 1 1, 1 0, 0.5 0.05))",
98+
],
99+
crs="EPSG:3857",
100+
)
101+
assert_geoseries_equal(geoms.simplify(0.1), expected, check_less_precise=True)
102+
103+
@pytest.mark.skipif(
104+
Version(gpd.__version__) >= Version("0.13.0"),
105+
reason="requires geopandas<0.13.0",
106+
)
107+
def test_from_lines_import_error(self):
108+
with pytest.raises(
109+
ImportError,
110+
match="Voronoi tessellation of lines requires geopandas 0.13.0 or later.",
111+
):
112+
voronoi_frames(self.lines, as_gdf=False, return_input=False, segment=0.1)
113+
114+
def test_clip_none(self):
115+
geoms = voronoi_frames(
116+
self.points2, as_gdf=False, return_input=False, clip=None
117+
)
118+
expected = gpd.GeoSeries.from_wkt(
119+
[
120+
"POLYGON ((16 11, 16 -4, 10.75 -4, 7.5 2.5, 7.5 11, 16 11))",
121+
"POLYGON ((-2 -4, -2 5.6666666, 7.5 2.5, 10.75 -4, -2 -4))",
122+
"POLYGON ((-2 11, 7.5 11, 7.5 2.5, -2 5.66666666, -2 11))",
123+
],
124+
)
125+
assert_geoseries_equal(
126+
shapely.normalize(geoms),
127+
shapely.normalize(expected),
128+
check_less_precise=True,
129+
)
130+
131+
def test_clip_chull(self):
132+
geoms = voronoi_frames(
133+
self.points2, as_gdf=False, return_input=False, clip="convex_hull"
134+
)
135+
expected = gpd.GeoSeries.from_wkt(
136+
[
137+
"POLYGON ((7.5 5, 10 5, 7.5 3.75, 7.5 5))",
138+
"POLYGON ((6 3, 4 2, 4.5 3.5, 6 3))",
139+
"POLYGON ((7.5 3.75, 6 3, 4.5 3.5, 5 5, 7.5 5, 7.5 3.75))",
140+
],
141+
)
142+
assert_geoseries_equal(
143+
shapely.normalize(geoms),
144+
shapely.normalize(expected),
145+
check_less_precise=True,
146+
)
147+
148+
def test_clip_ahull(self):
149+
geoms = voronoi_frames(
150+
self.points2, as_gdf=False, return_input=False, clip="alpha_shape"
151+
)
152+
expected = gpd.GeoSeries.from_wkt(
153+
[
154+
"POLYGON ((7.5 5, 10 5, 7.5 3.75, 7.5 5))",
155+
"POLYGON ((6 3, 4 2, 4.5 3.5, 6 3))",
156+
"POLYGON ((7.5 3.75, 6 3, 4.5 3.5, 5 5, 7.5 5, 7.5 3.75))",
157+
],
158+
)
159+
assert_geoseries_equal(
160+
shapely.normalize(geoms),
161+
shapely.normalize(expected),
162+
check_less_precise=True,
163+
)
164+
165+
def test_clip_polygon(self):
166+
geoms = voronoi_frames(
167+
self.points2,
168+
as_gdf=False,
169+
return_input=False,
170+
clip=shapely.box(-10, -10, 10, 10),
171+
)
172+
expected = gpd.GeoSeries.from_wkt(
173+
[
174+
"POLYGON ((7.5 2.5, 7.5 10, 10 10, 10 -2.5, 7.5 2.5))",
175+
"POLYGON ("
176+
"(-10 8.333333, 7.5 2.5, 10 -2.5, 10 -10, -10 -10, -10 8.333333))",
177+
"POLYGON ((7.5 2.5, -10 8.33333333, -10 10, 7.5 10, 7.5 2.5))",
178+
],
179+
)
180+
assert_geoseries_equal(
181+
shapely.normalize(geoms),
182+
shapely.normalize(expected),
183+
check_less_precise=True,
184+
)
185+
186+
def test_clip_error(self):
187+
with pytest.raises(ValueError, match="Clip type 'invalid' not understood."):
188+
voronoi_frames(self.points2, clip="invalid")
189+
190+
def test_as_gdf(self):
191+
geoms, polys = voronoi_frames(self.polygons, as_gdf=True, return_input=True)
192+
assert isinstance(geoms, gpd.GeoDataFrame)
193+
assert isinstance(polys, gpd.GeoDataFrame)
194+
195+
with pytest.warns(
196+
FutureWarning,
197+
match="The 'as_gdf' parameter currently defaults to True but will",
198+
):
199+
voronoi_frames(self.points2, return_input=True)
200+
201+
def test_return_input(self):
202+
geoms, polys = voronoi_frames(self.polygons, return_input=True, as_gdf=False)
203+
assert isinstance(geoms, gpd.GeoSeries)
204+
assert polys is self.polygons
205+
206+
with pytest.warns(
207+
FutureWarning,
208+
match="The 'return_input' parameter currently defaults to True but will",
209+
):
210+
voronoi_frames(self.points2, as_gdf=True)
211+
212+
def test_radius(self):
213+
with pytest.warns(FutureWarning, match="The 'radius' parameter is deprecated"):
214+
voronoi_frames(self.points2, radius=1)
215+
216+
@pytest.mark.parametrize("clip", ["none", "bounds", "chull", "ahull"])
217+
def test_deprecated_clip(self, clip):
218+
with pytest.warns(
219+
FutureWarning,
220+
match=f"The '{clip}' option for the 'clip' parameter is deprecated",
221+
):
222+
voronoi_frames(self.points2, clip=clip)

0 commit comments

Comments
 (0)