diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index 8a77ed8c..a8bd1167 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -29,4 +29,29 @@ uncertainty external.powgen -``` \ No newline at end of file +``` + +## ESSdream + +### Top-level functions + +```{eval-rst} +.. currentmodule:: ess.dream + +.. autosummary:: + :toctree: ../generated/functions + + instrument_view +``` + +### Submodules + +```{eval-rst} +.. autosummary:: + :toctree: ../generated/modules + :template: module-template.rst + :recursive: + + data + io +``` diff --git a/docs/conf.py b/docs/conf.py index f7cf1425..f601b83e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -130,7 +130,7 @@ 'Thumbs.db', '.DS_Store', '**.ipynb_checkpoints', - 'examples/preprocess_files.ipynb', + 'user-guide/sns-instruments/preprocess_files.ipynb', ] # The name of the Pygments (syntax highlighting) style to use. diff --git a/docs/examples/index.md b/docs/examples/index.md deleted file mode 100644 index b1c64c0a..00000000 --- a/docs/examples/index.md +++ /dev/null @@ -1,9 +0,0 @@ -# Examples - -```{toctree} ---- -maxdepth: 2 ---- - -POWGEN_data_reduction -``` diff --git a/docs/index.md b/docs/index.md index 5339fce8..61b5b626 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,7 @@ hidden: --- -examples/index +user-guide/index api-reference/index developer/index about/index diff --git a/docs/user-guide/dream/dream-instrument-view.ipynb b/docs/user-guide/dream/dream-instrument-view.ipynb new file mode 100644 index 00000000..ee3b7fbe --- /dev/null +++ b/docs/user-guide/dream/dream-instrument-view.ipynb @@ -0,0 +1,254 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4852fe41-3f9a-4cdb-8aba-ff7c7f198e6c", + "metadata": {}, + "source": [ + "# DREAM instrument view\n", + "\n", + "This notebook is a simple example of how to use the instrument view for the DREAM instrument.\n", + "\n", + "- The DREAM-specific instrument view is capable of slicing the data with a slider widget along a dimension (e.g. `tof`) by using the `dim` argument.\n", + "- There are also checkboxes to hide/show the different elements that make up the DREAM detectors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f416595-83b4-44d1-b506-9ba73bf0786e", + "metadata": {}, + "outputs": [], + "source": [ + "import scipp as sc\n", + "from ess import dream" + ] + }, + { + "cell_type": "markdown", + "id": "228a23dc-17f2-4273-a4c9-3e3276db8c54", + "metadata": {}, + "source": [ + "## Load the data\n", + "\n", + "We load a dataset from a Geant4 simulation (stored as a `.csv` file).\n", + "In each detector bank, the data is organised by `wire`, `strip`, `module`, `segment`, and `counter`.\n", + "The high resolution detector also has an additional `sector` dimension." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ef8f35b-815f-436a-80e5-cbe4b3172b58", + "metadata": {}, + "outputs": [], + "source": [ + "dg = dream.io.load_geant4_csv(\n", + " dream.data.get_path('data_dream_with_sectors.csv.zip')\n", + ")\n", + "dg = dg['instrument'] # Extract the instrument data\n", + "\n", + "# Construct the pixel positions from event positions\n", + "for da in dg.values():\n", + " da.coords['position'] = da.bins.coords['position'].bins.mean()\n", + "\n", + "dg" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8325797-5651-43c1-b601-3db6d4348758", + "metadata": { + "editable": true, + "nbsphinx": "hidden", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "# Only plot half of the pixels to reduce html docs size\n", + "dg = dg['counter', 0]" + ] + }, + { + "cell_type": "markdown", + "id": "49595486-ab4e-4662-86f7-d301aedcf974", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Full instrument view\n", + "\n", + "We first histogram the data along the time-of-flight (`tof`) dimension,\n", + "making sure the same bins are used for all elements:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e85a8364-e0a1-4c10-8cae-c873f297e651", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "tof_edges = sc.linspace('tof', 1.0e7, 1.0e8, 51, unit='ns', dtype=int)\n", + "data = dg.hist(tof=tof_edges)" + ] + }, + { + "cell_type": "markdown", + "id": "d08ca911-b1a4-4f17-ba1e-355971531ffe", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We now use the `instrument_view` function to show the 3D view of the instrument pixels,\n", + "specifying that we wish to have a slider along the `tof` dimension:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43f9ffbc-6bf5-4407-b3ad-5d1626efc43d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "full_view = dream.instrument_view(data, dim='tof')\n", + "full_view" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7030d56-a375-47b5-898c-28fd06c2f361", + "metadata": { + "editable": true, + "nbsphinx": "hidden", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "full_view[2].controls['tof']['slider'].value = 35" + ] + }, + { + "cell_type": "markdown", + "id": "c0b29ebf-21ff-4385-bf8b-0e4fa14dfaf9", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Note that it is possible to use any dimension for the slider instead of `tof`, such as `wavelength` (if present in the data).\n", + "\n", + "## Displaying individual detector elements\n", + "\n", + "It is also possible to view a single detector element, selecting e.g. `mantle` from the original data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b417011e-0d12-4287-91d5-c1fb6ecc7cac", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "mantle_view = dream.instrument_view(dg['mantle'].hist(wavelength=50), dim='wavelength')\n", + "mantle_view" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "299ec404-fb18-4533-ad96-e23bf8ba24d6", + "metadata": { + "editable": true, + "nbsphinx": "hidden", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "mantle_view[1].controls['wavelength']['slider'].value = 43" + ] + }, + { + "cell_type": "markdown", + "id": "1df1aa56-5251-4555-a4f4-283145747198", + "metadata": {}, + "source": [ + "The instrument view is designed to be flexible in terms of what it accepts as input.\n", + "This means that you can easily inspect, for example, a single module by using the usual slicing notation for data arrays.\n", + "\n", + "Below, we display the first module in the backward end-cap detector:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c86e491-3564-498d-9232-39485f5b95d7", + "metadata": {}, + "outputs": [], + "source": [ + "dream.instrument_view(dg['endcap_backward']['module', 0].hist(tof=1))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/user-guide/dream/index.md b/docs/user-guide/dream/index.md new file mode 100644 index 00000000..9fa523e4 --- /dev/null +++ b/docs/user-guide/dream/index.md @@ -0,0 +1,9 @@ +# DREAM + +```{toctree} +--- +maxdepth: 1 +--- + +dream-instrument-view +``` diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md new file mode 100644 index 00000000..8798abe5 --- /dev/null +++ b/docs/user-guide/index.md @@ -0,0 +1,10 @@ +# User guide + +```{toctree} +--- +maxdepth: 1 +--- + +dream/index +sns-instruments/index +``` diff --git a/docs/examples/POWGEN_data_reduction.ipynb b/docs/user-guide/sns-instruments/POWGEN_data_reduction.ipynb similarity index 100% rename from docs/examples/POWGEN_data_reduction.ipynb rename to docs/user-guide/sns-instruments/POWGEN_data_reduction.ipynb diff --git a/docs/user-guide/sns-instruments/index.md b/docs/user-guide/sns-instruments/index.md new file mode 100644 index 00000000..6c1321e2 --- /dev/null +++ b/docs/user-guide/sns-instruments/index.md @@ -0,0 +1,12 @@ +# SNS instruments + +This page contains guides for reducing data from Spallation Neutron Source (SNS) instruments. +Those instruments are not fully supported by ESSdiffraction and these pages may be removed in the future. + +```{toctree} +--- +maxdepth: 1 +--- + +POWGEN_data_reduction +``` diff --git a/docs/examples/preprocess_files.ipynb b/docs/user-guide/sns-instruments/preprocess_files.ipynb similarity index 100% rename from docs/examples/preprocess_files.ipynb rename to docs/user-guide/sns-instruments/preprocess_files.ipynb diff --git a/requirements/base.txt b/requirements/base.txt index 2a4e3e5b..08e038fc 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -23,13 +23,13 @@ contourpy==1.2.0 # via matplotlib cycler==0.12.1 # via matplotlib -dask==2024.2.0 +dask==2024.2.1 # via -r base.in decorator==5.1.1 # via ipython executing==2.0.1 # via stack-data -fonttools==4.48.1 +fonttools==4.49.0 # via matplotlib fsspec==2024.2.0 # via dask @@ -41,7 +41,7 @@ h5py==3.10.0 # scippnexus idna==3.6 # via requests -importlib-metadata==7.0.1 +importlib-metadata==7.0.2 # via dask ipydatawidgets==4.3.5 # via pythreejs @@ -61,7 +61,7 @@ kiwisolver==1.4.5 # via matplotlib locket==1.0.0 # via partd -matplotlib==3.8.2 +matplotlib==3.8.3 # via plopp matplotlib-inline==0.1.6 # via ipython @@ -75,7 +75,7 @@ numpy==1.26.4 # scipp # scippneutron # scipy -packaging==23.2 +packaging==24.0 # via # dask # matplotlib @@ -92,9 +92,9 @@ pillow==10.2.0 # via matplotlib platformdirs==4.2.0 # via pooch -plopp==24.1.1 +plopp==24.2.0 # via -r base.in -pooch==1.8.0 +pooch==1.8.1 # via scippneutron prompt-toolkit==3.0.36 # via @@ -106,9 +106,9 @@ pure-eval==0.2.2 # via stack-data pygments==2.17.2 # via ipython -pyparsing==3.1.1 +pyparsing==3.1.2 # via matplotlib -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 # via # matplotlib # scippnexus @@ -118,7 +118,7 @@ pyyaml==6.0.1 # via dask requests==2.31.0 # via pooch -sciline==24.2.0 +sciline==24.2.1 # via -r base.in scipp==24.2.0 # via @@ -127,7 +127,7 @@ scipp==24.2.0 # scippnexus scippneutron==24.1.0 # via -r base.in -scippnexus==23.12.1 +scippnexus==24.3.1 # via # -r base.in # scippneutron @@ -155,7 +155,7 @@ traitlets==5.14.1 # traittypes traittypes==0.2.1 # via ipydatawidgets -urllib3==2.2.0 +urllib3==2.2.1 # via requests wcwidth==0.2.13 # via prompt-toolkit diff --git a/requirements/basetest.txt b/requirements/basetest.txt index d12fb793..9066abeb 100644 --- a/requirements/basetest.txt +++ b/requirements/basetest.txt @@ -11,15 +11,15 @@ iniconfig==2.0.0 # via pytest numpy==1.26.4 # via pandas -packaging==23.2 +packaging==24.0 # via pytest -pandas==2.2.0 +pandas==2.2.1 # via -r basetest.in pluggy==1.4.0 # via pytest -pytest==8.0.0 +pytest==8.1.1 # via -r basetest.in -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 # via pandas pytz==2024.1 # via pandas diff --git a/requirements/ci.txt b/requirements/ci.txt index d6f215e2..2f27a0eb 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -5,7 +5,7 @@ # # pip-compile-multi # -cachetools==5.3.2 +cachetools==5.3.3 # via tox certifi==2024.2.2 # via requests @@ -23,11 +23,11 @@ filelock==3.13.1 # virtualenv gitdb==4.0.11 # via gitpython -gitpython==3.1.41 +gitpython==3.1.42 # via -r ci.in idna==3.6 # via requests -packaging==23.2 +packaging==24.0 # via # -r ci.in # pyproject-api @@ -48,9 +48,9 @@ tomli==2.0.1 # via # pyproject-api # tox -tox==4.12.1 +tox==4.14.1 # via -r ci.in -urllib3==2.2.0 +urllib3==2.2.1 # via requests -virtualenv==20.25.0 +virtualenv==20.25.1 # via tox diff --git a/requirements/dev.txt b/requirements/dev.txt index 7f65df8a..6a601813 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -14,7 +14,7 @@ -r wheels.txt annotated-types==0.6.0 # via pydantic -anyio==4.2.0 +anyio==4.3.0 # via # httpx # jupyter-server @@ -30,7 +30,7 @@ cffi==1.16.0 # via argon2-cffi-bindings copier==9.1.1 # via -r dev.in -dunamai==1.19.1 +dunamai==1.19.2 # via copier fqdn==1.5.1 # via jsonschema @@ -38,15 +38,15 @@ funcy==2.0 # via copier h11==0.14.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.4 # via httpx -httpx==0.26.0 +httpx==0.27.0 # via jupyterlab isoduration==20.11.0 # via jsonschema jinja2-ansible-filters==1.3.2 # via copier -json5==0.9.14 +json5==0.9.22 # via jupyterlab-server jsonpointer==2.4 # via jsonschema @@ -57,9 +57,9 @@ jsonschema[format-nongpl]==4.21.1 # nbformat jupyter-events==0.9.0 # via jupyter-server -jupyter-lsp==2.2.2 +jupyter-lsp==2.2.4 # via jupyterlab -jupyter-server==2.12.5 +jupyter-server==2.13.0 # via # jupyter-lsp # jupyterlab @@ -67,11 +67,11 @@ jupyter-server==2.12.5 # notebook-shim jupyter-server-terminals==0.5.2 # via jupyter-server -jupyterlab==4.1.1 +jupyterlab==4.1.4 # via -r dev.in -jupyterlab-server==2.25.2 +jupyterlab-server==2.25.3 # via jupyterlab -notebook-shim==0.2.3 +notebook-shim==0.2.4 # via jupyterlab overrides==7.7.0 # via jupyter-server @@ -79,17 +79,17 @@ pathspec==0.12.1 # via copier pip-compile-multi==2.6.3 # via -r dev.in -pip-tools==7.3.0 +pip-tools==7.4.1 # via pip-compile-multi plumbum==1.8.2 # via copier -prometheus-client==0.19.0 +prometheus-client==0.20.0 # via jupyter-server pycparser==2.21 # via cffi -pydantic==2.6.1 +pydantic==2.6.3 # via copier -pydantic-core==2.16.2 +pydantic-core==2.16.3 # via pydantic python-json-logger==2.0.7 # via jupyter-events @@ -107,7 +107,7 @@ rfc3986-validator==0.1.1 # jupyter-events send2trash==1.8.2 # via jupyter-server -sniffio==1.3.0 +sniffio==1.3.1 # via # anyio # httpx @@ -117,7 +117,7 @@ terminado==0.18.0 # jupyter-server-terminals toposort==1.10 # via pip-compile-multi -types-python-dateutil==2.8.19.20240106 +types-python-dateutil==2.8.19.20240311 # via arrow uri-template==1.3.0 # via jsonschema diff --git a/requirements/docs.in b/requirements/docs.in index d23bab31..813dd56f 100644 --- a/requirements/docs.in +++ b/requirements/docs.in @@ -4,8 +4,12 @@ ipympl ipython!=8.7.0 # Breaks syntax highlighting in Jupyter code cells. myst-parser nbsphinx +pandas pydata-sphinx-theme>=0.14 sphinx sphinx-autodoc-typehints sphinx-copybutton sphinx-design + +# needed by pandas < 3.0 +pyarrow diff --git a/requirements/docs.txt b/requirements/docs.txt index 903e9664..eb9f68dd 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,4 +1,4 @@ -# SHA1:c33ddd581d5b365fb1ff37b440d3f6363ca4f9ec +# SHA1:5644acd1b52f1f99fbca1349c29761b90702d03a # # This file is autogenerated by pip-compile-multi # To update, run: @@ -38,7 +38,7 @@ fastjsonschema==2.19.1 # via nbformat imagesize==1.4.1 # via sphinx -ipykernel==6.29.2 +ipykernel==6.29.3 # via -r docs.in ipympl==0.9.3 # via -r docs.in @@ -85,7 +85,7 @@ myst-parser==2.0.0 # via -r docs.in nbclient==0.9.0 # via nbconvert -nbconvert==7.16.0 +nbconvert==7.16.2 # via nbsphinx nbformat==5.9.2 # via @@ -96,12 +96,18 @@ nbsphinx==0.9.3 # via -r docs.in nest-asyncio==1.6.0 # via ipykernel +pandas==2.2.1 + # via -r docs.in pandocfilters==1.5.1 # via nbconvert psutil==5.9.8 # via ipykernel +pyarrow==15.0.1 + # via -r docs.in pydata-sphinx-theme==0.15.2 # via -r docs.in +pytz==2024.1 + # via pandas pyzmq==25.1.2 # via # ipykernel @@ -151,8 +157,10 @@ tornado==6.4 # via # ipykernel # jupyter-client -typing-extensions==4.9.0 +typing-extensions==4.10.0 # via pydata-sphinx-theme +tzdata==2024.1 + # via pandas webencodings==0.5.1 # via # bleach diff --git a/requirements/mypy.txt b/requirements/mypy.txt index ac285686..066d33ac 100644 --- a/requirements/mypy.txt +++ b/requirements/mypy.txt @@ -6,9 +6,9 @@ # pip-compile-multi # -r test.txt -mypy==1.8.0 +mypy==1.9.0 # via -r mypy.in mypy-extensions==1.0.0 # via mypy -typing-extensions==4.9.0 +typing-extensions==4.10.0 # via mypy diff --git a/requirements/nightly.txt b/requirements/nightly.txt index dfea5b05..d5ea5fa9 100644 --- a/requirements/nightly.txt +++ b/requirements/nightly.txt @@ -22,13 +22,13 @@ contourpy==1.2.0 # via matplotlib cycler==0.12.1 # via matplotlib -dask==2024.2.0 +dask==2024.2.1 # via -r nightly.in decorator==5.1.1 # via ipython executing==2.0.1 # via stack-data -fonttools==4.48.1 +fonttools==4.49.0 # via matplotlib fsspec==2024.2.0 # via dask @@ -40,11 +40,11 @@ h5py==3.10.0 # scippnexus idna==3.6 # via requests -importlib-metadata==7.0.1 +importlib-metadata==7.0.2 # via dask ipydatawidgets==4.3.5 # via pythreejs -ipython==8.21.0 +ipython==8.22.2 # via ipywidgets ipywidgets==8.1.2 # via @@ -58,7 +58,7 @@ kiwisolver==1.4.5 # via matplotlib locket==1.0.0 # via partd -matplotlib==3.8.2 +matplotlib==3.8.3 # via plopp matplotlib-inline==0.1.6 # via ipython @@ -74,7 +74,7 @@ platformdirs==4.2.0 # via pooch plopp @ git+https://github.com/scipp/plopp@main # via -r nightly.in -pooch==1.8.0 +pooch==1.8.1 # via scippneutron prompt-toolkit==3.0.43 # via ipython @@ -84,7 +84,7 @@ pure-eval==0.2.2 # via stack-data pygments==2.17.2 # via ipython -pyparsing==3.1.1 +pyparsing==3.1.2 # via matplotlib pythreejs==2.4.2 # via -r nightly.in @@ -125,7 +125,7 @@ traitlets==5.14.1 # traittypes traittypes==0.2.1 # via ipydatawidgets -urllib3==2.2.0 +urllib3==2.2.1 # via requests wcwidth==0.2.13 # via prompt-toolkit diff --git a/requirements/static.txt b/requirements/static.txt index 7499ff55..0e3af8ac 100644 --- a/requirements/static.txt +++ b/requirements/static.txt @@ -11,17 +11,17 @@ distlib==0.3.8 # via virtualenv filelock==3.13.1 # via virtualenv -identify==2.5.34 +identify==2.5.35 # via pre-commit nodeenv==1.8.0 # via pre-commit platformdirs==4.2.0 # via virtualenv -pre-commit==3.6.1 +pre-commit==3.6.2 # via -r static.in pyyaml==6.0.1 # via pre-commit -virtualenv==20.25.0 +virtualenv==20.25.1 # via pre-commit # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/wheels.txt b/requirements/wheels.txt index 2e33cfa3..23d6d310 100644 --- a/requirements/wheels.txt +++ b/requirements/wheels.txt @@ -5,9 +5,9 @@ # # pip-compile-multi # -build==1.0.3 +build==1.1.1 # via -r wheels.in -packaging==23.2 +packaging==24.0 # via build pyproject-hooks==1.0.0 # via build diff --git a/src/ess/dream/__init__.py b/src/ess/dream/__init__.py index 99d88e5e..5203fc71 100644 --- a/src/ess/dream/__init__.py +++ b/src/ess/dream/__init__.py @@ -7,6 +7,7 @@ import importlib.metadata from . import data +from .instrument_view import instrument_view from .io import fold_nexus_detectors, load_geant4_csv, load_nexus try: @@ -17,8 +18,9 @@ del importlib __all__ = [ - "data", - "fold_nexus_detectors", - "load_geant4_csv", - "load_nexus", + 'data', + 'fold_nexus_detectors', + 'instrument_view', + 'load_geant4_csv', + 'load_nexus', ] diff --git a/src/ess/dream/data.py b/src/ess/dream/data.py index 806facdc..9e11dfba 100644 --- a/src/ess/dream/data.py +++ b/src/ess/dream/data.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) +"""Data for tests and documentation with DREAM.""" + _version = '1' __all__ = ['get_path'] @@ -9,7 +11,7 @@ def _make_pooch(): import pooch return pooch.create( - path=pooch.os_cache('ess/powgen'), + path=pooch.os_cache('ess/dream'), env='ESS_DATA_DIR', base_url='https://public.esss.dk/groups/scipp/ess/dream/{version}/', version=_version, diff --git a/src/ess/dream/instrument_view.py b/src/ess/dream/instrument_view.py new file mode 100644 index 00000000..06294535 --- /dev/null +++ b/src/ess/dream/instrument_view.py @@ -0,0 +1,156 @@ +# 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, Optional, Union + +import plopp as pp +import scipp as sc + +if TYPE_CHECKING: + try: + from plopp.widgets import Box + except ModuleNotFoundError: + Box = object + + +def instrument_view( + data: Union[sc.DataArray, sc.DataGroup, dict], + dim: Optional[str] = None, + pixel_size: Optional[Union[float, sc.Variable]] = None, + **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. + + 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. + **kwargs: + Additional arguments are forwarded to the scatter3d figure + (see https://scipp.github.io/plopp/generated/plopp.scatter3d.html). + """ + from plopp.widgets import Box + + view = InstrumentView(data, dim=dim, pixel_size=pixel_size, **kwargs) + return Box(view.children) + + +def _to_data_group(data: Union[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: Union[sc.DataArray, sc.DataGroup, dict], + dim: Optional[str] = None, + pixel_size: Optional[Union[float, sc.Variable]] = None, + **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 + + self.scatter = 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.scatter) + + if len(self.data) > 1: + self._add_module_control() + + def _add_module_control(self): + import ipywidgets as ipw + + self.fig = self.scatter[0] + self.cutting_tool = self.scatter[1] + self.artist_mapping = { + name: key for name, key in zip(self.data.keys(), self.fig.artists.keys()) + } + 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:     ")] + + list(self.checkboxes.values()) + ) + for key, ch in self.checkboxes.items(): + ch.key = key + ch.observe(self._check_visibility, names='value') + self.cutting_tool.cut_x.button.observe(self._check_visibility, names="value") + self.cutting_tool.cut_y.button.observe(self._check_visibility, names="value") + self.cutting_tool.cut_z.button.observe(self._check_visibility, names="value") + self.children.insert(0, self.modules_widget) + + def _check_visibility(self, _): + # Note that this brute force method of looping over all artists is not optimal + # but it is non-invasive in the sense that it does not require changes to the + # plopp code. If performance becomes an issue, we will consider a different + # approach. + for name, ch in self.checkboxes.items(): + key = self.artist_mapping[name] + val = ch.value + self.fig.artists[key].points.visible = val + for c in "xyz": + cut_nodes = getattr(self.cutting_tool, f'cut_{c}').select_nodes + if key in cut_nodes: + self.fig.artists[cut_nodes[key].id].points.visible = val diff --git a/tests/dream/instrument_view_test.py b/tests/dream/instrument_view_test.py new file mode 100644 index 00000000..d67bde6c --- /dev/null +++ b/tests/dream/instrument_view_test.py @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) + +import numpy as np +import pytest +import scipp as sc + +from ess.dream.instrument_view import InstrumentView + + +@pytest.fixture +def fake_instrument_data(modules=('bank1', 'bank2', 'bank3', 'bank4', 'bank5')): + rng = np.random.default_rng() + + out = {} + npix = 300 + ntof = 100 + locations = range(len(modules)) + for name, loc in zip(modules, locations): + position = rng.normal(loc=[0, 0, loc], scale=[0.2, 0.2, 0.05], size=[npix, 3]) + tof = sc.linspace('tof', 0, 1.0e5, ntof + 1, unit='us') + values = rng.normal(loc=5.0e4, scale=2.0e4, size=[npix, ntof]) + vec = sc.vectors(dims=['pixel'], unit='m', values=position) + out[name] = sc.DataArray( + data=sc.array(dims=['pixel', 'tof'], values=values, unit='counts'), + coords={ + 'position': vec, + 'x': vec.fields.x, + 'y': vec.fields.y, + 'z': vec.fields.z, + 'tof': tof, + }, + ) + return sc.DataGroup(out) + + +def test_instrument_view_all_modules(fake_instrument_data): + view = InstrumentView(fake_instrument_data, dim='tof') + assert hasattr(view, 'checkboxes') + assert hasattr(view, 'scatter') + assert hasattr(view, 'slider') + + +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, 'scatter') + assert hasattr(view, 'slider') + + +def test_instrument_view_no_tof_slider(fake_instrument_data): + view = InstrumentView(fake_instrument_data.sum('tof')) + assert hasattr(view, 'checkboxes') + assert hasattr(view, 'scatter') + assert not hasattr(view, 'slider') + + +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, 'scatter') + 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