Source code for ess.dream.instrument_view

# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)

import plopp as pp
import scipp as sc
from plopp.core.typing import FigureLike


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(list(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 _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] + 1].sum(params[0])


[docs] def instrument_view( data: sc.DataArray | sc.DataGroup | dict, dim: str | None = None, pixel_size: float | sc.Variable | None = None, 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, 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.value = int_slider.min int_slider.layout = {"width": "42em"} range_slicer = RangeSliceWidget(data, dims=[dim]) range_slider = range_slicer.controls[dim].slider range_slider.value = 0, data.sizes[dim] range_slider.layout = {"width": "42em"} def move_range(change): range_slider.value = (change["new"], change["new"]) int_slider.observe(move_range, names='value') slider_toggler = ToggleButtons( options=["o-o", "-o-"], tooltips=['Range slider', 'Single slice slider'], style={"button_width": "3.2em"}, ) slicing_container = HBar([slider_toggler, range_slicer]) def toggle_slider_mode(change): 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 = [slider_toggler, int_slicer] 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) 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, ) clip_planes = ClippingManager(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(slicing_container) 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') fig.bottom_bar.add(VBar(widgets)) return fig