From 7a61f840423e558c9a7452653742d5e0ac53afa5 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 3 Feb 2026 22:07:41 +0100 Subject: [PATCH 1/9] optimize preprocessing and remove checkboxes for detector banks --- src/ess/dream/instrument_view.py | 206 ++++++++----------------------- 1 file changed, 54 insertions(+), 152 deletions(-) diff --git a/src/ess/dream/instrument_view.py b/src/ess/dream/instrument_view.py index d97a6348..245ba22b 100644 --- a/src/ess/dream/instrument_view.py +++ b/src/ess/dream/instrument_view.py @@ -1,18 +1,32 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) -from __future__ import annotations - -from html import escape -from typing import TYPE_CHECKING, Any import plopp as pp import scipp as sc +from plopp.core.typing import FigureLike + -if TYPE_CHECKING: - try: - from plopp.widgets import Box - except ModuleNotFoundError: - Box = object +def _to_data_array( + data: sc.DataArray | sc.DataGroup | dict, dim: str | None +) -> sc.DataArray: + if isinstance(data, sc.DataArray): + data = sc.DataGroup({"": data}) + pieces = [] + for da in data.values(): + da = da.drop_coords(set(da.coords) - {"position", dim}) + dims = list(da.dims) + if (dim is not None) and (dim in dims): + # Ensure that the dims to be flattened are contiguous + da = da.transpose([d for d in dims if d != dim] + [dim]) + dims.remove(dim) + flat = da.flatten(dims=dims, to="pixel") + filtered = flat[sc.isfinite(flat.coords["position"])] + pieces.append( + filtered.assign_coords( + {k: getattr(filtered.coords["position"].fields, k) for k in "xyz"} + ).drop_coords("position") + ) + return sc.concat(pieces, dim="pixel").squeeze() def instrument_view( @@ -20,152 +34,40 @@ def instrument_view( dim: str | None = None, pixel_size: float | sc.Variable | None = None, autoscale: bool = False, - **kwargs: Any, -) -> Box: - """ - Three-dimensional visualization of the DREAM instrument. - The instrument view is capable of slicing the input data with a slider widget along - a dimension (e.g. ``tof``) by using the ``dim`` argument. - It will also generate checkboxes to hide/show the different modules that make up - the DREAM detectors. + **kwargs, +) -> FigureLike: + from plopp.widgets import ClippingPlanes, SliceWidget, ToggleTool, VBar, slice_dims - Parameters - ---------- - data: - Data to visualize. The data can be a single detector module (``DataArray``), - or a group of detector modules (``dict`` or ``DataGroup``). - The data must contain a ``position`` coordinate. - dim: - Dimension to use for the slider. No slider will be shown if this is None. - pixel_size: - Size of the pixels. - autoscale: - If ``True``, the color scale will be automatically adjusted to the data as it - gets updated. This can be somewhat expensive with many pixels, so it is set to - ``False`` by default. - **kwargs: - Additional arguments are forwarded to the scatter3d figure - (see https://scipp.github.io/plopp/generated/plopp.scatter3d.html). - """ - from plopp.widgets import Box - - if dim and isinstance(data, sc.DataArray) and dim in data.dims[:-1]: - data = data.transpose([d for d in data.dims if d != dim] + [dim]) - - if dim and isinstance(data, sc.DataGroup): - data = data.copy(deep=False) - for k, v in data.items(): - if dim in v.dims[:-1]: - data[k] = v.transpose([d for d in v.dims if d != dim] + [dim]) - - view = InstrumentView( - data, dim=dim, pixel_size=pixel_size, autoscale=autoscale, **kwargs - ) - return Box(view.children) + data = _to_data_array(data, dim) - -def _to_data_group(data: sc.DataArray | sc.DataGroup | dict) -> sc.DataGroup: - if isinstance(data, sc.DataArray): - data = sc.DataGroup({data.name or "data": data}) - elif isinstance(data, dict): - data = sc.DataGroup(data) - return data - - -@pp.node -def _pre_process(da: sc.DataArray, dim: str) -> sc.DataArray: - dims = list(da.dims) if dim is not None: - dims.remove(dim) - out = da.flatten(dims=dims, to="pixel") - sel = sc.isfinite(out.coords["position"]) - return out[sel] - - -class InstrumentView: - """Instrument view for DREAM.""" - - def __init__( - self, - data: sc.DataArray | sc.DataGroup | dict, - dim: str | None = None, - pixel_size: float | sc.Variable | None = None, + slider = SliceWidget(data, dims=[dim]) + slider.controls[dim].slider.layout = {"width": "600px"} + slider_node = pp.widget_node(slider) + to_scatter = slice_dims(data, slider_node) + else: + to_scatter = pp.Node(data) + + kwargs.setdefault('cbar', True) + fig = pp.scatter3dfigure( + to_scatter, + x="x", + y="y", + z="z", + pixel_size=1.0 * sc.Unit("cm") if pixel_size is None else pixel_size, + autoscale=autoscale, **kwargs, - ): - from plopp.widgets import SliceWidget, slice_dims - - self.data = _to_data_group(data) - self.pre_process_nodes = { - key: _pre_process(da, dim) for key, da in self.data.items() - } - - self.children = [] - - if dim is not None: - self.slider = SliceWidget(next(iter(self.data.values())), dims=[dim]) - self.slider.controls[dim].slider.layout = {"width": "600px"} - self.slider_node = pp.widget_node(self.slider) - self.slice_nodes = { - key: slice_dims(n, self.slider_node) - for key, n in self.pre_process_nodes.items() - } - to_scatter = self.slice_nodes - self.children.append(self.slider) - else: - self.slice_nodes = self.pre_process_nodes - to_scatter = self.pre_process_nodes - - kwargs.setdefault('cbar', True) - self.fig = pp.scatter3d( - to_scatter, - pos="position", - pixel_size=1.0 * sc.Unit("cm") if pixel_size is None else pixel_size, - **kwargs, - ) - - self.children.insert(0, self.fig) - - if len(self.data) > 1: - self._add_module_control() - - def _add_module_control(self): - import ipywidgets as ipw - - self.cutting_tool = self.fig.bottom_bar[0] - self._node_backup = list(self.cutting_tool._original_nodes) - self.artist_mapping = dict( - zip(self.data.keys(), self.fig.artists.keys(), strict=True) - ) - self.checkboxes = { - key: ipw.Checkbox( - value=True, - description=f"{escape(key)}", - indent=False, - layout={"width": "initial"}, - ) - for key in self.data - } + ) - self.modules_widget = ipw.HBox( - [ - ipw.HTML(value="Modules:     "), - *self.checkboxes.values(), - ] - ) - for key, ch in self.checkboxes.items(): - ch.key = key - ch.observe(self._check_visibility, names="value") - self.children.insert(0, self.modules_widget) + clip_planes = ClippingPlanes(fig) + fig.toolbar['cut3d'] = ToggleTool( + callback=clip_planes.toggle_visibility, + icon='layer-group', + tooltip='Hide/show spatial cutting tool', + ) + widgets = [clip_planes] + if dim is not None: + widgets.append(slider) + fig.bottom_bar.add(VBar(widgets)) - def _check_visibility(self, _): - active_nodes = [ - node_id - for key, node_id in self.artist_mapping.items() - if self.checkboxes[key].value - ] - for n in self._node_backup: - self.fig.artists[n.id].points.visible = n.id in active_nodes - self.cutting_tool._original_nodes = [ - n for n in self._node_backup if n.id in active_nodes - ] - self.cutting_tool.update_state() + return fig From 8a42003660e25cc7b5c2daf38e56cc1ecd23098e Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 3 Feb 2026 22:22:06 +0100 Subject: [PATCH 2/9] use range slider --- src/ess/dream/instrument_view.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/ess/dream/instrument_view.py b/src/ess/dream/instrument_view.py index 245ba22b..0b684a35 100644 --- a/src/ess/dream/instrument_view.py +++ b/src/ess/dream/instrument_view.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) +# Copyright (c) 2026 Scipp contributors (https://github.com/scipp) import plopp as pp import scipp as sc @@ -29,6 +29,13 @@ def _to_data_array( return sc.concat(pieces, dim="pixel").squeeze() +def _slice_dim( + da: sc.DataArray, slice_params: dict[str, tuple[int, int]] +) -> sc.DataArray: + (params,) = slice_params.items() + return da[params[0], params[1][0] : params[1][1]].sum(params[0]) + + def instrument_view( data: sc.DataArray | sc.DataGroup | dict, dim: str | None = None, @@ -36,15 +43,15 @@ def instrument_view( autoscale: bool = False, **kwargs, ) -> FigureLike: - from plopp.widgets import ClippingPlanes, SliceWidget, ToggleTool, VBar, slice_dims + from plopp.widgets import ClippingPlanes, RangeSliceWidget, ToggleTool, VBar data = _to_data_array(data, dim) if dim is not None: - slider = SliceWidget(data, dims=[dim]) + slider = RangeSliceWidget(data, dims=[dim]) slider.controls[dim].slider.layout = {"width": "600px"} slider_node = pp.widget_node(slider) - to_scatter = slice_dims(data, slider_node) + to_scatter = pp.Node(_slice_dim, da=data, slice_params=slider_node) else: to_scatter = pp.Node(data) From bdc881e5b6158baaafef548cbe942184191f86a6 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 3 Feb 2026 23:08:05 +0100 Subject: [PATCH 3/9] start with full range and add potential addition to interact with value based cut --- src/ess/dream/instrument_view.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/ess/dream/instrument_view.py b/src/ess/dream/instrument_view.py index 0b684a35..3d61fc94 100644 --- a/src/ess/dream/instrument_view.py +++ b/src/ess/dream/instrument_view.py @@ -48,10 +48,13 @@ def instrument_view( data = _to_data_array(data, dim) if dim is not None: - slider = RangeSliceWidget(data, dims=[dim]) - slider.controls[dim].slider.layout = {"width": "600px"} - slider_node = pp.widget_node(slider) + range_slicer = RangeSliceWidget(data, dims=[dim]) + slider = range_slicer.controls[dim].slider + slider.value = 0, data.sizes[dim] + slider.layout = {"width": "600px"} + slider_node = pp.widget_node(range_slicer) to_scatter = pp.Node(_slice_dim, da=data, slice_params=slider_node) + else: to_scatter = pp.Node(data) @@ -74,7 +77,14 @@ def instrument_view( ) widgets = [clip_planes] if dim is not None: - widgets.append(slider) + widgets.append(range_slicer) + + # def _maybe_update_value_cut(_): + # if any(cut._direction == "v" for cut in clip_planes.cuts): + # clip_planes.update_state() + + # range_slicer.observe(_maybe_update_value_cut, names='value') + fig.bottom_bar.add(VBar(widgets)) return fig From 758918d700264ea4ea5ece7137e9a46c16d4e94c Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 4 Feb 2026 16:00:36 +0100 Subject: [PATCH 4/9] fix instrument view tests --- tests/dream/instrument_view_test.py | 45 ++++++++--------------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/tests/dream/instrument_view_test.py b/tests/dream/instrument_view_test.py index fe4583d6..8c37e7aa 100644 --- a/tests/dream/instrument_view_test.py +++ b/tests/dream/instrument_view_test.py @@ -5,7 +5,7 @@ import pytest import scipp as sc -from ess.dream.instrument_view import InstrumentView +from ess.dream import instrument_view @pytest.fixture @@ -35,54 +35,33 @@ def fake_instrument_data(modules=('bank1', 'bank2', 'bank3', 'bank4', 'bank5')): def test_instrument_view_all_modules(fake_instrument_data): - view = InstrumentView(fake_instrument_data, dim='tof') - assert hasattr(view, 'checkboxes') - assert hasattr(view, 'fig') - assert hasattr(view, 'slider') + instrument_view(fake_instrument_data, dim='tof') def test_instrument_view_one_module(fake_instrument_data): - view = InstrumentView(fake_instrument_data['bank1'], dim='tof') - assert not hasattr(view, 'checkboxes') - assert hasattr(view, 'fig') - assert hasattr(view, 'slider') + instrument_view(fake_instrument_data['bank1'], dim='tof') def test_instrument_view_slider_not_last_dim_dataarray(fake_instrument_data): da = fake_instrument_data['bank1'] da = da.transpose(('tof', *(set(da.dims) - {'tof'}))) - InstrumentView(da, dim='tof') + instrument_view(da, dim='tof') def test_instrument_view_slider_not_last_dim_datagroup(fake_instrument_data): - da = fake_instrument_data + dg = fake_instrument_data # Add extra dim so that not all entries in the group have the same set of dimensions - da['bank2'] = da['bank2'].broadcast( - dims=[*da['bank2'].dims, 'extra_dimension'], shape=[*da['bank2'].shape, 1] + dg['bank2'] = dg['bank2'].broadcast( + dims=[*dg['bank2'].dims, 'extra_dimension'], shape=[*dg['bank2'].shape, 1] ) - for k, v in da.items(): - da[k] = v.transpose(('tof', *(set(v.dims) - {'tof'}))) - InstrumentView(da, dim='tof') + for k, v in dg.items(): + dg[k] = v.transpose(('tof', *(set(v.dims) - {'tof'}))) + instrument_view(dg, dim='tof') def test_instrument_view_no_tof_slider(fake_instrument_data): - view = InstrumentView(fake_instrument_data.sum('tof')) - assert hasattr(view, 'checkboxes') - assert hasattr(view, 'fig') - assert not hasattr(view, 'slider') + instrument_view(fake_instrument_data.sum('tof')) def test_instrument_view_one_module_no_tof_slider(fake_instrument_data): - view = InstrumentView(fake_instrument_data['bank3'].sum('tof')) - assert not hasattr(view, 'checkboxes') - assert hasattr(view, 'fig') - assert not hasattr(view, 'slider') - - -def test_instrument_view_toggle_module(fake_instrument_data): - view = InstrumentView(fake_instrument_data, dim='tof') - for name in fake_instrument_data: - key = view.artist_mapping[name] - assert view.fig.artists[key].points.visible - view.checkboxes[name].value = False - assert not view.fig.artists[key].points.visible + instrument_view(fake_instrument_data['bank3'].sum('tof')) From 49e58ac79cdd1fb03864120026e8ee7cff8b2747 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 4 Feb 2026 17:27:21 +0100 Subject: [PATCH 5/9] add switcher between range slider and intslider --- src/ess/dream/instrument_view.py | 45 ++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/ess/dream/instrument_view.py b/src/ess/dream/instrument_view.py index 3d61fc94..de2ca912 100644 --- a/src/ess/dream/instrument_view.py +++ b/src/ess/dream/instrument_view.py @@ -43,15 +43,50 @@ def instrument_view( autoscale: bool = False, **kwargs, ) -> FigureLike: - from plopp.widgets import ClippingPlanes, RangeSliceWidget, ToggleTool, VBar + from ipywidgets import ToggleButton + from plopp.widgets import ( + ClippingPlanes, + HBar, + RangeSliceWidget, + SliceWidget, + ToggleTool, + VBar, + ) data = _to_data_array(data, dim) if dim is not None: + int_slicer = SliceWidget(data, dims=[dim]) + int_slider = int_slicer.controls[dim].slider + int_slider.layout = {"width": "600px"} + range_slicer = RangeSliceWidget(data, dims=[dim]) - slider = range_slicer.controls[dim].slider - slider.value = 0, data.sizes[dim] - slider.layout = {"width": "600px"} + range_slider = range_slicer.controls[dim].slider + range_slider.value = 0, data.sizes[dim] + range_slider.layout = {"width": "600px"} + + def move_range(change): + range_slider.value = (change["new"], change["new"] + 1) + + int_slider.observe(move_range, names='value') + slider_toggler = ToggleButton( + value=True, + tooltip="Toggle slicing mode (range/single-slice)", + icon="arrows-h", + layout={"width": "37px"}, + ) + + slicing_container = HBar([range_slicer, slider_toggler]) + + def toggle_slider_mode(change): + if change["new"]: + slicing_container.children = [range_slicer, slider_toggler] + else: + int_slider.value = int(0.5 * sum(range_slider.value)) + slicing_container.children = [int_slicer, slider_toggler] + + slider_toggler.observe(toggle_slider_mode, names='value') + slider_node = pp.widget_node(range_slicer) to_scatter = pp.Node(_slice_dim, da=data, slice_params=slider_node) @@ -77,7 +112,7 @@ def instrument_view( ) widgets = [clip_planes] if dim is not None: - widgets.append(range_slicer) + widgets.append(slicing_container) # def _maybe_update_value_cut(_): # if any(cut._direction == "v" for cut in clip_planes.cuts): From 0387d6e27c68ef4ba67e58a48e5fbf4c474dc3d7 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 4 Feb 2026 23:46:49 +0100 Subject: [PATCH 6/9] use togglebuttons for selecting slider type --- src/ess/dream/instrument_view.py | 39 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/ess/dream/instrument_view.py b/src/ess/dream/instrument_view.py index de2ca912..afa3dd8b 100644 --- a/src/ess/dream/instrument_view.py +++ b/src/ess/dream/instrument_view.py @@ -33,7 +33,7 @@ def _slice_dim( da: sc.DataArray, slice_params: dict[str, tuple[int, int]] ) -> sc.DataArray: (params,) = slice_params.items() - return da[params[0], params[1][0] : params[1][1]].sum(params[0]) + return da[params[0], params[1][0] : params[1][1] + 1].sum(params[0]) def instrument_view( @@ -43,9 +43,9 @@ def instrument_view( autoscale: bool = False, **kwargs, ) -> FigureLike: - from ipywidgets import ToggleButton + from ipywidgets import ToggleButtons from plopp.widgets import ( - ClippingPlanes, + ClippingManager, HBar, RangeSliceWidget, SliceWidget, @@ -58,32 +58,31 @@ def instrument_view( if dim is not None: int_slicer = SliceWidget(data, dims=[dim]) int_slider = int_slicer.controls[dim].slider - int_slider.layout = {"width": "600px"} + int_slider.layout = {"width": "550px"} range_slicer = RangeSliceWidget(data, dims=[dim]) range_slider = range_slicer.controls[dim].slider range_slider.value = 0, data.sizes[dim] - range_slider.layout = {"width": "600px"} + range_slider.layout = {"width": "550px"} def move_range(change): - range_slider.value = (change["new"], change["new"] + 1) + range_slider.value = (change["new"], change["new"]) int_slider.observe(move_range, names='value') - slider_toggler = ToggleButton( - value=True, - tooltip="Toggle slicing mode (range/single-slice)", - icon="arrows-h", - layout={"width": "37px"}, + slider_toggler = ToggleButtons( + options=["o-o", "-o-"], + tooltips=['Range slider', 'Single slice slider'], + style={"button_width": "39px"}, ) - slicing_container = HBar([range_slicer, slider_toggler]) + slicing_container = HBar([slider_toggler, range_slicer]) def toggle_slider_mode(change): - if change["new"]: - slicing_container.children = [range_slicer, slider_toggler] + if change["new"] == "o-o": + slicing_container.children = [slider_toggler, range_slicer] else: int_slider.value = int(0.5 * sum(range_slider.value)) - slicing_container.children = [int_slicer, slider_toggler] + slicing_container.children = [slider_toggler, int_slicer] slider_toggler.observe(toggle_slider_mode, names='value') @@ -104,7 +103,7 @@ def toggle_slider_mode(change): **kwargs, ) - clip_planes = ClippingPlanes(fig) + clip_planes = ClippingManager(fig) fig.toolbar['cut3d'] = ToggleTool( callback=clip_planes.toggle_visibility, icon='layer-group', @@ -114,11 +113,11 @@ def toggle_slider_mode(change): if dim is not None: widgets.append(slicing_container) - # def _maybe_update_value_cut(_): - # if any(cut._direction == "v" for cut in clip_planes.cuts): - # clip_planes.update_state() + def _maybe_update_value_cut(_): + if any(cut.kind == "v" for cut in clip_planes.cuts): + clip_planes.update_state() - # range_slicer.observe(_maybe_update_value_cut, names='value') + range_slicer.observe(_maybe_update_value_cut, names='value') fig.bottom_bar.add(VBar(widgets)) From 5282196faae76e740a696bc2ecc2bb5bb7c48a2a Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 4 Feb 2026 23:47:32 +0100 Subject: [PATCH 7/9] bump plopp version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 477e2609..27e7da38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "essreduce>=25.12.1", "graphviz", "numpy>=2", - "plopp>=25.07.0", + "plopp>=26.2.0", "pythreejs>=2.4.1", "sciline>=25.04.1", "scipp>=25.11.0", From 6ce2f5f88192dbb2fc1acc8c5c380d87175479d1 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 4 Feb 2026 23:50:35 +0100 Subject: [PATCH 8/9] update deps --- requirements/base.in | 4 ++-- requirements/base.txt | 20 ++++++++++---------- requirements/basetest.txt | 8 ++++---- requirements/ci.txt | 2 +- requirements/dev.txt | 6 +++--- requirements/docs.txt | 8 ++++---- requirements/mypy.txt | 2 +- requirements/nightly.in | 2 +- requirements/nightly.txt | 13 ++++++------- 9 files changed, 32 insertions(+), 33 deletions(-) diff --git a/requirements/base.in b/requirements/base.in index 29b2fedd..98ed6dca 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -5,8 +5,8 @@ dask>=2022.1.0 essreduce>=25.12.1 graphviz -numpy>=1.25 -plopp>=25.07.0 +numpy>=2 +plopp>=26.2.0 pythreejs>=2.4.1 sciline>=25.04.1 scipp>=25.11.0 diff --git a/requirements/base.txt b/requirements/base.txt index 92395650..0c0cca58 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:40cd03cd4a9e9632db0eebb7d094cb1a55638372 +# SHA1:303c70637d6536773db66ff40b4306cd86aee2c0 # # This file was generated by pip-compile-multi. # To update, run: @@ -27,7 +27,7 @@ cyclebane==24.10.0 # via sciline cycler==0.12.1 # via matplotlib -dask==2026.1.1 +dask==2026.1.2 # via -r base.in decorator==5.2.1 # via ipython @@ -35,7 +35,7 @@ dnspython==2.8.0 # via email-validator email-validator==2.3.0 # via scippneutron -essreduce==26.1.1 +essreduce==26.2.0 # via -r base.in executing==2.2.1 # via stack-data @@ -59,7 +59,7 @@ importlib-metadata==8.7.1 # via dask ipydatawidgets==4.3.5 # via pythreejs -ipython==9.9.0 +ipython==9.10.0 # via ipywidgets ipython-pygments-lexers==1.1.1 # via ipython @@ -97,7 +97,7 @@ ncrystal-python==4.2.12 # via ncrystal networkx==3.6.1 # via cyclebane -numpy==2.4.1 +numpy==2.4.2 # via # -r base.in # ase @@ -127,12 +127,12 @@ pillow==12.1.0 # via matplotlib platformdirs==4.5.1 # via pooch -plopp==25.11.0 +plopp==26.2.0 # via # -r base.in # scippneutron # tof -pooch==1.8.2 +pooch==1.9.0 # via tof prompt-toolkit==3.0.52 # via ipython @@ -164,7 +164,7 @@ sciline==25.11.1 # via # -r base.in # essreduce -scipp==26.1.1 +scipp==26.2.0 # via # -r base.in # essreduce @@ -175,7 +175,7 @@ scippneutron==25.11.2 # via # -r base.in # essreduce -scippnexus==26.1.0 +scippnexus==26.1.1 # via # -r base.in # essreduce @@ -219,7 +219,7 @@ typing-inspection==0.4.2 # via pydantic urllib3==2.6.3 # via requests -wcwidth==0.3.0 +wcwidth==0.5.3 # via prompt-toolkit widgetsnbextension==4.0.15 # via ipywidgets diff --git a/requirements/basetest.txt b/requirements/basetest.txt index ae57aaaf..131f3b2d 100644 --- a/requirements/basetest.txt +++ b/requirements/basetest.txt @@ -21,7 +21,7 @@ idna==3.11 # via requests iniconfig==2.3.0 # via pytest -ipython==9.9.0 +ipython==9.10.0 # via ipywidgets ipython-pygments-lexers==1.1.1 # via ipython @@ -33,7 +33,7 @@ jupyterlab-widgets==3.0.16 # via ipywidgets matplotlib-inline==0.2.1 # via ipython -numpy==2.4.1 +numpy==2.4.2 # via pandas packaging==26.0 # via @@ -49,7 +49,7 @@ platformdirs==4.5.1 # via pooch pluggy==1.6.0 # via pytest -pooch==1.8.2 +pooch==1.9.0 # via -r basetest.in prompt-toolkit==3.0.52 # via ipython @@ -81,7 +81,7 @@ typing-extensions==4.15.0 # via ipython urllib3==2.6.3 # via requests -wcwidth==0.3.0 +wcwidth==0.5.3 # via prompt-toolkit widgetsnbextension==4.0.15 # via ipywidgets diff --git a/requirements/ci.txt b/requirements/ci.txt index f90a93aa..20ed8164 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -5,7 +5,7 @@ # # requirements upgrade # -cachetools==6.2.4 +cachetools==7.0.0 # via tox certifi==2026.1.4 # via requests diff --git a/requirements/dev.txt b/requirements/dev.txt index fbd2ac32..ab0b7715 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -26,7 +26,7 @@ async-lru==2.1.0 # via jupyterlab cffi==2.0.0 # via argon2-cffi-bindings -copier==9.11.2 +copier==9.11.3 # via -r dev.in dunamai==1.25.0 # via copier @@ -65,7 +65,7 @@ jupyter-server==2.17.0 # notebook-shim jupyter-server-terminals==0.5.4 # via jupyter-server -jupyterlab==4.5.2 +jupyterlab==4.5.3 # via -r dev.in jupyterlab-server==2.28.0 # via jupyterlab @@ -115,7 +115,7 @@ webcolors==25.10.0 # via jsonschema websocket-client==1.9.0 # via jupyter-server -wheel==0.46.2 +wheel==0.46.3 # via pip-tools # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/docs.txt b/requirements/docs.txt index 0a9d2b36..5e177870 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -16,7 +16,7 @@ attrs==25.4.0 # referencing autodoc-pydantic==2.2.0 # via -r docs.in -babel==2.17.0 +babel==2.18.0 # via # pydata-sphinx-theme # sphinx @@ -26,7 +26,7 @@ beautifulsoup4==4.14.3 # pydata-sphinx-theme bleach[css]==6.3.0 # via nbconvert -debugpy==1.8.19 +debugpy==1.8.20 # via ipykernel defusedxml==0.7.1 # via nbconvert @@ -89,7 +89,7 @@ myst-parser==5.0.0 # via -r docs.in nbclient==0.10.4 # via nbconvert -nbconvert==7.16.6 +nbconvert==7.17.0 # via nbsphinx nbformat==5.10.4 # via @@ -104,7 +104,7 @@ pandas==3.0.0 # via -r docs.in pandocfilters==1.5.1 # via nbconvert -psutil==7.2.1 +psutil==7.2.2 # via ipykernel pyarrow==23.0.0 # via -r docs.in diff --git a/requirements/mypy.txt b/requirements/mypy.txt index 2ee61c57..0f22723e 100644 --- a/requirements/mypy.txt +++ b/requirements/mypy.txt @@ -12,5 +12,5 @@ mypy==1.19.1 # via -r mypy.in mypy-extensions==1.1.0 # via mypy -pathspec==1.0.3 +pathspec==1.0.4 # via mypy diff --git a/requirements/nightly.in b/requirements/nightly.in index e22fd4d3..d709f8aa 100644 --- a/requirements/nightly.in +++ b/requirements/nightly.in @@ -3,7 +3,7 @@ # The following was generated by 'tox -e deps', DO NOT EDIT MANUALLY! dask>=2022.1.0 graphviz -numpy>=1.25 +numpy>=2 pythreejs>=2.4.1 ncrystal[cif]>=4.1.0 spglib!=2.7 diff --git a/requirements/nightly.txt b/requirements/nightly.txt index f69bb181..f5e1b18a 100644 --- a/requirements/nightly.txt +++ b/requirements/nightly.txt @@ -1,4 +1,4 @@ -# SHA1:c7ea57c5e953bfa839536ce478aea6b0964cf25a +# SHA1:02c4ca6025e8a899b9987d08704c722ee69e177a # # This file was generated by pip-compile-multi. # To update, run: @@ -30,7 +30,7 @@ cyclebane==24.10.0 # via sciline cycler==0.12.1 # via matplotlib -dask==2026.1.1 +dask==2026.1.2 # via -r nightly.in decorator==5.2.1 # via ipython @@ -64,7 +64,7 @@ iniconfig==2.3.0 # via pytest ipydatawidgets==4.3.5 # via pythreejs -ipython==9.9.0 +ipython==9.10.0 # via ipywidgets ipython-pygments-lexers==1.1.1 # via ipython @@ -103,12 +103,11 @@ ncrystal-python==4.2.12 # via ncrystal networkx==3.6.1 # via cyclebane -numpy==2.3.5 +numpy==2.4.2 # via # -r nightly.in # ase # contourpy - # essreduce # h5py # ipydatawidgets # matplotlib @@ -145,7 +144,7 @@ plopp @ git+https://github.com/scipp/plopp@main # tof pluggy==1.6.0 # via pytest -pooch==1.8.2 +pooch==1.9.0 # via # -r nightly.in # tof @@ -238,7 +237,7 @@ typing-inspection==0.4.2 # via pydantic urllib3==2.6.3 # via requests -wcwidth==0.3.0 +wcwidth==0.5.3 # via prompt-toolkit widgetsnbextension==4.0.15 # via ipywidgets From b78b2f2953b3619272450fd5f0bf85b52396c75a Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 4 Feb 2026 23:58:15 +0100 Subject: [PATCH 9/9] restor docstring --- src/ess/dream/instrument_view.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/ess/dream/instrument_view.py b/src/ess/dream/instrument_view.py index afa3dd8b..db7d0d84 100644 --- a/src/ess/dream/instrument_view.py +++ b/src/ess/dream/instrument_view.py @@ -43,6 +43,32 @@ def instrument_view( autoscale: bool = False, **kwargs, ) -> FigureLike: + """ + Three-dimensional visualization of the DREAM instrument. + The instrument view is capable of slicing the input data with a slider widget along + a dimension (e.g. ``tof``) by using the ``dim`` argument. + + Use the clipping tool to create cuts in 3d space, as well as according to data + values. + + Parameters + ---------- + data: + Data to visualize. The data can be a single detector module (``DataArray``), + or a group of detector modules (``dict`` or ``DataGroup``). + The data must contain a ``position`` coordinate. + dim: + Dimension to use for the slider. No slider will be shown if this is None. + pixel_size: + Size of the pixels. + autoscale: + If ``True``, the color scale will be automatically adjusted to the data as it + gets updated. This can be somewhat expensive with many pixels, so it is set to + ``False`` by default. + **kwargs: + Additional arguments are forwarded to the scatter3d figure + (see https://scipp.github.io/plopp/generated/plopp.scatter3d.html). + """ from ipywidgets import ToggleButtons from plopp.widgets import ( ClippingManager,