From 6561ceb3900e89ee95b53358d9e8bab23692a11a Mon Sep 17 00:00:00 2001 From: Marvin Albert Date: Tue, 1 Jul 2025 15:12:09 +0200 Subject: [PATCH 1/6] Add test showing that DataTree transform fails when transformed scales have zero shape --- tests/core/operations/test_transform.py | 29 ++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/core/operations/test_transform.py b/tests/core/operations/test_transform.py index e8031820e..f9b4ee900 100644 --- a/tests/core/operations/test_transform.py +++ b/tests/core/operations/test_transform.py @@ -11,7 +11,7 @@ from spatialdata._core.data_extent import are_extents_equal, get_extent from spatialdata._core.spatialdata import SpatialData from spatialdata._utils import unpad_raster -from spatialdata.models import PointsModel, ShapesModel, get_axes_names +from spatialdata.models import Image2DModel, PointsModel, ShapesModel, get_axes_names from spatialdata.transformations.operations import ( align_elements_using_landmarks, get_transformation, @@ -229,6 +229,33 @@ def test_transform_shapes(shapes: SpatialData): assert geom_almost_equals(p0["geometry"], p1["geometry"]) +def test_transform_multiscale_image(): + """ + Test the case in which the highest scales of the result of a + transformed multi-scale image would be zero shape. + """ + + test_image = Image2DModel.parse( + np.ones((1, 10, 10)), + dims=("c", "y", "x"), + scale_factors=[2, 4], + transformations={ + "cs1": Scale([0.5] * 2, axes=["y", "x"]), + "cs2": Scale([0.01] * 2, axes=["y", "x"]), + }, + ) + + # check that the transform doesn't raise an error and that it + # discards the lowest resolution level + test_image_t = transform(test_image, to_coordinate_system="cs1") + assert len(test_image.items()) != len(test_image_t.items()) + + # check that a ValueError is raised when no resolution level + # is left after the transformation + with pytest.raises(ValueError, match="The transformation leads to zero shaped data."): + transform(test_image, to_coordinate_system="cs2") + + def test_map_coordinate_systems_single_path(full_sdata: SpatialData): scale = Scale([2], axes=("x",)) translation = Translation([100], axes=("x",)) From 98ec1008c23c9c3640591313be31e443baa42f50 Mon Sep 17 00:00:00 2001 From: Marvin Albert Date: Tue, 1 Jul 2025 15:33:33 +0200 Subject: [PATCH 2/6] Handle resulting scales of shape zero in DataTree transform and make associated test pass --- src/spatialdata/_core/operations/transform.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/spatialdata/_core/operations/transform.py b/src/spatialdata/_core/operations/transform.py index d74bbf2ff..fa3b1446a 100644 --- a/src/spatialdata/_core/operations/transform.py +++ b/src/spatialdata/_core/operations/transform.py @@ -388,6 +388,16 @@ def _( transformed_dask, raster_translation_single_scale = _transform_raster( data=xdata.data, axes=xdata.dims, transformation=composed, **kwargs ) + + # if a scale in the transformed data has zero shape, we skip it + if not np.min(transformed_dask.shape): + if k == "scale0": + raise ValueError( + "The transformation leads to zero shaped data even at the highest resolution level. " + "Check the scaling component of the transformation." + ) + continue + if raster_translation is None: raster_translation = raster_translation_single_scale # we set a dummy empty dict for the transformation that will be replaced with the correct transformation for From ba9fe1631341e496a7df01213bb09ab1464ef8b4 Mon Sep 17 00:00:00 2001 From: Marvin Albert Date: Tue, 1 Jul 2025 15:53:44 +0200 Subject: [PATCH 3/6] Make test name more specific --- tests/core/operations/test_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/operations/test_transform.py b/tests/core/operations/test_transform.py index f9b4ee900..b3e0cb7e3 100644 --- a/tests/core/operations/test_transform.py +++ b/tests/core/operations/test_transform.py @@ -229,7 +229,7 @@ def test_transform_shapes(shapes: SpatialData): assert geom_almost_equals(p0["geometry"], p1["geometry"]) -def test_transform_multiscale_image(): +def test_transform_datatree_scale_handling(): """ Test the case in which the highest scales of the result of a transformed multi-scale image would be zero shape. From 1b1bb376c9d86368a3ecfae5397745c2175e271f Mon Sep 17 00:00:00 2001 From: LucaMarconato <2664412+LucaMarconato@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:03:09 +0100 Subject: [PATCH 4/6] Update tests/core/operations/test_transform.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/core/operations/test_transform.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/core/operations/test_transform.py b/tests/core/operations/test_transform.py index a5a8f2d6c..80d53c6c1 100644 --- a/tests/core/operations/test_transform.py +++ b/tests/core/operations/test_transform.py @@ -252,7 +252,10 @@ def test_transform_datatree_scale_handling(): # check that a ValueError is raised when no resolution level # is left after the transformation - with pytest.raises(ValueError, match="The transformation leads to zero shaped data."): + with pytest.raises( + ValueError, + match="The transformation leads to zero shaped data even at the highest resolution level", + ): transform(test_image, to_coordinate_system="cs2") From d9a1e25e40c345ff34aaa2aa296ba03ddcc5b846 Mon Sep 17 00:00:00 2001 From: LucaMarconato <2664412+LucaMarconato@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:03:46 +0100 Subject: [PATCH 5/6] Update src/spatialdata/_core/operations/transform.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/spatialdata/_core/operations/transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spatialdata/_core/operations/transform.py b/src/spatialdata/_core/operations/transform.py index ee23a30d3..322a8d773 100644 --- a/src/spatialdata/_core/operations/transform.py +++ b/src/spatialdata/_core/operations/transform.py @@ -383,7 +383,7 @@ def _( ) # if a scale in the transformed data has zero shape, we skip it - if not np.min(transformed_dask.shape): + if 0 in transformed_dask.shape: if k == "scale0": raise ValueError( "The transformation leads to zero shaped data even at the highest resolution level. " From 15cdb6e173a7bda25e51d86a0d652d46275e4487 Mon Sep 17 00:00:00 2001 From: Luca Marconato Date: Mon, 5 Jan 2026 18:09:24 +0100 Subject: [PATCH 6/6] tiny improvements --- src/spatialdata/_core/operations/transform.py | 2 ++ tests/core/operations/test_transform.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/spatialdata/_core/operations/transform.py b/src/spatialdata/_core/operations/transform.py index ee23a30d3..ae02fcf76 100644 --- a/src/spatialdata/_core/operations/transform.py +++ b/src/spatialdata/_core/operations/transform.py @@ -389,6 +389,8 @@ def _( "The transformation leads to zero shaped data even at the highest resolution level. " "Check the scaling component of the transformation." ) + # no risk of skipping a scale (e.g. scale1) but not the next ones (e.g. scale2), because once a scale + # is skipped, all the lower scales are also skipped continue if raster_translation is None: diff --git a/tests/core/operations/test_transform.py b/tests/core/operations/test_transform.py index a5a8f2d6c..f6b2d3360 100644 --- a/tests/core/operations/test_transform.py +++ b/tests/core/operations/test_transform.py @@ -231,7 +231,7 @@ def test_transform_shapes(shapes: SpatialData): def test_transform_datatree_scale_handling(): """ - Test the case in which the highest scales of the result of a + Test the cases in which the lowest and highest scale of the result of a transformed multi-scale image would be zero shape. """ @@ -248,7 +248,8 @@ def test_transform_datatree_scale_handling(): # check that the transform doesn't raise an error and that it # discards the lowest resolution level test_image_t = transform(test_image, to_coordinate_system="cs1") - assert len(test_image.items()) != len(test_image_t.items()) + assert list(test_image.keys()) == ["scale0", "scale1", "scale2"] + assert list(test_image_t.keys()) == ["scale0", "scale1"] # check that a ValueError is raised when no resolution level # is left after the transformation