Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 24 additions & 16 deletions src/erlab/interactive/imagetool/slicer.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,15 +308,18 @@ def qsel_args_from_indexers(
if dim in binned_dim_set:
coord = data[dim][selector].values
center = float(np.round(coord.mean(), order))
width = float(np.round(coord[-1] - coord[0] + inc, order))
width = float(np.abs(np.round(coord[-1] - coord[0] + inc, order)))

out[dim] = center
if not np.allclose(
data[dim]
.sel({dim: slice(center - width / 2, center + width / 2)})
.values,
coord,
):
slice_obj = slice(center - width / 2, center + width / 2)
if coord[0] > coord[-1]:
slice_obj = slice(
slice_obj.stop,
slice_obj.start,
-slice_obj.step if slice_obj.step is not None else None,
)

if not np.allclose(data[dim].sel({dim: slice_obj}).values, coord):
raise ValueError(
"Bin does not contain the same values as the original data."
)
Expand Down Expand Up @@ -1342,7 +1345,18 @@ def _bin_slice(
center = self._indices[cursor][axis]
if self._binned[cursor][axis]:
window = self._bins[cursor][axis]
return slice(center - window // 2, center + (window - 1) // 2 + 1)
start = center - window // 2
stop = center + (window - 1) // 2 + 1
start = max(0, start)
stop = min(self._obj.shape[axis], stop)
size = stop - start
if size < window:
missing = window - size
if start == 0:
stop = min(self._obj.shape[axis], stop + missing)
elif stop == self._obj.shape[axis]:
start = max(0, start - missing)
return slice(start, stop)
if int_if_one:
return center
return slice(center, center + 1)
Expand All @@ -1353,8 +1367,7 @@ def _bin_along_axis(
center = self._indices[cursor][axis]
if not self._binned[cursor][axis]:
return self._obj.data[(slice(None),) * axis + (center,)]
window = self._bins[cursor][axis]
selection = slice(center - window // 2, center + (window - 1) // 2 + 1)
selection = self._bin_slice(cursor, axis)
return erlab.interactive.imagetool.fastbinning.fast_nanmean_skipcheck(
self._obj.data[(slice(None),) * axis + (selection,)],
axis=axis,
Expand All @@ -1373,7 +1386,6 @@ def _bin_along_multiaxis(
"""
# Internal callers pass sorted, unique axes.
reduced_axes: Sequence[int] = axis
bins = self._bins[cursor]
binned = self._binned[cursor]
indices = self._indices[cursor]
selection: list[slice | int] = [slice(None)] * self._obj.ndim
Expand All @@ -1386,11 +1398,7 @@ def _bin_along_multiaxis(
# Keep binned axes in the view and track their positions after any
# earlier unbinned axes have been removed by integer indexing.
any_binned = True
center = indices[ax]
window = bins[ax]
selection[ax] = slice(
center - window // 2, center + (window - 1) // 2 + 1
)
selection[ax] = self._bin_slice(cursor, ax)
selected_axis.append(ax - dropped)
else:
all_binned = False
Expand Down
51 changes: 51 additions & 0 deletions tests/interactive/imagetool/test_slicer.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,19 @@ def test_bin_along_axis_unbinned_matches_integer_index_selection(qtbot) -> None:
np.testing.assert_allclose(slicer._bin_along_axis(0, 1), values[:, 3, :])


def test_bin_slice_descending_axis_boundary_window_two(qtbot) -> None:
data = xr.DataArray(
np.zeros((4, 3), dtype=np.float32),
dims=("x", "y"),
coords={"x": np.array([4.0, 3.0, 2.0, 1.0]), "y": np.arange(3)},
)
slicer = ArraySlicer(data, parent=QtCore.QObject())
slicer.set_indices(0, [0, 1], update=False)
slicer.set_bin(0, 0, 2, update=False)

assert slicer._bin_slice(0, 0) == slice(0, 2)


def test_point_value_unbinned_returns_current_scalar(qtbot) -> None:
values = np.arange(4 * 5, dtype=np.float32).reshape(4, 5)
data = xr.DataArray(
Expand Down Expand Up @@ -588,6 +601,44 @@ def test_qsel_args_from_indexers_validates_live_bin_coordinates() -> None:
qsel_args_from_indexers(data, {"x": slice(2, 5)}, ("x",))


def test_qsel_args_desc_uniform_descending_axis_emits_positive_width() -> None:
data = xr.DataArray(
np.arange(5, dtype=np.float32),
dims=("x",),
coords={"x": np.array([5.0, 4.0, 3.0, 2.0, 1.0])},
)

args = qsel_args_from_indexers(data, {"x": slice(0, 2)}, ("x",))

assert args["x"] == 4.5
assert args["x_width"] > 0.0

actual = data.qsel(args)
expected = data.sortby("x").sel(
x=slice(
args["x"] - args["x_width"] / 2,
args["x"] + args["x_width"] / 2,
)
)
np.testing.assert_allclose(actual.values, expected.mean().values)


def test_xslice_descending_hidden_axis_with_boundary_bin_succeeds() -> None:
values = np.arange(10, dtype=np.float32).reshape(2, 5)
data = xr.DataArray(
values,
dims=("x", "y"),
coords={"x": np.arange(2), "y": np.array([5.0, 4.0, 3.0, 2.0, 1.0])},
)
slicer = ArraySlicer(data, parent=QtCore.QObject())
slicer.set_indices(0, [0, 0], update=False)
slicer.set_bin(0, 1, 2, update=False)

selected = slicer.xslice(0, (0,))

np.testing.assert_allclose(selected.values, np.array([0.5, 5.5], dtype=np.float32))


def test_isel_code_uses_call_kwargs_formatting(qtbot) -> None:
data = xr.DataArray(
np.zeros((4, 5, 6), dtype=np.float32),
Expand Down