Describe the bug
contours() can emit zero-length, invalid LineString geometries when raster corner values land exactly on the contour level.
Corners are classified with >= level (xrspatial/contour.py lines 77-84), and _emit_seg (lines 163-209) interpolates edge crossings even when the "crossing" is really an edge or corner exactly equal to the level. When a corner equals the level, the interpolation parameter t lands on the corner, so both endpoints of a segment collapse to the same point. The result is a zero-length segment.
Reproduce
A 4x4 checkerboard of 0 and 1 at level=1.0 produces 8 zero-length LineStrings such as LINESTRING (0 1, 0 1, 0 1). Each has length 0 and is_valid == False in Shapely, and they propagate into the GeoDataFrame from return_type="geopandas".
import numpy as np
from xrspatial.contour import contours
from xrspatial.tests.general_checks import create_test_raster
data = np.array([[0., 1., 0., 1.], [1., 0., 1., 0.], [0., 1., 0., 1.], [1., 0., 1., 0.]])
agg = create_test_raster(data, backend='numpy')
gdf = contours(agg, levels=[1.0], return_type='geopandas')
for g in gdf.geometry:
print(g.is_valid, g.length, g.wkt)
Expected behavior
No zero-length or invalid geometry should reach the output. Degenerate segments (zero-length, or collapsed to a single distinct point) and degenerate polylines should be dropped before stitching and GeoDataFrame output, and equality with the level should be handled the same way every time. This should hold for all four backends (numpy, cupy, dask+numpy, dask+cupy).
Additional context
The numpy, cupy, and dask paths all feed through _emit_seg, and the results become LineStrings in _to_geopandas.
Describe the bug
contours()can emit zero-length, invalid LineString geometries when raster corner values land exactly on the contour level.Corners are classified with
>= level(xrspatial/contour.pylines 77-84), and_emit_seg(lines 163-209) interpolates edge crossings even when the "crossing" is really an edge or corner exactly equal to the level. When a corner equals the level, the interpolation parametertlands on the corner, so both endpoints of a segment collapse to the same point. The result is a zero-length segment.Reproduce
A 4x4 checkerboard of 0 and 1 at
level=1.0produces 8 zero-length LineStrings such asLINESTRING (0 1, 0 1, 0 1). Each has length 0 andis_valid == Falsein Shapely, and they propagate into the GeoDataFrame fromreturn_type="geopandas".Expected behavior
No zero-length or invalid geometry should reach the output. Degenerate segments (zero-length, or collapsed to a single distinct point) and degenerate polylines should be dropped before stitching and GeoDataFrame output, and equality with the level should be handled the same way every time. This should hold for all four backends (numpy, cupy, dask+numpy, dask+cupy).
Additional context
The
numpy,cupy, anddaskpaths all feed through_emit_seg, and the results become LineStrings in_to_geopandas.