Skip to content

Collection of minor issues found by an automated bug hunt (validation edge cases, misleading errors, small inefficiencies) #1244

Description

@LucaMarconato

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

Each item was confirmed with a script; the repro below demonstrates items 1–8.

  1. Empty GeoDataFrameIndexError in ShapesModel.parse and get_axes_names (.iloc[0]) instead of the ValueError("Column geometry is empty") that validate would raise.
  2. TableModel.parse(region=("a", "b")) (tuple) fails with the misleading adata.obs[region] values do not match with region values; region=pd.Index([...]) fails with TypeError: unhashable type: 'Index'. Only list/np.ndarray are normalised.
  3. RasterSchema.parse(chunks=<float>) crashes with ValueError: not enough values to unpack ({dim: chunks for index, dim in data.dims} iterates dimension names).
  4. RasterSchema.parse(DataArray) mutates its input: a numpy-backed DataArray has its .data replaced by a dask array in place.
  5. PointsModel.validate rejects pandas nullable / pyarrow numeric coordinate dtypes (Int64, float64[pyarrow]) while accepting every numpy int/float — use pd.api.types.is_numeric_dtype.
  6. get_element_instances(shapes_or_points, return_background=True)TypeError: _() got an unexpected keyword argument (the GeoDataFrame/DaskDataFrame overloads lack the keyword the generic signature advertises).
  7. set_table_annotates_spatialelement(region=pd.Series([...])) — the type hint allows a Series, but the Series object is stored in uns["spatialdata_attrs"]["region"]; TableModel.validate then fails with unhashable type: 'Series' and the table cannot be written.
  8. filter_by_table_query(table_name, element_names=[...]) raises KeyError: 't' when no table row annotates the requested elements (subset(filter_tables=True) drops the empty table before sdata_subset.tables[table_name]).

Not in the script:

  1. CLI: python -m spatialdata peek <store> table silently drops the tables — the click Choice offers "table" (singular) but read_zarr expects "tables", and tables is rejected by click.
  2. TableModel.parse uniqueness check computes groupby(region).nunique() over all obs columns instead of only the instance key (10–100× slower; 0.35 s vs 0.02 s for 1 M rows × 54 columns). Use grouped[instance_key].nunique().
  3. relabel_sequential breaks when the internal lookup array spans several dask chunks (max_label > ~16 M with the default chunk size): ValueError: Dimension 1 has N blocks, adjust_chunks specified with 1 blocks. rechunk(-1) the lookup array.
  4. polygon_query(circles, clip=True) returns Polygon geometries but keeps the radius column.
  5. to_polygons(circles) buffers row-by-row with DataFrame.apply(axis=1) (2.1 s for 167 k Xenium circles) whereas shapely.buffer(geoms, radii, quad_segs=...) takes 0.58 s with identical areas.
  6. map_raster docstring: changing dimensionality needs drop_axis/new_axis (forwarded via **kwargs) in addition to dims; with only dims=("y", "x") the call fails with number of dimensions of the output data (3) differs....
  7. get_transformation_between_landmarks docstring example uses PointsModel(points_moving) instead of PointsModel.parse(...); _get_current_output_axes contains a dead statement set(transformation.map_axis.keys()).
  8. Compressor level docs inconsistent: _validate_compressor_args messages say 'between 1 and 9' in two places and 'between 0 and 9' in another; level 0 is accepted.
  9. Docstring/signature mismatches: rasterize_bins documents return_regions_as_labels (parameter: return_region_as_labels); SpatialData.subset documents filter_table (parameter: filter_tables); validate_axes documents axis (parameter: axes); get_extent's generic docstring lists its parameters under 'Returns'; get_transformation_between_coordinate_systems does not document sdata/intermediate_coordinate_systems; the points↔geopandas converters do not document suppress_z_warning.
  10. Forward compatibility: Pandas4Warning: The copy keyword is deprecated from table.obs.rename(columns=..., copy=False) in concatenate._concatenate_tables (will raise in pandas 4).
  11. ShapesModel.validate inspects only the first geometry, so [Polygon, Point] without a radius column is accepted; validate_shapes_not_mixed_types exists but is opt-in.
  12. Table filtering via left join computes da.unique on full-resolution labels in subset(), filter_by_coordinate_system() and spatial queries (0.1 s per 3648×5472 labels, 1.8 s for filter_by_coordinate_system on the 30-labels CosMx dataset); for filter_by_coordinate_system a name-based filter would be O(n_rows). (See also Bounding box query APIs slowed down by join operations #1177.)

Severity (agent's assessment): low (each item)

Where: various, see list

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",
# ]
# ///
"""Collection of small issues, each demonstrated below (see the issue text for the full list)."""
import warnings
import numpy as np
import pandas as pd
import geopandas as gpd
from anndata import AnnData
from shapely.geometry import Point
from spatialdata import SpatialData, get_element_instances
from spatialdata.models import Image2DModel, PointsModel, ShapesModel, TableModel

warnings.simplefilter("ignore")
found = []

print("1. empty GeoDataFrame -> IndexError instead of a clear ValueError")
try:
    ShapesModel.parse(gpd.GeoDataFrame({"geometry": gpd.GeoSeries([], dtype="geometry")}))
except Exception as e:  # noqa: BLE001
    print("   ", type(e).__name__, str(e)[:60]); found.append(type(e).__name__ == "IndexError")

print("2. TableModel.parse(region=tuple / pd.Index) -> misleading errors")
obs = pd.DataFrame({"region": pd.Categorical(["a", "a", "b", "b"]), "instance_id": [0, 1, 0, 1]})
for reg in [("a", "b"), pd.Index(["a", "b"])]:
    try:
        TableModel.parse(AnnData(X=np.zeros((4, 1)), obs=obs.copy()), region=reg, region_key="region", instance_key="instance_id")
        print("   ", type(reg).__name__, "OK")
    except Exception as e:  # noqa: BLE001
        print("   ", type(reg).__name__, "->", type(e).__name__, str(e)[:70]); found.append(True)

print("3. RasterSchema.parse(chunks=<float>) -> ValueError from a wrong comprehension")
try:
    Image2DModel.parse(np.zeros((1, 8, 8)), scale_factors=[2], chunks=4.0)
except Exception as e:  # noqa: BLE001
    print("   ", type(e).__name__, str(e)[:70]); found.append(True)

print("4. RasterSchema.parse mutates a numpy-backed input DataArray in place")
import xarray as xr
arr = xr.DataArray(np.zeros((1, 4, 4)), dims=("c", "y", "x"))
Image2DModel.parse(arr)
print("    input .data type after parse:", type(arr.data).__name__); found.append(type(arr.data).__name__ != "ndarray")

print("5. PointsModel.validate rejects pandas nullable / pyarrow numeric coordinate dtypes")
for dt in ["Int64", "float64[pyarrow]"]:
    try:
        PointsModel.parse(pd.DataFrame({"x": pd.array([1, 2], dtype=dt), "y": [1.0, 2.0]})); print("   ", dt, "OK")
    except Exception as e:  # noqa: BLE001
        print("   ", dt, "->", type(e).__name__, str(e)[:60]); found.append(True)

print("6. get_element_instances(shapes, return_background=True) -> TypeError (overload lacks the keyword)")
shp = ShapesModel.parse(gpd.GeoDataFrame({"geometry": [Point(0, 0)], "radius": [1.0]}))
try:
    get_element_instances(shp, return_background=True)
except Exception as e:  # noqa: BLE001
    print("   ", type(e).__name__, str(e)[:70]); found.append(True)

print("7. set_table_annotates_spatialelement(region=pd.Series) stores the Series in uns -> validate fails")
obs2 = pd.DataFrame({"region": pd.Categorical(["shp"]), "instance_id": [0]})
t = TableModel.parse(AnnData(X=np.zeros((1, 1)), obs=obs2), region="shp", region_key="region", instance_key="instance_id")
sdata = SpatialData(shapes={"shp": shp}, tables={"t": t})
sdata.set_table_annotates_spatialelement("t", region=pd.Series(["shp"]))
try:
    TableModel.validate(sdata["t"]); print("    validate OK")
except Exception as e:  # noqa: BLE001
    print("    validate ->", type(e).__name__, str(e)[:60]); found.append(True)

print("8. filter_by_table_query(element_names=[...]) -> KeyError when no table row annotates those elements")
other = ShapesModel.parse(gpd.GeoDataFrame({"geometry": [Point(5, 5)], "radius": [1.0]}))
t2 = TableModel.parse(AnnData(X=np.zeros((1, 1)), obs=obs2.copy()), region="shp", region_key="region", instance_key="instance_id")
sdata2 = SpatialData(shapes={"shp": shp, "other": other}, tables={"t": t2})
try:
    sdata2.filter_by_table_query("t", element_names=["other"]); print("    OK")
except Exception as e:  # noqa: BLE001
    print("    ->", type(e).__name__, str(e)[:60]); found.append(True)

print("VERDICT:", "BUG REPRODUCED" if all(found) and found else "NOT REPRODUCED")

Observed output

1. empty GeoDataFrame -> IndexError instead of a clear ValueError
    IndexError single positional indexer is out-of-bounds
2. TableModel.parse(region=tuple / pd.Index) -> misleading errors
    tuple -> ValueError `adata.obs[region]` values do not match with `region` values.
    Index -> TypeError unhashable type: 'Index'
3. RasterSchema.parse(chunks=<float>) -> ValueError from a wrong comprehension
    ValueError not enough values to unpack (expected 2, got 1)
4. RasterSchema.parse mutates a numpy-backed input DataArray in place
    input .data type after parse: Array
5. PointsModel.validate rejects pandas nullable / pyarrow numeric coordinate dtypes
    Int64 -> ValueError Column `x` must be of type `int` or `float`.
    float64[pyarrow] -> ValueError Column `x` must be of type `int` or `float`.
6. get_element_instances(shapes, return_background=True) -> TypeError (overload lacks the keyword)
    TypeError _() got an unexpected keyword argument 'return_background'
7. set_table_annotates_spatialelement(region=pd.Series) stores the Series in uns -> validate fails
    validate -> TypeError unhashable type: 'Series'
8. filter_by_table_query(element_names=[...]) -> KeyError when no table row annotates those elements
    -> KeyError 't'
VERDICT: BUG REPRODUCED

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.

Possibly related issues

#1177


Automatically generated; discovered by an AI agent (Claude) and not yet reviewed by a human.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions