Skip to content

Commit fddb0d7

Browse files
authored
fix(labels): defer float-dtype rejection to spatialdata's model validation (#758)
1 parent 4e93077 commit fddb0d7

3 files changed

Lines changed: 17 additions & 29 deletions

File tree

src/spatialdata_plot/pl/render.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2189,12 +2189,9 @@ def _render_labels(
21892189
is_label=True,
21902190
)
21912191

2192-
if np.issubdtype(label.dtype, np.floating):
2193-
raise ValueError(
2194-
f"Label element '{element}' has dtype {label.dtype}. Label arrays must use an "
2195-
f"integer dtype (e.g. int32 or uint16). Cast before plotting, e.g.:\n"
2196-
f" sdata['{element}'] = sdata['{element}'].astype('int32')"
2197-
)
2192+
# Label dtype is validated upstream: spatialdata rejects non-integer label rasters at the model
2193+
# boundary (parse / SpatialData construction / __setitem__), so a validly built element always
2194+
# reaches here with an integer dtype. No local guard needed (see #606, resolved upstream).
21982195

21992196
# rasterize spatial image if necessary to speed up performance
22002197
if rasterize:

tests/pl/test_render_labels.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -719,23 +719,6 @@ def test_render_labels_lognorm_with_zeros_does_not_crash(sdata_blobs: SpatialDat
719719
plt.close(fig)
720720

721721

722-
@pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64])
723-
def test_render_labels_rejects_float_dtype(dtype):
724-
# Regression test for #606: float-dtype labels must raise a clear
725-
# ValueError naming the element and dtype, not a cryptic skimage TypeError.
726-
arr = np.zeros((20, 20), dtype=dtype)
727-
arr[3:8, 3:8] = 1
728-
arr[12:17, 12:17] = 2
729-
sdata = SpatialData(labels={"lbl": Labels2DModel.parse(arr, dims=["y", "x"])})
730-
731-
fig, ax = plt.subplots()
732-
try:
733-
with pytest.raises(ValueError, match=r"Label element 'lbl'.*integer dtype"):
734-
sdata.pl.render_labels("lbl").pl.show(ax=ax)
735-
finally:
736-
plt.close(fig)
737-
738-
739722
def test_render_labels_rejects_background_instance_id_in_table():
740723
# Regression test for #607: table row with instance_id=0 (background)
741724
# used to crash with obnscure error.

tests/pl/test_utils.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -628,12 +628,20 @@ def test_unmatched_instance_ids_warn_and_write_nan(self, sdata_blobs: SpatialDat
628628
measure_obs(sdata_blobs, "blobs_labels")
629629
assert np.isnan(table.obsm["spatial"]).all()
630630

631-
def test_float_dtype_labels_supported(self, sdata_blobs: SpatialData) -> None:
632-
# #3: a float-typed (but integer-valued) labels raster must not crash np.bincount.
633-
arr = np.asarray(sdata_blobs["blobs_labels"].data).astype(np.float32)
634-
sd = _labels_sdata(arr)
635-
measure_obs(sd, "lab", table_name="t")
636-
assert np.isfinite(sd["t"].obsm["spatial"]).all()
631+
def test_float_dtype_labels_handled_by_centroid_stats(self, sdata_blobs: SpatialData) -> None:
632+
# #3: an integer-valued but float-typed raster must be cast to int, not crash np.bincount.
633+
# spatialdata now rejects float labels at the model boundary, so this can no longer reach
634+
# `measure_obs` through a SpatialData; the cast lives in `_stream_label_centroid_stats`, so
635+
# exercise it there: a float raster must yield exactly what its integer counterpart yields.
636+
from spatialdata_plot.pl.utils import _stream_label_centroid_stats
637+
638+
arr_int = np.asarray(sdata_blobs["blobs_labels"].data).astype(np.int64)
639+
lbl_i, x_i, y_i, area_i = _stream_label_centroid_stats(arr_int)
640+
lbl_f, x_f, y_f, area_f = _stream_label_centroid_stats(arr_int.astype(np.float32))
641+
np.testing.assert_array_equal(lbl_f, lbl_i)
642+
np.testing.assert_allclose(x_f, x_i)
643+
np.testing.assert_allclose(y_f, y_i)
644+
np.testing.assert_array_equal(area_f, area_i)
637645

638646
def test_existing_nonnumeric_column_raises_before_any_write(self, sdata_blobs: SpatialData) -> None:
639647
# #4: a non-numeric collision raises BEFORE obsm is mutated (no half-written table).

0 commit comments

Comments
 (0)