From 992abc59221d43b4195ca5e19563352976277635 Mon Sep 17 00:00:00 2001 From: YooSunyoung <17974113+YooSunYoung@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:05:05 +0100 Subject: [PATCH 01/13] Specify which coordinates to keep. --- src/ess/imaging/tools/analysis.py | 59 +++++++++++++++++++++++++--- tests/imaging/tools/analysis_test.py | 37 +++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/src/ess/imaging/tools/analysis.py b/src/ess/imaging/tools/analysis.py index 3fc5bb7..5c8bba3 100644 --- a/src/ess/imaging/tools/analysis.py +++ b/src/ess/imaging/tools/analysis.py @@ -37,10 +37,23 @@ def blockify( return out +def _is_1d_sorted_bin_edge(img: sc.DataArray, coord_name: str) -> bool: + orig_coord = img.coords[coord_name] + return len(orig_coord.dims) == 1 and ( + len(orig_coord) == img.sizes[orig_coord.dim] + 1 + ) + + +def _has_bin_edge(img: sc.DataArray, coord_name: str) -> bool: + orig_coord = img.coords[coord_name] + return any(orig_coord.sizes[dim] == img.sizes[dim] + 1 for dim in orig_coord.dims) + + def resample( image: sc.Variable | sc.DataArray, sizes: dict[str, int], method: str | Callable = 'sum', + keep: tuple[str, ...] = ('position',), ) -> sc.Variable | sc.DataArray: """ Resample an image by folding it into blocks of specified sizes and applying a @@ -63,14 +76,39 @@ def resample( signature should accept a ``scipp.Variable`` or ``scipp.DataArray`` as first argument and a set of dimensions to reduce over as second argument. The function should return a ``scipp.Variable`` or ``scipp.DataArray``. + keep: + Coordinates to keep in the output. + This argument is ignored if the `image` is not a ``scipp.DataArray``. + If the length of the coordinate is same as the dimension size, + values of each resampled block will be averaged. + New bin edge coordinate will be calculated + only if the original bin edge coordinate is + 1-dimensional and sorted in the ascending order. + Any coordinate that does not exist in the original coordinates + or does not match the condition to be kept, will be ignored. + """ blocked = blockify(image, sizes=sizes) _method = getattr(sc, method) if isinstance(method, str) else method out = _method(blocked, set(blocked.dims) - set(image.dims)) - if 'position' in blocked.coords: - out.coords['position'] = blocked.coords['position'].mean( - set(blocked.dims) - set(image.dims) - ) + if isinstance(image, sc.DataArray) and isinstance(blocked, sc.DataArray): + relevant_keeps = [kcoord for kcoord in keep if kcoord in image.coords] + + for keep_coordinate in relevant_keeps: + coord = blocked.coords[keep_coordinate] + reduced_dims = set(coord.dims) - set(image.coords[keep_coordinate].dims) + if _is_1d_sorted_bin_edge(image, keep_coordinate): + orig_dim = next(iter(image.coords[keep_coordinate].dims)) + reduced_dim = next(iter(reduced_dims)) + out.coords[keep_coordinate] = sc.concat( + [coord.min(reduced_dim), coord.max(dim=reduced_dim)], + dim=orig_dim, + ) + elif _has_bin_edge(image, keep_coordinate): + ... + else: + out.coords[keep_coordinate] = coord.mean(reduced_dims) + return out @@ -78,6 +116,7 @@ def resize( image: sc.Variable | sc.DataArray, sizes: dict[str, int], method: str | Callable = 'sum', + keep: tuple[str, ...] = ('position',), ) -> sc.Variable | sc.DataArray: """ Resize an image by folding it into blocks of specified sizes and applying a @@ -102,6 +141,16 @@ def resize( signature should accept a ``scipp.Variable`` or ``scipp.DataArray`` as first argument and a set of dimensions to reduce over as second argument. The function should return a ``scipp.Variable`` or ``scipp.DataArray``. + keep: + Coordinates to keep in the output. + If the length of the coordinate is same as the dimension size, + values of each resized block will be averaged. + New bin edge coordinate will be calculated + only if the original bin edge coordinate is + 1-dimensional and sorted in the ascending order. + Any coordinate that does not exist in the original coordinates + or does not match the condition to be kept, will be ignored. + """ block_sizes = {} for dim, size in sizes.items(): @@ -111,7 +160,7 @@ def resize( f" the requested size ({size})." ) block_sizes[dim] = image.sizes[dim] // size - return resample(image, sizes=block_sizes, method=method) + return resample(image, sizes=block_sizes, method=method, keep=keep) def laplace_2d( diff --git a/tests/imaging/tools/analysis_test.py b/tests/imaging/tools/analysis_test.py index 7f3d5de..ba77479 100644 --- a/tests/imaging/tools/analysis_test.py +++ b/tests/imaging/tools/analysis_test.py @@ -3,6 +3,7 @@ import numpy as np import pytest import scipp as sc +from scipp.testing import assert_allclose, assert_identical from scitiff.io import load_scitiff from ess import imaging as img @@ -35,6 +36,23 @@ def test_resample_with_position_coord() -> None: ) +def test_resample_keep_specific_coordinates() -> None: + da = load_scitiff(siemens_star_path())["image"] + resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2}, keep=('x',)) + expected_x = img.tools.blockify(da.coords['x'], sizes={'x': 2}).mean('newdim0') + assert_allclose(expected_x, resampled.coords['x']) + assert 'y' not in resampled.coords + + +def test_resample_keep_bin_edge_coordinate() -> None: + da = load_scitiff(siemens_star_path())["image"] + # Overwrite the coordinate 't' to be a bin-edge. + da.coords['t'] = sc.array(dims=['t'], values=[0, 1, 2, 3]) + resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2, 't': 3}, keep=('t',)) + expected_x = sc.array(dims=['t'], values=[0, 3], unit='dimensionless') + assert_identical(expected_x, resampled.coords['t']) + + def test_resample_mean() -> None: da = load_scitiff(siemens_star_path())["image"] resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2}, method='mean') @@ -73,6 +91,25 @@ def test_resize_callable() -> None: assert resized.sizes['y'] == 256 +def test_resize_keep_specific_coordinates() -> None: + da = load_scitiff(siemens_star_path())["image"] + resized = img.tools.resize( + da, sizes={'x': 256, 'y': 256}, method=sc.max, keep=('x',) + ) + expected_x = img.tools.blockify(da.coords['x'], sizes={'x': 5}).mean('newdim0') + assert_identical(expected_x, resized.coords['x']) + assert 'y' not in resized.coords + + +def test_resize_keep_bin_edge_coordinate() -> None: + da = load_scitiff(siemens_star_path())["image"] + # Overwrite the coordinate 't' to be a bin-edge. + da.coords['t'] = sc.array(dims=['t'], values=[0, 1, 2, 3]) + resized = img.tools.resize(da, sizes={'x': 256, 'y': 256, 't': 1}, keep=('t',)) + expected_x = sc.array(dims=['t'], values=[0, 3], unit='dimensionless') + assert_identical(expected_x, resized.coords['t']) + + def test_resize_bad_size_requested_raises(): da = load_scitiff(siemens_star_path())["image"] with pytest.raises(ValueError, match="Size of dimension 'x' .* is not divisible"): From 7778faabf7077fa9093ce3f7c064877dd1cb19fe Mon Sep 17 00:00:00 2001 From: YooSunyoung <17974113+YooSunYoung@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:21:31 +0100 Subject: [PATCH 02/13] Fix bin edge checking logic --- src/ess/imaging/tools/analysis.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ess/imaging/tools/analysis.py b/src/ess/imaging/tools/analysis.py index 5c8bba3..deca4f0 100644 --- a/src/ess/imaging/tools/analysis.py +++ b/src/ess/imaging/tools/analysis.py @@ -39,14 +39,16 @@ def blockify( def _is_1d_sorted_bin_edge(img: sc.DataArray, coord_name: str) -> bool: orig_coord = img.coords[coord_name] - return len(orig_coord.dims) == 1 and ( - len(orig_coord) == img.sizes[orig_coord.dim] + 1 + return ( + len(orig_coord.dims) == 1 + and bool(sc.issorted(orig_coord, dim=orig_coord.dim, order='ascending')) + and (len(orig_coord) == img.sizes[orig_coord.dim] + 1) ) def _has_bin_edge(img: sc.DataArray, coord_name: str) -> bool: orig_coord = img.coords[coord_name] - return any(orig_coord.sizes[dim] == img.sizes[dim] + 1 for dim in orig_coord.dims) + return any(img.coords.is_edges(coord_name, dim) for dim in orig_coord.dims) def resample( From b07d91d50f89f1b8228de25924a44095ec0994b0 Mon Sep 17 00:00:00 2001 From: YooSunyoung <17974113+YooSunYoung@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:25:49 +0100 Subject: [PATCH 03/13] Add test if it ignores unsorted bin edge coordinate. --- tests/imaging/tools/analysis_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/imaging/tools/analysis_test.py b/tests/imaging/tools/analysis_test.py index ba77479..1e5afc2 100644 --- a/tests/imaging/tools/analysis_test.py +++ b/tests/imaging/tools/analysis_test.py @@ -53,6 +53,14 @@ def test_resample_keep_bin_edge_coordinate() -> None: assert_identical(expected_x, resampled.coords['t']) +def test_resample_keep_unsorted_bin_edge_coordinate_ignored() -> None: + da = load_scitiff(siemens_star_path())["image"] + # Overwrite the coordinate 't' to be a bin-edge. + da.coords['t'] = sc.array(dims=['t'], values=[0, 1, 3, 2]) + resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2, 't': 3}, keep=('t',)) + assert 't' not in resampled.coords + + def test_resample_mean() -> None: da = load_scitiff(siemens_star_path())["image"] resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2}, method='mean') From 980dedd35905d1b9204fda1f6fb139ee6e91bef6 Mon Sep 17 00:00:00 2001 From: YooSunyoung <17974113+YooSunYoung@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:34:22 +0100 Subject: [PATCH 04/13] Use stride instead of concat --- src/ess/imaging/tools/analysis.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ess/imaging/tools/analysis.py b/src/ess/imaging/tools/analysis.py index deca4f0..5fe8010 100644 --- a/src/ess/imaging/tools/analysis.py +++ b/src/ess/imaging/tools/analysis.py @@ -102,10 +102,9 @@ def resample( if _is_1d_sorted_bin_edge(image, keep_coordinate): orig_dim = next(iter(image.coords[keep_coordinate].dims)) reduced_dim = next(iter(reduced_dims)) - out.coords[keep_coordinate] = sc.concat( - [coord.min(reduced_dim), coord.max(dim=reduced_dim)], - dim=orig_dim, - ) + out.coords[keep_coordinate] = image.coords[keep_coordinate][ + orig_dim, :: blocked.sizes[reduced_dim] + ] elif _has_bin_edge(image, keep_coordinate): ... else: From 8cc1b60686be5d43e6ad18e198a619944e686585 Mon Sep 17 00:00:00 2001 From: YooSunyoung <17974113+YooSunYoung@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:38:07 +0100 Subject: [PATCH 05/13] Remove position form the default kept coordinate. --- src/ess/imaging/tools/analysis.py | 4 ++-- tests/imaging/tools/analysis_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ess/imaging/tools/analysis.py b/src/ess/imaging/tools/analysis.py index 5fe8010..bb0ec2b 100644 --- a/src/ess/imaging/tools/analysis.py +++ b/src/ess/imaging/tools/analysis.py @@ -55,7 +55,7 @@ def resample( image: sc.Variable | sc.DataArray, sizes: dict[str, int], method: str | Callable = 'sum', - keep: tuple[str, ...] = ('position',), + keep: tuple[str, ...] = (), ) -> sc.Variable | sc.DataArray: """ Resample an image by folding it into blocks of specified sizes and applying a @@ -117,7 +117,7 @@ def resize( image: sc.Variable | sc.DataArray, sizes: dict[str, int], method: str | Callable = 'sum', - keep: tuple[str, ...] = ('position',), + keep: tuple[str, ...] = (), ) -> sc.Variable | sc.DataArray: """ Resize an image by folding it into blocks of specified sizes and applying a diff --git a/tests/imaging/tools/analysis_test.py b/tests/imaging/tools/analysis_test.py index 1e5afc2..af5c129 100644 --- a/tests/imaging/tools/analysis_test.py +++ b/tests/imaging/tools/analysis_test.py @@ -28,7 +28,7 @@ def test_resample_with_position_coord() -> None: da = load_scitiff(siemens_star_path())["image"] vectors = np.random.randn(*da.shape[1:], 3) da.coords['position'] = sc.vectors(dims=['x', 'y'], values=vectors) - resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2}) + resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2}, keep=('position',)) ny, nx = resampled.shape[1:] np.testing.assert_allclose( resampled.coords['position'].values, From 72c20a0b9a05c6ee30a7b3d877dfe3b9acb9bfe8 Mon Sep 17 00:00:00 2001 From: YooSunyoung <17974113+YooSunYoung@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:26:15 +0100 Subject: [PATCH 06/13] Restore coordinates if possible after resampling/resizing. --- src/ess/imaging/tools/analysis.py | 109 ++++++++++++++++----------- tests/imaging/tools/analysis_test.py | 66 ++++++++++------ 2 files changed, 104 insertions(+), 71 deletions(-) diff --git a/src/ess/imaging/tools/analysis.py b/src/ess/imaging/tools/analysis.py index bb0ec2b..2996dac 100644 --- a/src/ess/imaging/tools/analysis.py +++ b/src/ess/imaging/tools/analysis.py @@ -37,25 +37,35 @@ def blockify( return out -def _is_1d_sorted_bin_edge(img: sc.DataArray, coord_name: str) -> bool: - orig_coord = img.coords[coord_name] +def _sizes_not_changed(coord: sc.Variable, target_sizes: dict) -> bool: + return all( + target_resample_size == 1 + for dim, target_resample_size in target_sizes.items() + if dim in coord.dims + ) + + +def _is_1d_sorted_bin_edges(coords: sc.Coords, coord_name: str) -> bool: return ( - len(orig_coord.dims) == 1 - and bool(sc.issorted(orig_coord, dim=orig_coord.dim, order='ascending')) - and (len(orig_coord) == img.sizes[orig_coord.dim] + 1) + (coord := coords[coord_name]).ndim == 1 + and coords.is_edges(coord_name) + and ( + bool(sc.issorted(coord, dim=coord.dim, order='ascending')) + or bool(sc.issorted(coord, dim=coord.dim, order='descending')) + ) ) -def _has_bin_edge(img: sc.DataArray, coord_name: str) -> bool: - orig_coord = img.coords[coord_name] - return any(img.coords.is_edges(coord_name, dim) for dim in orig_coord.dims) +def _is_non_bin_edges(coords: sc.Coords, coord_name: str) -> bool: + return not any( + coords.is_edges(coord_name, dim=dim) for dim in coords[coord_name].dims + ) def resample( image: sc.Variable | sc.DataArray, sizes: dict[str, int], method: str | Callable = 'sum', - keep: tuple[str, ...] = (), ) -> sc.Variable | sc.DataArray: """ Resample an image by folding it into blocks of specified sizes and applying a @@ -78,37 +88,40 @@ def resample( signature should accept a ``scipp.Variable`` or ``scipp.DataArray`` as first argument and a set of dimensions to reduce over as second argument. The function should return a ``scipp.Variable`` or ``scipp.DataArray``. - keep: - Coordinates to keep in the output. - This argument is ignored if the `image` is not a ``scipp.DataArray``. - If the length of the coordinate is same as the dimension size, - values of each resampled block will be averaged. - New bin edge coordinate will be calculated - only if the original bin edge coordinate is - 1-dimensional and sorted in the ascending order. - Any coordinate that does not exist in the original coordinates - or does not match the condition to be kept, will be ignored. + + + Notes + ----- + If the image is a ``scipp.DataArray``, + bin edges in the resampling dimensions + will be preserved if they are 1-dimensional and sorted. + New bin edges will be chosen according to the resampling sizes. + For other coordinates, new coordinates will be average values + of each resampled blocks. + + .. warning:: + The coordinates in the output may not be correct + if they are not sorted or not linear. """ blocked = blockify(image, sizes=sizes) _method = getattr(sc, method) if isinstance(method, str) else method out = _method(blocked, set(blocked.dims) - set(image.dims)) - if isinstance(image, sc.DataArray) and isinstance(blocked, sc.DataArray): - relevant_keeps = [kcoord for kcoord in keep if kcoord in image.coords] - - for keep_coordinate in relevant_keeps: - coord = blocked.coords[keep_coordinate] - reduced_dims = set(coord.dims) - set(image.coords[keep_coordinate].dims) - if _is_1d_sorted_bin_edge(image, keep_coordinate): - orig_dim = next(iter(image.coords[keep_coordinate].dims)) - reduced_dim = next(iter(reduced_dims)) - out.coords[keep_coordinate] = image.coords[keep_coordinate][ - orig_dim, :: blocked.sizes[reduced_dim] - ] - elif _has_bin_edge(image, keep_coordinate): - ... - else: - out.coords[keep_coordinate] = coord.mean(reduced_dims) + + if isinstance(image, sc.DataArray): + # Restore the coordinates dropped by the `_method` if possible. + _dropped_cnames = set(image.coords.keys()) - set(out.coords.keys()) + + for name in _dropped_cnames: + coord = image.coords[name] + if _sizes_not_changed(coord, sizes): + out.coords[name] = image.coords[name] + elif _is_1d_sorted_bin_edges(image.coords, name): + out.coords[name] = coord[coord.dim, :: sizes[coord.dim]] + elif _is_non_bin_edges(image.coords, name): + folded_coord = blocked.coords[name] + reduced_dims = set(folded_coord.dims) - set(coord.dims) + out.coords[name] = folded_coord.mean(reduced_dims) return out @@ -117,7 +130,6 @@ def resize( image: sc.Variable | sc.DataArray, sizes: dict[str, int], method: str | Callable = 'sum', - keep: tuple[str, ...] = (), ) -> sc.Variable | sc.DataArray: """ Resize an image by folding it into blocks of specified sizes and applying a @@ -142,15 +154,20 @@ def resize( signature should accept a ``scipp.Variable`` or ``scipp.DataArray`` as first argument and a set of dimensions to reduce over as second argument. The function should return a ``scipp.Variable`` or ``scipp.DataArray``. - keep: - Coordinates to keep in the output. - If the length of the coordinate is same as the dimension size, - values of each resized block will be averaged. - New bin edge coordinate will be calculated - only if the original bin edge coordinate is - 1-dimensional and sorted in the ascending order. - Any coordinate that does not exist in the original coordinates - or does not match the condition to be kept, will be ignored. + + + Notes + ----- + If the image is a ``scipp.DataArray``, + bin edges in the resizing dimensions + will be preserved if they are 1-dimensional and sorted. + New bin edges will be chosen according to the resizing sizes. + For other coordinates, new coordinates will be average values + of each resized blocks. + + .. warning:: + The coordinates in the output may not be correct + if they are not sorted or not linear. """ block_sizes = {} @@ -161,7 +178,7 @@ def resize( f" the requested size ({size})." ) block_sizes[dim] = image.sizes[dim] // size - return resample(image, sizes=block_sizes, method=method, keep=keep) + return resample(image, sizes=block_sizes, method=method) def laplace_2d( diff --git a/tests/imaging/tools/analysis_test.py b/tests/imaging/tools/analysis_test.py index af5c129..bbe5f0b 100644 --- a/tests/imaging/tools/analysis_test.py +++ b/tests/imaging/tools/analysis_test.py @@ -3,7 +3,7 @@ import numpy as np import pytest import scipp as sc -from scipp.testing import assert_allclose, assert_identical +from scipp.testing import assert_identical from scitiff.io import load_scitiff from ess import imaging as img @@ -24,11 +24,11 @@ def test_resample() -> None: assert resampled.sizes['y'] == da.sizes['y'] // 2 -def test_resample_with_position_coord() -> None: +def test_resample_with_2d_position_coord() -> None: da = load_scitiff(siemens_star_path())["image"] vectors = np.random.randn(*da.shape[1:], 3) da.coords['position'] = sc.vectors(dims=['x', 'y'], values=vectors) - resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2}, keep=('position',)) + resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2}) ny, nx = resampled.shape[1:] np.testing.assert_allclose( resampled.coords['position'].values, @@ -36,31 +36,41 @@ def test_resample_with_position_coord() -> None: ) -def test_resample_keep_specific_coordinates() -> None: - da = load_scitiff(siemens_star_path())["image"] - resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2}, keep=('x',)) - expected_x = img.tools.blockify(da.coords['x'], sizes={'x': 2}).mean('newdim0') - assert_allclose(expected_x, resampled.coords['x']) - assert 'y' not in resampled.coords - - def test_resample_keep_bin_edge_coordinate() -> None: da = load_scitiff(siemens_star_path())["image"] # Overwrite the coordinate 't' to be a bin-edge. da.coords['t'] = sc.array(dims=['t'], values=[0, 1, 2, 3]) - resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2, 't': 3}, keep=('t',)) + resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2, 't': 3}) expected_x = sc.array(dims=['t'], values=[0, 3], unit='dimensionless') assert_identical(expected_x, resampled.coords['t']) -def test_resample_keep_unsorted_bin_edge_coordinate_ignored() -> None: +def test_resample_unsorted_bin_edge_coordinate_ignored() -> None: da = load_scitiff(siemens_star_path())["image"] # Overwrite the coordinate 't' to be a bin-edge. da.coords['t'] = sc.array(dims=['t'], values=[0, 1, 3, 2]) - resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2, 't': 3}, keep=('t',)) + resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2, 't': 3}) assert 't' not in resampled.coords +def test_resample_2d_partial_bin_edge_coordinate_ignored() -> None: + data = sc.array(dims=['x', 'y'], values=[[1, 2], [3, 4]]) + pos = sc.array(dims=['x', 'y'], values=[[1, 2, 3], [3, 4, 5]]) + da = sc.DataArray(data=data, coords={'pos': pos}) + assert not da.coords.is_edges('pos', 'x') + assert da.coords.is_edges('pos', 'y') + resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2}) + assert 'pos' not in resampled.coords + + +def test_resample_2d_coordinate_not_dropped_if_not_changed() -> None: + data = sc.array(dims=['x', 'y'], values=[[1, 2], [3, 4]]) + pos = sc.array(dims=['x', 'y'], values=[[1, 2, 3], [3, 4, 5]]) + da = sc.DataArray(data=data, coords={'pos': pos}) + resampled = img.tools.resample(da, sizes={'x': 1, 'y': 1}) + assert_identical(pos, resampled.coords['pos']) + + def test_resample_mean() -> None: da = load_scitiff(siemens_star_path())["image"] resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2}, method='mean') @@ -99,25 +109,31 @@ def test_resize_callable() -> None: assert resized.sizes['y'] == 256 -def test_resize_keep_specific_coordinates() -> None: - da = load_scitiff(siemens_star_path())["image"] - resized = img.tools.resize( - da, sizes={'x': 256, 'y': 256}, method=sc.max, keep=('x',) - ) - expected_x = img.tools.blockify(da.coords['x'], sizes={'x': 5}).mean('newdim0') - assert_identical(expected_x, resized.coords['x']) - assert 'y' not in resized.coords - - def test_resize_keep_bin_edge_coordinate() -> None: da = load_scitiff(siemens_star_path())["image"] # Overwrite the coordinate 't' to be a bin-edge. da.coords['t'] = sc.array(dims=['t'], values=[0, 1, 2, 3]) - resized = img.tools.resize(da, sizes={'x': 256, 'y': 256, 't': 1}, keep=('t',)) + resized = img.tools.resize(da, sizes={'x': 256, 'y': 256, 't': 1}) expected_x = sc.array(dims=['t'], values=[0, 3], unit='dimensionless') assert_identical(expected_x, resized.coords['t']) +def test_resize_unsorted_bin_edge_ignored() -> None: + da = load_scitiff(siemens_star_path())["image"] + # Overwrite the coordinate 't' to be a bin-edge. + da.coords['t'] = sc.array(dims=['t'], values=[0, 2, 1, 3]) + resized = img.tools.resize(da, sizes={'x': 256, 'y': 256, 't': 1}) + assert 't' not in resized.coords + + +def test_resize_2d_coordinate_not_dropped_if_not_changed() -> None: + data = sc.array(dims=['x', 'y'], values=[[1, 2], [3, 4]]) + pos = sc.array(dims=['x', 'y'], values=[[1, 2, 3], [3, 4, 5]]) + da = sc.DataArray(data=data, coords={'pos': pos}) + resampled = img.tools.resize(da, sizes={'x': 2, 'y': 2}) + assert_identical(pos, resampled.coords['pos']) + + def test_resize_bad_size_requested_raises(): da = load_scitiff(siemens_star_path())["image"] with pytest.raises(ValueError, match="Size of dimension 'x' .* is not divisible"): From 18990470477f6329f96ef0040ffd20540c1961dd Mon Sep 17 00:00:00 2001 From: YooSunyoung <17974113+YooSunYoung@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:30:26 +0100 Subject: [PATCH 07/13] Add more tests with coordinates for resample/resize and rename variable. --- tests/imaging/tools/analysis_test.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/tests/imaging/tools/analysis_test.py b/tests/imaging/tools/analysis_test.py index bbe5f0b..647a3ad 100644 --- a/tests/imaging/tools/analysis_test.py +++ b/tests/imaging/tools/analysis_test.py @@ -36,13 +36,22 @@ def test_resample_with_2d_position_coord() -> None: ) +def test_resample_keep_coordinate() -> None: + da = load_scitiff(siemens_star_path())["image"] + # Overwrite the coordinate 't' to be a bin-edge. + da.coords['t'] = sc.array(dims=['t'], values=[1, 2, 3]) + resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2, 't': 3}) + expected_t = sc.array(dims=['t'], values=[2.0], unit='dimensionless') + assert_identical(expected_t, resampled.coords['t']) + + def test_resample_keep_bin_edge_coordinate() -> None: da = load_scitiff(siemens_star_path())["image"] # Overwrite the coordinate 't' to be a bin-edge. da.coords['t'] = sc.array(dims=['t'], values=[0, 1, 2, 3]) resampled = img.tools.resample(da, sizes={'x': 2, 'y': 2, 't': 3}) - expected_x = sc.array(dims=['t'], values=[0, 3], unit='dimensionless') - assert_identical(expected_x, resampled.coords['t']) + expected_t = sc.array(dims=['t'], values=[0, 3], unit='dimensionless') + assert_identical(expected_t, resampled.coords['t']) def test_resample_unsorted_bin_edge_coordinate_ignored() -> None: @@ -109,13 +118,22 @@ def test_resize_callable() -> None: assert resized.sizes['y'] == 256 +def test_resize_keep_coordinate() -> None: + da = load_scitiff(siemens_star_path())["image"] + # Overwrite the coordinate 't' to be a bin-edge. + da.coords['t'] = sc.array(dims=['t'], values=[1, 2, 3]) + resampled = img.tools.resize(da, sizes={'t': 1}) + expected_t = sc.array(dims=['t'], values=[2.0], unit='dimensionless') + assert_identical(expected_t, resampled.coords['t']) + + def test_resize_keep_bin_edge_coordinate() -> None: da = load_scitiff(siemens_star_path())["image"] # Overwrite the coordinate 't' to be a bin-edge. da.coords['t'] = sc.array(dims=['t'], values=[0, 1, 2, 3]) resized = img.tools.resize(da, sizes={'x': 256, 'y': 256, 't': 1}) - expected_x = sc.array(dims=['t'], values=[0, 3], unit='dimensionless') - assert_identical(expected_x, resized.coords['t']) + expected_t = sc.array(dims=['t'], values=[0, 3], unit='dimensionless') + assert_identical(expected_t, resized.coords['t']) def test_resize_unsorted_bin_edge_ignored() -> None: From f1446245686039e73190508cab076392bc831d63 Mon Sep 17 00:00:00 2001 From: Sunyoung Yoo <17974113+YooSunYoung@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:41:53 +0100 Subject: [PATCH 08/13] Update docstring Co-authored-by: Neil Vaytet <39047984+nvaytet@users.noreply.github.com> --- src/ess/imaging/tools/analysis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ess/imaging/tools/analysis.py b/src/ess/imaging/tools/analysis.py index 2996dac..57a5020 100644 --- a/src/ess/imaging/tools/analysis.py +++ b/src/ess/imaging/tools/analysis.py @@ -94,10 +94,10 @@ def resample( ----- If the image is a ``scipp.DataArray``, bin edges in the resampling dimensions - will be preserved if they are 1-dimensional and sorted. + will be preserved if they are 1-dimensional and sorted (they are dropped otherwise). New bin edges will be chosen according to the resampling sizes. - For other coordinates, new coordinates will be average values - of each resampled blocks. + For midpoint coordinates, new coordinates will be average values + of each resampled block. .. warning:: The coordinates in the output may not be correct From 379fd0cada33358db3f9153c1bc4c34e1d8cd957 Mon Sep 17 00:00:00 2001 From: YooSunyoung <17974113+YooSunYoung@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:59:15 +0100 Subject: [PATCH 09/13] Filter unnecessary sizes earlier. --- src/ess/imaging/tools/analysis.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/ess/imaging/tools/analysis.py b/src/ess/imaging/tools/analysis.py index 57a5020..a02108a 100644 --- a/src/ess/imaging/tools/analysis.py +++ b/src/ess/imaging/tools/analysis.py @@ -37,14 +37,6 @@ def blockify( return out -def _sizes_not_changed(coord: sc.Variable, target_sizes: dict) -> bool: - return all( - target_resample_size == 1 - for dim, target_resample_size in target_sizes.items() - if dim in coord.dims - ) - - def _is_1d_sorted_bin_edges(coords: sc.Coords, coord_name: str) -> bool: return ( (coord := coords[coord_name]).ndim == 1 @@ -81,6 +73,9 @@ def resample( sizes: A dictionary specifying the block sizes for each dimension. For example, ``{'x': 4, 'y': 4}`` will create blocks of size 4x4. + Any dimensions with size ``1`` will be ignored. + If all sizes are set to ``1``, + it will not apply ``method`` and return a copy of the input resampling image. method: The reduction method to apply to the blocks. This can be a string referring to any valid Scipp reduction method, such as 'sum', 'mean', 'max', etc. @@ -104,6 +99,11 @@ def resample( if they are not sorted or not linear. """ + # Filter the resample sizes first. + sizes = {dim: size for dim, size in sizes.items() if size != 1} + if not sizes: + return image.copy() + blocked = blockify(image, sizes=sizes) _method = getattr(sc, method) if isinstance(method, str) else method out = _method(blocked, set(blocked.dims) - set(image.dims)) @@ -114,9 +114,7 @@ def resample( for name in _dropped_cnames: coord = image.coords[name] - if _sizes_not_changed(coord, sizes): - out.coords[name] = image.coords[name] - elif _is_1d_sorted_bin_edges(image.coords, name): + if _is_1d_sorted_bin_edges(image.coords, name): out.coords[name] = coord[coord.dim, :: sizes[coord.dim]] elif _is_non_bin_edges(image.coords, name): folded_coord = blocked.coords[name] @@ -147,6 +145,9 @@ def resize( The original sizes should be divisible by the specified sizes. For example, ``{'x': 128, 'y': 128}`` will create an output image of size 128x128. + Any dimensions with same sizes to the resizing image will be ignored. + If the output image sizes will be same as the input image sizes, + it will not apply the ``method`` and return a copy of the input image. method: The reduction method to apply to the blocks. This can be a string referring to any valid Scipp reduction method, such as 'sum', 'mean', 'max', etc. @@ -177,7 +178,9 @@ def resize( f"Size of dimension '{dim}' ({image.sizes[dim]}) is not divisible by" f" the requested size ({size})." ) - block_sizes[dim] = image.sizes[dim] // size + if (_resample_size := image.sizes[dim] // size) != 1: + block_sizes[dim] = _resample_size + return resample(image, sizes=block_sizes, method=method) From bef8ba50f7b6cb152797d299abdce306deccb23b Mon Sep 17 00:00:00 2001 From: YooSunyoung <17974113+YooSunYoung@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:03:06 +0100 Subject: [PATCH 10/13] Add tests with the same size results. --- tests/imaging/tools/analysis_test.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/imaging/tools/analysis_test.py b/tests/imaging/tools/analysis_test.py index 647a3ad..cf82895 100644 --- a/tests/imaging/tools/analysis_test.py +++ b/tests/imaging/tools/analysis_test.py @@ -24,6 +24,13 @@ def test_resample() -> None: assert resampled.sizes['y'] == da.sizes['y'] // 2 +def test_resample_with_sizes_1() -> None: + da = load_scitiff(siemens_star_path())["image"] + resampled = img.tools.resample(da, sizes=dict.fromkeys(da.dims, 1)) + assert_identical(resampled, da) + assert da is not resampled + + def test_resample_with_2d_position_coord() -> None: da = load_scitiff(siemens_star_path())["image"] vectors = np.random.randn(*da.shape[1:], 3) @@ -118,6 +125,13 @@ def test_resize_callable() -> None: assert resized.sizes['y'] == 256 +def test_resize_same_sizes() -> None: + da = load_scitiff(siemens_star_path())["image"] + resized = img.tools.resize(da, sizes=da.sizes) + assert_identical(resized, da) + assert da is not resized + + def test_resize_keep_coordinate() -> None: da = load_scitiff(siemens_star_path())["image"] # Overwrite the coordinate 't' to be a bin-edge. From b5b41d51a1d3f8e5fc5978e7222950a1dffab62f Mon Sep 17 00:00:00 2001 From: Sunyoung Yoo <17974113+YooSunYoung@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:07:21 +0100 Subject: [PATCH 11/13] Remove unecessary check. --- src/ess/imaging/tools/analysis.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ess/imaging/tools/analysis.py b/src/ess/imaging/tools/analysis.py index a02108a..fe8203a 100644 --- a/src/ess/imaging/tools/analysis.py +++ b/src/ess/imaging/tools/analysis.py @@ -178,8 +178,7 @@ def resize( f"Size of dimension '{dim}' ({image.sizes[dim]}) is not divisible by" f" the requested size ({size})." ) - if (_resample_size := image.sizes[dim] // size) != 1: - block_sizes[dim] = _resample_size + block_sizes[dim] = image.sizes[dim] // size return resample(image, sizes=block_sizes, method=method) From 14039862bcca993c0e7d73d84e4b5999c07d68bb Mon Sep 17 00:00:00 2001 From: Sunyoung Yoo <17974113+YooSunYoung@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:07:51 +0100 Subject: [PATCH 12/13] Remove unecessary line... --- src/ess/imaging/tools/analysis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ess/imaging/tools/analysis.py b/src/ess/imaging/tools/analysis.py index fe8203a..145a7e1 100644 --- a/src/ess/imaging/tools/analysis.py +++ b/src/ess/imaging/tools/analysis.py @@ -179,7 +179,6 @@ def resize( f" the requested size ({size})." ) block_sizes[dim] = image.sizes[dim] // size - return resample(image, sizes=block_sizes, method=method) From 68caeec6f1ac0818aabf3f6ca3a7fce8f3c2282d Mon Sep 17 00:00:00 2001 From: Sunyoung Yoo <17974113+YooSunYoung@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:17:13 +0100 Subject: [PATCH 13/13] Shallow copy if resampling not needed. --- src/ess/imaging/tools/analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ess/imaging/tools/analysis.py b/src/ess/imaging/tools/analysis.py index 145a7e1..f62dc39 100644 --- a/src/ess/imaging/tools/analysis.py +++ b/src/ess/imaging/tools/analysis.py @@ -102,7 +102,7 @@ def resample( # Filter the resample sizes first. sizes = {dim: size for dim, size in sizes.items() if size != 1} if not sizes: - return image.copy() + return image.copy(deep=False) blocked = blockify(image, sizes=sizes) _method = getattr(sc, method) if isinstance(method, str) else method