Skip to content

Commit 832edaf

Browse files
committed
fix(labels): defer float-dtype rejection to spatialdata's model validation
spatialdata 0.8 rejects non-integer label rasters at the model boundary (parse / SpatialData construction / __setitem__) with a clear, actionable message. This makes _render_labels' own float-dtype guard (added in #606, when spatialdata 0.5 still accepted float labels) redundant dead code: a validly built element can no longer reach the renderer with a float dtype. - Remove the redundant render-time guard; trust the upstream boundary. - Drop test_render_labels_rejects_float_dtype (asserts an unreachable render-time scenario; upstream now raises earlier at parse). - Rewrite the measure_obs float test to exercise _stream_label_centroid_stats directly (where the integer-valued-float -> int cast lives), since a float labels element can no longer be placed in a SpatialData. The helper's float->int cast is kept as cheap, correct-by-construction robustness for any integer-valued float raster passed to it directly.
1 parent 227434d commit 832edaf

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
@@ -710,23 +710,6 @@ def test_render_labels_lognorm_with_zeros_does_not_crash(sdata_blobs: SpatialDat
710710
plt.close(fig)
711711

712712

713-
@pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64])
714-
def test_render_labels_rejects_float_dtype(dtype):
715-
# Regression test for #606: float-dtype labels must raise a clear
716-
# ValueError naming the element and dtype, not a cryptic skimage TypeError.
717-
arr = np.zeros((20, 20), dtype=dtype)
718-
arr[3:8, 3:8] = 1
719-
arr[12:17, 12:17] = 2
720-
sdata = SpatialData(labels={"lbl": Labels2DModel.parse(arr, dims=["y", "x"])})
721-
722-
fig, ax = plt.subplots()
723-
try:
724-
with pytest.raises(ValueError, match=r"Label element 'lbl'.*integer dtype"):
725-
sdata.pl.render_labels("lbl").pl.show(ax=ax)
726-
finally:
727-
plt.close(fig)
728-
729-
730713
def test_render_labels_rejects_background_instance_id_in_table():
731714
# Regression test for #607: table row with instance_id=0 (background)
732715
# 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)