Skip to content

Commit e073e99

Browse files
committed
Fix degenerate ring handling and add missing test coverage (#1151)
1 parent 1973f22 commit e073e99

File tree

2 files changed

+90
-9
lines changed

2 files changed

+90
-9
lines changed

xrspatial/polygonize.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,17 +1364,14 @@ def _simplify_polygons(polygon_points, tolerance, method="douglas-peucker"):
13641364
new_rings.append(assembled)
13651365

13661366
# Drop degenerate rings (fewer than 4 vertices = triangle minimum).
1367-
filtered = []
1368-
for ring in new_rings:
1367+
# If the exterior ring is degenerate, drop the entire polygon.
1368+
if len(new_rings) == 0 or len(new_rings[0]) < 4:
1369+
continue
1370+
filtered = [new_rings[0]] # exterior is valid
1371+
for ring in new_rings[1:]:
13691372
if len(ring) >= 4:
13701373
filtered.append(ring)
1371-
elif len(new_rings) > 0 and ring is new_rings[0]:
1372-
# Keep exterior even if degenerate.
1373-
filtered.append(ring)
1374-
if filtered:
1375-
result.append(filtered)
1376-
else:
1377-
result.append(new_rings)
1374+
result.append(filtered)
13781375

13791376
return result
13801377

xrspatial/tests/test_polygonize.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,29 @@ def test_visvalingam_whyatt_two_points(self):
692692
result = _visvalingam_whyatt(coords, 1.0)
693693
assert_allclose(result, coords)
694694

695+
def test_simplify_polygons_drops_degenerate(self):
696+
"""Polygons that collapse below 4 vertices should be dropped."""
697+
from ..polygonize import _simplify_polygons
698+
699+
# A single-pixel polygon (4 unique vertices forming a unit square).
700+
# With a very large tolerance, it should collapse and be dropped.
701+
raster = np.array([
702+
[1, 2, 2],
703+
[2, 2, 2],
704+
[2, 2, 2],
705+
], dtype=np.int64)
706+
data = xr.DataArray(raster)
707+
column, pp = polygonize(data, return_type="numpy")
708+
709+
# Find the small polygon (value 1, single pixel).
710+
small_idx = column.index(1)
711+
small_poly = [pp[small_idx]] # wrap in list for _simplify_polygons
712+
713+
# Large tolerance should collapse the single-pixel polygon.
714+
result = _simplify_polygons(small_poly, tolerance=10.0)
715+
# The polygon should be dropped since its exterior collapses.
716+
assert len(result) == 0 or all(len(r[0]) >= 4 for r in result)
717+
695718

696719
class TestPolygonizeSimplify:
697720
"""Tests for simplify_tolerance and simplify_method parameters."""
@@ -830,6 +853,32 @@ def test_simplify_vw_with_geopandas(self):
830853
assert len(gdf) == 2
831854
assert gdf.geometry.is_valid.all()
832855

856+
def test_simplify_with_spatialpandas(self):
857+
"""Simplification should work with spatialpandas return type."""
858+
pytest.importorskip("spatialpandas")
859+
raster = np.array([
860+
[1, 1, 2, 2],
861+
[1, 1, 2, 2],
862+
[1, 1, 2, 2],
863+
], dtype=np.int64)
864+
data = xr.DataArray(raster)
865+
result = polygonize(data, simplify_tolerance=0.5,
866+
return_type="spatialpandas")
867+
assert len(result) == 2
868+
869+
def test_simplify_with_awkward(self):
870+
"""Simplification should work with awkward return type."""
871+
pytest.importorskip("awkward")
872+
raster = np.array([
873+
[1, 1, 2, 2],
874+
[1, 1, 2, 2],
875+
[1, 1, 2, 2],
876+
], dtype=np.int64)
877+
data = xr.DataArray(raster)
878+
result = polygonize(data, simplify_tolerance=0.5,
879+
return_type="awkward")
880+
assert result is not None
881+
833882

834883
@pytest.mark.skipif(da is None, reason="dask not installed")
835884
class TestPolygonizeSimplifyDask:
@@ -895,3 +944,38 @@ def test_simplify_vw_dask_matches_numpy(self):
895944

896945
for val in areas_np:
897946
assert_allclose(areas_dask[val], areas_np[val], atol=1e-10)
947+
948+
949+
@cuda_and_cupy_available
950+
class TestPolygonizeSimplifyCuPy:
951+
"""Simplification with CuPy backend."""
952+
953+
def test_simplify_cupy_matches_numpy(self):
954+
"""CuPy simplification should produce same areas as numpy."""
955+
import cupy as cp
956+
raster = np.array([
957+
[1, 1, 1, 2, 2, 2],
958+
[1, 1, 2, 2, 2, 2],
959+
[1, 2, 2, 2, 2, 2],
960+
[1, 1, 2, 2, 2, 2],
961+
[1, 1, 1, 2, 2, 2],
962+
], dtype=np.int64)
963+
964+
data_np = xr.DataArray(raster)
965+
data_cp = xr.DataArray(cp.asarray(raster))
966+
967+
col_np, pp_np = polygonize(data_np, simplify_tolerance=1.5)
968+
col_cp, pp_cp = polygonize(data_cp, simplify_tolerance=1.5)
969+
970+
areas_np = {}
971+
for val, rings in zip(col_np, pp_np):
972+
a = sum(calc_boundary_area(r) for r in rings)
973+
areas_np[val] = areas_np.get(val, 0.0) + a
974+
975+
areas_cp = {}
976+
for val, rings in zip(col_cp, pp_cp):
977+
a = sum(calc_boundary_area(r) for r in rings)
978+
areas_cp[val] = areas_cp.get(val, 0.0) + a
979+
980+
for val in areas_np:
981+
assert_allclose(areas_cp[val], areas_np[val], atol=1e-10)

0 commit comments

Comments
 (0)