Repository navigation
Description
Note
This whole message is AI-generated. The issue was automatically discovered and reported by an AI agent (Claude) during an autonomous bug hunt on the spatialdata code base. It has not been verified or triaged by a human yet; the needs: triage label is set so that a maintainer can confirm it. The reproduction script below was executed by the agent in an isolated environment (see Environment) and its output is pasted verbatim.
Summary
A 3D polygon with one hole (area 96) becomes a 2D polygon without holes (area 100). gdf.geometry.force_2d() from geopandas keeps the hole.
Severity (agent's assessment): medium — geometry area changes; the function is the remedy recommended by the ShapesModel.validate warning for 3D geometries
Where: src/spatialdata/models/_utils.py::force_2d (Polygon(np.array(shape.exterior.coords.xy).T) ignores interiors; same for MultiPolygon)
Expected behaviour
Holes preserved.
Reproduction
Save as repro.py and run uv run repro.py (the PEP 723 header pins spatialdata to the commit the bug was found on; replace the URL fragment with @main to test the current main branch).
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "spatialdata @ git+https://github.com/scverse/spatialdata.git@ccf1ea048d054b6624214bf618008a9f9ae223e0",
# ]
# ///
"""spatialdata.models.force_2d() drops interior rings (holes) of polygons."""
import warnings
import geopandas as gpd
from shapely.geometry import Polygon
from spatialdata.models import force_2d
warnings.simplefilter("ignore")
outer = [(0, 0, 1), (10, 0, 1), (10, 10, 1), (0, 10, 1)]
hole = [(4, 4, 1), (6, 4, 1), (6, 6, 1), (4, 6, 1)]
gdf = gpd.GeoDataFrame({"geometry": [Polygon(outer, [hole])]})
before = gdf.geometry.iloc[0]
print(f"before: has_z={before.has_z} interiors={len(before.interiors)} area={before.area}")
force_2d(gdf)
after = gdf.geometry.iloc[0]
print(f"after : has_z={after.has_z} interiors={len(after.interiors)} area={after.area} (expected: interiors=1, area=96.0)")
print("geopandas reference (gdf.geometry.force_2d()) area:", gpd.GeoDataFrame({"geometry": [Polygon(outer, [hole])]}).geometry.force_2d().iloc[0].area)
bug = len(after.interiors) == 0
print("VERDICT:", "BUG REPRODUCED" if bug else "NOT REPRODUCED")Observed output
before: has_z=True interiors=1 area=96.0
after : has_z=False interiors=0 area=100.0 (expected: interiors=1, area=96.0)
geopandas reference (gdf.geometry.force_2d()) area: 96.0
VERDICT: BUG REPRODUCED
Possible fix direction (unverified)
Delegate to gdf.geometry.force_2d() / shapely.force_2d, or pass holes=[np.array(r.coords)[:, :2] for r in shape.interiors].
Environment
uv run repro.py with the PEP 723 metadata in the script (fresh, isolated environment; spatialdata built from main @ ccf1ea0 (2026-08-28); Python 3.13, latest releases of the dependencies at run time: pandas 3.0, anndata 0.13, zarr 3.3, dask 2026.8, numpy 2.5, geopandas 1.1, shapely 2.1). macOS (arm64). Also reproduced in a second environment with pandas 2.3.3 / anndata 0.12.11 / numpy 2.4.4 / zarr 3.2.1.
Automatically generated; discovered by an AI agent (Claude) and not yet reviewed by a human.
Activity