diff --git a/docs/user-guide/loki/index.md b/docs/user-guide/loki/index.md index 8a26222d..78e160c3 100644 --- a/docs/user-guide/loki/index.md +++ b/docs/user-guide/loki/index.md @@ -7,5 +7,7 @@ maxdepth: 1 loki-direct-beam loki-iofq +loki-reduction-ess workflow-widget-loki +loki-make-tof-lookup-table ``` diff --git a/docs/user-guide/loki/loki-make-tof-lookup-table.ipynb b/docs/user-guide/loki/loki-make-tof-lookup-table.ipynb new file mode 100644 index 00000000..d692ef7a --- /dev/null +++ b/docs/user-guide/loki/loki-make-tof-lookup-table.ipynb @@ -0,0 +1,125 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Create a time-of-flight lookup table for LoKI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import scipp as sc\n", + "from ess.reduce import time_of_flight\n", + "from ess.reduce.nexus.types import AnyRun" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Setting up the workflow\n", + "\n", + "Note here that for now, we have no chopper in the beamline.\n", + "This should be added in the next iteration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "source_position = sc.vector([0, 0, 0], unit='m')\n", + "\n", + "wf = time_of_flight.TofLookupTableWorkflow()\n", + "wf[time_of_flight.DiskChoppers[AnyRun]] = {}\n", + "wf[time_of_flight.SourcePosition] = source_position\n", + "wf[time_of_flight.NumberOfSimulatedNeutrons] = 200_000 # Increase this number for more reliable results\n", + "wf[time_of_flight.SimulationSeed] = 1234\n", + "wf[time_of_flight.PulseStride] = 1\n", + "wf[time_of_flight.LtotalRange] = sc.scalar(9.0, unit=\"m\"), sc.scalar(35.0, unit=\"m\")\n", + "wf[time_of_flight.DistanceResolution] = sc.scalar(0.1, unit=\"m\")\n", + "wf[time_of_flight.TimeResolution] = sc.scalar(250.0, unit='us')\n", + "wf[time_of_flight.LookupTableRelativeErrorThreshold] = 1.0" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## Compute the table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "table = wf.compute(time_of_flight.TimeOfFlightLookupTable)\n", + "table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "table.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## Save to file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "# Write to file\n", + "table.save_hdf5('loki-tof-lookup-table-no-choppers.h5')" + ] + } + ], + "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.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/user-guide/loki/loki-reduction-ess.ipynb b/docs/user-guide/loki/loki-reduction-ess.ipynb new file mode 100644 index 00000000..23c7cd49 --- /dev/null +++ b/docs/user-guide/loki/loki-reduction-ess.ipynb @@ -0,0 +1,163 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Loki workflow\n", + "\n", + "A short experimental notebook to illustrate how to set-up and run the reduction workflow for Loki @ ESS." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import scipp as sc\n", + "import ess.loki.data # noqa: F401\n", + "from ess import loki\n", + "from ess.sans.types import *" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Workflow setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "wf = loki.LokiWorkflow()\n", + "\n", + "# Set detector bank name: in this case there is only one bank\n", + "wf[NeXusDetectorName] = \"loki_detector_0\"\n", + "\n", + "# Wavelength and Q binning parameters\n", + "wf[WavelengthBins] = sc.linspace(\"wavelength\", 1.0, 13.0, 201, unit=\"angstrom\")\n", + "wf[QBins] = sc.linspace(dim=\"Q\", start=0.01, stop=0.3, num=101, unit=\"1/angstrom\")\n", + "\n", + "# Other parameters\n", + "wf[CorrectForGravity] = True\n", + "wf[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound\n", + "wf[ReturnEvents] = False\n", + "wf[BeamCenter] = sc.vector([0.0, 0.0, 0.0], unit=\"m\")\n", + "wf[DirectBeam] = None\n", + "wf[DetectorMasks] = {}\n", + "wf[TimeOfFlightLookupTableFilename] = loki.data.loki_tof_lookup_table_no_choppers()\n", + "\n", + "# Use a small dummy file for testing.\n", + "# TODO: We currently use the same file for all runs; this should be updated\n", + "# once we have files from an actual run.\n", + "wf[Filename[SampleRun]] = loki.data.loki_coda_file_one_event()\n", + "wf[Filename[EmptyBeamRun]] = loki.data.loki_coda_file_one_event()\n", + "wf[Filename[TransmissionRun[SampleRun]]] = loki.data.loki_coda_file_one_event()\n", + "\n", + "# Visualize the workflow\n", + "wf.visualize(IntensityQ[SampleRun], graph_attr={'rankdir': 'LR'})" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "### Compute $I(Q)$\n", + "\n", + "We compute the `IntensityQ` for the sample run.\n", + "\n", + "**Note:** since we are currently using the same file for sample, empty-beam, and transmission runs,\n", + "the final results are meaningless (NaNs in all Q bins). However, this should not prevent the workflow\n", + "from running." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "wf.compute(IntensityQ[SampleRun])" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Map over detector banks\n", + "\n", + "Loki has 9 detectors banks, and in principle we would want to run the same workflow on all banks\n", + "(treating all pixels in the same way).\n", + "\n", + "To compute a reduced result for all banks, we map the workflow over all bank names:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "bank_ids = list(range(9))\n", + "bank_names = [f'loki_detector_{i}' for i in bank_ids]\n", + "param_table = pd.DataFrame({NeXusDetectorName: bank_names}, index=bank_ids).rename_axis(\n", + " index='bank_id'\n", + ")\n", + "param_table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "mapped = wf.map(param_table)\n", + "\n", + "results = sciline.compute_mapped(mapped, IntensityQ[SampleRun])\n", + "\n", + "# Convert to a DataGroup for better notebook visualization\n", + "sc.DataGroup({str(k): v for k, v in results.items()})" + ] + } + ], + "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.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index b29ab68e..02e994f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ requires-python = ">=3.11" dependencies = [ "dask>=2022.1.0", "graphviz>=0.20", - "essreduce>=25.11.0", + "essreduce>=25.12.1", "numpy>=1.26.4", "pandas>=2.1.2", "plopp>=25.03.0", diff --git a/requirements/base.in b/requirements/base.in index e770d5d4..cfe621de 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -4,7 +4,7 @@ # The following was generated by 'tox -e deps', DO NOT EDIT MANUALLY! dask>=2022.1.0 graphviz>=0.20 -essreduce>=25.11.0 +essreduce>=25.12.1 numpy>=1.26.4 pandas>=2.1.2 plopp>=25.03.0 diff --git a/requirements/base.txt b/requirements/base.txt index 7d2c944d..2eafc9a9 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:6adb8082982c7c5bb1806abc8670bfd2cb6375cd +# SHA1:bec19874e7866d34bee81eb0bac3393397378b4f # # This file was generated by pip-compile-multi. # To update, run: @@ -7,9 +7,9 @@ # annotated-types==0.7.0 # via pydantic -asttokens==3.0.0 +asttokens==3.0.1 # via stack-data -click==8.3.0 +click==8.3.1 # via dask cloudpickle==3.1.2 # via dask @@ -21,7 +21,7 @@ cyclebane==24.10.0 # via sciline cycler==0.12.1 # via matplotlib -dask==2025.10.0 +dask==2025.11.0 # via -r base.in decorator==5.2.1 # via ipython @@ -29,13 +29,13 @@ dnspython==2.8.0 # via email-validator email-validator==2.3.0 # via scippneutron -essreduce==25.11.0 +essreduce==25.12.1 # via -r base.in executing==2.2.1 # via stack-data -fonttools==4.60.1 +fonttools==4.61.0 # via matplotlib -fsspec==2025.10.0 +fsspec==2025.12.0 # via dask graphviz==0.21 # via -r base.in @@ -49,7 +49,7 @@ importlib-metadata==8.7.0 # via dask ipydatawidgets==4.3.5 # via pythreejs -ipython==9.6.0 +ipython==9.8.0 # via ipywidgets ipython-pygments-lexers==1.1.1 # via ipython @@ -77,9 +77,9 @@ matplotlib-inline==0.2.1 # via ipython mpltoolbox==25.10.0 # via scippneutron -networkx==3.5 +networkx==3.6.1 # via cyclebane -numpy==2.3.4 +numpy==2.3.5 # via # -r base.in # contourpy @@ -106,7 +106,7 @@ pexpect==4.9.0 # via ipython pillow==12.0.0 # via matplotlib -plopp==25.10.0 +plopp==25.11.0 # via # -r base.in # scippneutron @@ -116,9 +116,9 @@ ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 # via stack-data -pydantic==2.12.3 +pydantic==2.12.5 # via scippneutron -pydantic-core==2.41.4 +pydantic-core==2.41.5 # via pydantic pygments==2.19.2 # via @@ -131,7 +131,6 @@ python-dateutil==2.9.0.post0 # matplotlib # pandas # scippneutron - # scippnexus pythreejs==2.4.2 # via -r base.in pytz==2025.2 @@ -148,11 +147,11 @@ scipp==25.11.0 # essreduce # scippneutron # scippnexus -scippneutron==25.7.0 +scippneutron==25.11.2 # via # -r base.in # essreduce -scippnexus==25.6.0 +scippnexus==25.11.0 # via # -r base.in # essreduce diff --git a/requirements/basetest.txt b/requirements/basetest.txt index 98697db8..c71bd32e 100644 --- a/requirements/basetest.txt +++ b/requirements/basetest.txt @@ -5,7 +5,7 @@ # # requirements upgrade # -certifi==2025.10.5 +certifi==2025.11.12 # via requests charset-normalizer==3.4.4 # via requests @@ -13,13 +13,13 @@ idna==3.11 # via requests iniconfig==2.3.0 # via pytest -numpy==2.3.4 +numpy==2.3.5 # via scipy packaging==25.0 # via # pooch # pytest -platformdirs==4.5.0 +platformdirs==4.5.1 # via pooch pluggy==1.6.0 # via pytest @@ -27,11 +27,11 @@ pooch==1.8.2 # via -r basetest.in pygments==2.19.2 # via pytest -pytest==8.4.2 +pytest==9.0.2 # via -r basetest.in requests==2.32.5 # via pooch scipy==1.16.3 # via -r basetest.in -urllib3==2.5.0 +urllib3==2.6.1 # via requests diff --git a/requirements/ci.txt b/requirements/ci.txt index 5bc02f40..5e597a7b 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -5,9 +5,9 @@ # # requirements upgrade # -cachetools==6.2.1 +cachetools==6.2.2 # via tox -certifi==2025.10.5 +certifi==2025.11.12 # via requests chardet==5.2.0 # via tox @@ -32,7 +32,7 @@ packaging==25.0 # -r ci.in # pyproject-api # tox -platformdirs==4.5.0 +platformdirs==4.5.1 # via # tox # virtualenv @@ -46,7 +46,7 @@ smmap==5.0.2 # via gitdb tox==4.32.0 # via -r ci.in -urllib3==2.5.0 +urllib3==2.6.1 # via requests virtualenv==20.35.4 # via tox diff --git a/requirements/dev.txt b/requirements/dev.txt index 4afeb88a..eadaf0b4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -12,7 +12,7 @@ -r static.txt -r test.txt -r wheels.txt -anyio==4.11.0 +anyio==4.12.0 # via # httpx # jupyter-server @@ -26,7 +26,7 @@ async-lru==2.0.5 # via jupyterlab cffi==2.0.0 # via argon2-cffi-bindings -copier==9.10.3 +copier==9.11.0 # 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.3 # via jupyter-server -jupyterlab==4.4.10 +jupyterlab==4.5.0 # via -r dev.in jupyterlab-server==2.28.0 # via jupyterlab @@ -77,7 +77,7 @@ overrides==7.7.0 # via jupyter-server pip-compile-multi==3.2.2 # via -r dev.in -pip-tools==7.5.1 +pip-tools==7.5.2 # via pip-compile-multi plumbum==1.10.0 # via copier @@ -101,8 +101,6 @@ rfc3987-syntax==1.1.0 # via jsonschema send2trash==1.8.3 # via jupyter-server -sniffio==1.3.1 - # via anyio terminado==0.18.1 # via # jupyter-server diff --git a/requirements/docs.in b/requirements/docs.in index 18c780bd..c419d20d 100644 --- a/requirements/docs.in +++ b/requirements/docs.in @@ -10,3 +10,4 @@ sphinx sphinx-autodoc-typehints sphinx-copybutton sphinx-design +tof diff --git a/requirements/docs.txt b/requirements/docs.txt index 75f10a5e..8567c125 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,4 +1,4 @@ -# SHA1:146b34fbbe620c54529c722d6969f2c7b408a8b2 +# SHA1:fb8690cf652b557fa725b3e1de9864a559158477 # # This file was generated by pip-compile-multi. # To update, run: @@ -20,17 +20,17 @@ babel==2.17.0 # via # pydata-sphinx-theme # sphinx -beautifulsoup4==4.14.2 +beautifulsoup4==4.14.3 # via # nbconvert # pydata-sphinx-theme bleach[css]==6.3.0 # via nbconvert -certifi==2025.10.5 +certifi==2025.11.12 # via requests charset-normalizer==3.4.4 # via requests -debugpy==1.8.17 +debugpy==1.8.18 # via ipykernel defusedxml==0.7.1 # via nbconvert @@ -56,7 +56,7 @@ jsonschema==4.25.1 # via nbformat jsonschema-specifications==2025.9.1 # via jsonschema -jupyter-client==8.6.3 +jupyter-client==8.7.0 # via # ipykernel # nbclient @@ -94,21 +94,23 @@ nbformat==5.10.4 # nbclient # nbconvert # nbsphinx -nbsphinx==0.9.7 +nbsphinx==0.9.8 # via -r docs.in nest-asyncio==1.6.0 # via ipykernel pandocfilters==1.5.1 # via nbconvert -platformdirs==4.5.0 +platformdirs==4.5.1 # via # jupyter-core # pooch pooch==1.8.2 - # via -r docs.in + # via + # -r docs.in + # tof psutil==7.1.3 # via ipykernel -pydantic-settings==2.11.0 +pydantic-settings==2.12.0 # via autodoc-pydantic pydata-sphinx-theme==0.16.1 # via -r docs.in @@ -126,7 +128,9 @@ requests==2.32.5 # via # pooch # sphinx -rpds-py==0.28.0 +roman-numerals-py==3.1.0 + # via sphinx +rpds-py==0.30.0 # via # jsonschema # referencing @@ -134,7 +138,7 @@ snowballstemmer==3.0.1 # via sphinx soupsieve==2.8 # via beautifulsoup4 -sphinx==8.1.3 +sphinx==8.2.3 # via # -r docs.in # autodoc-pydantic @@ -144,7 +148,7 @@ sphinx==8.1.3 # sphinx-autodoc-typehints # sphinx-copybutton # sphinx-design -sphinx-autodoc-typehints==3.0.1 +sphinx-autodoc-typehints==3.5.2 # via -r docs.in sphinx-copybutton==0.5.2 # via -r docs.in @@ -164,11 +168,13 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx tinycss2==1.4.0 # via bleach +tof==25.12.1 + # via -r docs.in tornado==6.5.2 # via # ipykernel # jupyter-client -urllib3==2.5.0 +urllib3==2.6.1 # via requests webencodings==0.5.1 # via diff --git a/requirements/mypy.txt b/requirements/mypy.txt index 088d0c28..67ba9295 100644 --- a/requirements/mypy.txt +++ b/requirements/mypy.txt @@ -6,7 +6,9 @@ # requirements upgrade # -r test.txt -mypy==1.18.2 +librt==0.7.3 + # via mypy +mypy==1.19.0 # via -r mypy.in mypy-extensions==1.1.0 # via mypy diff --git a/requirements/nightly.txt b/requirements/nightly.txt index 03e9bcf1..7e01df9f 100644 --- a/requirements/nightly.txt +++ b/requirements/nightly.txt @@ -10,13 +10,13 @@ annotated-types==0.7.0 # via pydantic -asttokens==3.0.0 +asttokens==3.0.1 # via stack-data -certifi==2025.10.5 +certifi==2025.11.12 # via requests charset-normalizer==3.4.4 # via requests -click==8.3.0 +click==8.3.1 # via dask cloudpickle==3.1.2 # via dask @@ -28,7 +28,7 @@ cyclebane==24.10.0 # via sciline cycler==0.12.1 # via matplotlib -dask==2025.10.0 +dask==2025.11.0 # via -r nightly.in decorator==5.2.1 # via ipython @@ -40,9 +40,9 @@ essreduce @ git+https://github.com/scipp/essreduce@main # via -r nightly.in executing==2.2.1 # via stack-data -fonttools==4.60.1 +fonttools==4.61.0 # via matplotlib -fsspec==2025.10.0 +fsspec==2025.12.0 # via dask graphviz==0.21 # via -r nightly.in @@ -60,7 +60,7 @@ iniconfig==2.3.0 # via pytest ipydatawidgets==4.3.5 # via pythreejs -ipython==9.6.0 +ipython==9.8.0 # via ipywidgets ipython-pygments-lexers==1.1.1 # via ipython @@ -88,9 +88,9 @@ matplotlib-inline==0.2.1 # via ipython mpltoolbox==25.10.0 # via scippneutron -networkx==3.5 +networkx==3.6.1 # via cyclebane -numpy==2.3.4 +numpy==2.4.0rc1 # via # -r nightly.in # contourpy @@ -109,7 +109,7 @@ packaging==25.0 # matplotlib # pooch # pytest -pandas==2.3.3 +pandas==3.0.0rc0 # via -r nightly.in parso==0.8.5 # via jedi @@ -119,7 +119,7 @@ pexpect==4.9.0 # via ipython pillow==12.0.0 # via matplotlib -platformdirs==4.5.0 +platformdirs==4.5.1 # via pooch plopp @ git+https://github.com/scipp/plopp@main # via @@ -135,18 +135,18 @@ ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 # via stack-data -pydantic==2.12.3 +pydantic==2.12.5 # via scippneutron -pydantic-core==2.41.4 +pydantic-core==2.41.5 # via pydantic pygments==2.19.2 # via # ipython # ipython-pygments-lexers # pytest -pyparsing==3.3.0a1 +pyparsing==3.3.0b1 # via matplotlib -pytest==8.4.2 +pytest==9.0.2 # via -r nightly.in python-dateutil==2.9.0.post0 # via @@ -155,8 +155,6 @@ python-dateutil==2.9.0.post0 # scippneutron pythreejs==2.4.2 # via -r nightly.in -pytz==2025.2 - # via pandas pyyaml==6.0.3 # via dask requests==2.32.5 @@ -180,7 +178,7 @@ scippnexus @ git+https://github.com/scipp/scippnexus@main # -r nightly.in # essreduce # scippneutron -scipy==1.16.3 +scipy==1.17.0rc1 # via # -r nightly.in # scippneutron @@ -212,7 +210,7 @@ typing-inspection==0.4.2 # via pydantic tzdata==2025.2 # via pandas -urllib3==2.5.0 +urllib3==2.6.1 # via requests wcwidth==0.2.14 # via prompt-toolkit diff --git a/requirements/static.txt b/requirements/static.txt index 991a8921..2a110d83 100644 --- a/requirements/static.txt +++ b/requirements/static.txt @@ -5,7 +5,7 @@ # # requirements upgrade # -cfgv==3.4.0 +cfgv==3.5.0 # via pre-commit distlib==0.4.0 # via virtualenv @@ -15,9 +15,9 @@ identify==2.6.15 # via pre-commit nodeenv==1.9.1 # via pre-commit -platformdirs==4.5.0 +platformdirs==4.5.1 # via virtualenv -pre-commit==4.3.0 +pre-commit==4.5.0 # via -r static.in pyyaml==6.0.3 # via pre-commit diff --git a/src/ess/isissans/general.py b/src/ess/isissans/general.py index c5955ba5..3b607324 100644 --- a/src/ess/isissans/general.py +++ b/src/ess/isissans/general.py @@ -30,7 +30,6 @@ RawMonitor, RunType, SampleRun, - ScatteringRunType, TofDetector, TofMonitor, Transmission, @@ -227,11 +226,11 @@ def dummy_assemble_monitor_data( def data_to_tof( - da: RawDetector[ScatteringRunType], -) -> TofDetector[ScatteringRunType]: + da: RawDetector[RunType], +) -> TofDetector[RunType]: """Dummy conversion of data to time-of-flight data. The data already has a time-of-flight coordinate.""" - return TofDetector[ScatteringRunType](da) + return TofDetector[RunType](da) def monitor_to_tof( @@ -250,7 +249,7 @@ def experiment_metadata(dg: LoadedFileContents[RunType]) -> Measurement[RunType] ) -def helium3_tube_detector_pixel_shape() -> DetectorPixelShape[ScatteringRunType]: +def helium3_tube_detector_pixel_shape() -> DetectorPixelShape[RunType]: # Pixel radius and length # found here: # https://github.com/mantidproject/mantid/blob/main/instrument/SANS2D_Definition_Tubes.xml @@ -277,9 +276,9 @@ def helium3_tube_detector_pixel_shape() -> DetectorPixelShape[ScatteringRunType] return pixel_shape -def lab_frame_transform() -> NeXusTransformation[snx.NXdetector, ScatteringRunType]: +def lab_frame_transform() -> NeXusTransformation[snx.NXdetector, RunType]: # Rotate +y to -x - return NeXusTransformation[snx.NXdetector, ScatteringRunType]( + return NeXusTransformation[snx.NXdetector, RunType]( sc.spatial.rotation(value=[0, 0, 1 / 2**0.5, 1 / 2**0.5]) ) diff --git a/src/ess/loki/__init__.py b/src/ess/loki/__init__.py index 73f18a23..c2b4f56a 100644 --- a/src/ess/loki/__init__.py +++ b/src/ess/loki/__init__.py @@ -4,7 +4,12 @@ import importlib.metadata from . import workflow -from .workflow import LokiAtLarmorWorkflow, default_parameters +from .workflow import ( + LokiAtLarmorWorkflow, + LokiWorkflow, + larmor_default_parameters, + loki_default_parameters, +) try: __version__ = importlib.metadata.version(__package__ or __name__) @@ -15,6 +20,8 @@ __all__ = [ 'LokiAtLarmorWorkflow', - 'default_parameters', + 'LokiWorkflow', + 'larmor_default_parameters', + 'loki_default_parameters', 'workflow', ] diff --git a/src/ess/loki/data.py b/src/ess/loki/data.py index a16c1c15..4d1d3435 100644 --- a/src/ess/loki/data.py +++ b/src/ess/loki/data.py @@ -51,6 +51,10 @@ 'mask_new_July2022.xml': 'md5:421b6dc9db74126ffbc5d88164d017b0', # Direct beam from LoKI@Larmor detector test experiment 'direct-beam-loki-all-pixels.h5': "md5:b85d7b486b312c5bb2a31d2bb6314f69", + # TOF lookup table without choppers + 'loki-tof-lookup-table-no-choppers.h5': 'md5:5b817466d3a07d4def12535d7317c044', + # CODA file with a single event per detector bank (for testing purposes) + 'loki-coda-one-event.hdf': 'md5:ab9dbef793fad2fca96210c3b55d60ce', }, version='2', ) @@ -159,3 +163,26 @@ def loki_tutorial_direct_beam_all_pixels() -> DirectBeamFilename: """File containing direct beam function computed using the direct beam iterations notebook, summing all pixels.""" return DirectBeamFilename(_registry.get_path('direct-beam-loki-all-pixels.h5')) + + +def loki_tof_lookup_table_no_choppers() -> Path: + """ + LoKI TOF lookup table without choppers. + This file is used to convert the neutron arrival time to time-of-flight. + + This table was computed using `Create a time-of-flight lookup table for LoKI + <../../loki/loki-make-tof-lookup-table.rst>`_ + with ``NumberOfSimulatedNeutrons = 5_000_000``. + """ + return _registry.get_path("loki-tof-lookup-table-no-choppers.h5") + + +def loki_coda_file_one_event() -> Path: + """ + LoKI CODA file with a single event per detector bank (for testing purposes). + The file was created from a CODA file 'loki_999999_00009928.hdf' but the event lists + were replaced by lists with a single event to reduce the size of the file. + The workflow should still be able to run, it will just produce results with NaNs + everywhere. + """ + return _registry.get_path("loki-coda-one-event.hdf") diff --git a/src/ess/loki/workflow.py b/src/ess/loki/workflow.py index 0f358a5a..92a8903b 100644 --- a/src/ess/loki/workflow.py +++ b/src/ess/loki/workflow.py @@ -34,7 +34,6 @@ RawMonitor, RunType, SampleRun, - ScatteringRunType, TofDetector, TofMonitor, Transmission, @@ -42,11 +41,20 @@ ) DETECTOR_BANK_SIZES = { - 'larmor_detector': {'layer': 4, 'tube': 32, 'straw': 7, 'pixel': 512} + 'larmor_detector': {'layer': 4, 'tube': -1, 'straw': 7, 'pixel': 512}, + 'loki_detector_0': {'layer': 4, 'tube': 56, 'straw': 7, 'pixel': -1}, + 'loki_detector_1': {'layer': 4, 'tube': 16, 'straw': 7, 'pixel': -1}, + 'loki_detector_2': {'layer': 4, 'tube': 12, 'straw': 7, 'pixel': -1}, + 'loki_detector_3': {'layer': 4, 'tube': 16, 'straw': 7, 'pixel': -1}, + 'loki_detector_4': {'layer': 4, 'tube': 12, 'straw': 7, 'pixel': -1}, + 'loki_detector_5': {'layer': 4, 'tube': 28, 'straw': 7, 'pixel': -1}, + 'loki_detector_6': {'layer': 4, 'tube': 32, 'straw': 7, 'pixel': -1}, + 'loki_detector_7': {'layer': 4, 'tube': 20, 'straw': 7, 'pixel': -1}, + 'loki_detector_8': {'layer': 4, 'tube': 32, 'straw': 7, 'pixel': -1}, } -def default_parameters() -> dict: +def larmor_default_parameters() -> dict: return { DetectorBankSizes: DETECTOR_BANK_SIZES, NeXusMonitorName[Incident]: 'monitor_1', @@ -56,7 +64,17 @@ def default_parameters() -> dict: } -def _convert_to_tof(da: sc.DataArray) -> sc.DataArray: +def loki_default_parameters() -> dict: + return { + DetectorBankSizes: DETECTOR_BANK_SIZES, + NeXusMonitorName[Incident]: 'beam_monitor_1', + NeXusMonitorName[Transmission]: 'beam_monitor_3', + PixelShapePath: 'pixel_shape', + NonBackgroundWavelengthRange: None, + } + + +def _larmor_convert_to_tof(da: sc.DataArray) -> sc.DataArray: event_time_offset = da.bins.coords['event_time_offset'] da = da.bins.drop_coords('event_time_offset') da.bins.coords['tof'] = event_time_offset @@ -65,23 +83,37 @@ def _convert_to_tof(da: sc.DataArray) -> sc.DataArray: return da -def data_to_tof( - da: RawDetector[ScatteringRunType], -) -> TofDetector[ScatteringRunType]: - return TofDetector[ScatteringRunType](_convert_to_tof(da)) +def larmor_data_to_tof(da: RawDetector[RunType]) -> TofDetector[RunType]: + """ + Compute time-of-flight coordinate for Loki detector data at Larmor. + This is different from the standard conversion from the GenericTofWorkflow because + the detector test was conducted as ISIS where the pulse has a different time + structure. + The conversion here is much simpler: the event_time_offset coordinate is directly + renamed as time-of-flight. + """ + return TofDetector[RunType](_larmor_convert_to_tof(da)) -def monitor_to_tof( +def larmor_monitor_to_tof( da: RawMonitor[RunType, MonitorType], ) -> TofMonitor[RunType, MonitorType]: - return TofMonitor[RunType, MonitorType](_convert_to_tof(da)) + """ + Compute time-of-flight coordinate for Loki monitor data at Larmor. + This is different from the standard conversion from the GenericTofWorkflow because + the detector test was conducted as ISIS where the pulse has a different time + structure. + The conversion here is much simpler: the event_time_offset coordinate is directly + renamed as time-of-flight. + """ + return TofMonitor[RunType, MonitorType](_larmor_convert_to_tof(da)) def detector_pixel_shape( - detector: NeXusComponent[snx.NXdetector, ScatteringRunType], + detector: NeXusComponent[snx.NXdetector, RunType], pixel_shape_path: PixelShapePath, -) -> DetectorPixelShape[ScatteringRunType]: - return DetectorPixelShape[ScatteringRunType](detector[pixel_shape_path]) +) -> DetectorPixelShape[RunType]: + return DetectorPixelShape[RunType](detector[pixel_shape_path]) def load_direct_beam(filename: DirectBeamFilename) -> DirectBeam: @@ -89,7 +121,7 @@ def load_direct_beam(filename: DirectBeamFilename) -> DirectBeam: return DirectBeam(sc.io.load_hdf5(filename)) -loki_providers = (detector_pixel_shape, data_to_tof, load_direct_beam, monitor_to_tof) +loki_providers = (detector_pixel_shape, load_direct_beam) @register_workflow @@ -110,8 +142,10 @@ def LokiAtLarmorWorkflow() -> sciline.Pipeline: workflow = sans.SansWorkflow() for provider in loki_providers: workflow.insert(provider) - for key, param in default_parameters().items(): + for key, param in larmor_default_parameters().items(): workflow[key] = param + workflow.insert(larmor_data_to_tof) + workflow.insert(larmor_monitor_to_tof) workflow.insert(read_xml_detector_masking) workflow[NeXusDetectorName] = 'larmor_detector' workflow.typical_outputs = typical_outputs @@ -136,3 +170,22 @@ def LokiAtLarmorTutorialWorkflow() -> sciline.Pipeline: workflow[Filename[EmptyBeamRun]] = str(data.loki_tutorial_run_60392()) workflow[BeamCenter] = sc.vector(value=[-0.02914868, -0.01816138, 0.0], unit='m') return workflow + + +@register_workflow +def LokiWorkflow() -> sciline.Pipeline: + """ + Workflow with default parameters for Loki. + + Returns + ------- + : + Loki workflow as a sciline.Pipeline + """ + workflow = sans.SansWorkflow() + for provider in loki_providers: + workflow.insert(provider) + for key, param in loki_default_parameters().items(): + workflow[key] = param + workflow.typical_outputs = typical_outputs + return workflow diff --git a/src/ess/sans/conversions.py b/src/ess/sans/conversions.py index d25cd143..3151163e 100644 --- a/src/ess/sans/conversions.py +++ b/src/ess/sans/conversions.py @@ -29,7 +29,6 @@ QDetector, QxyDetector, RunType, - ScatteringRunType, TofMonitor, UncertaintyBroadcastMode, WavelengthDetector, @@ -193,59 +192,59 @@ def monitor_to_wavelength( # for RawData, MaskedData, ... no reason to restrict necessarily. # Would we be fine with just choosing on option, or will this get in the way for users? def detector_to_wavelength( - detector: CorrectedDetector[ScatteringRunType, Numerator], - graph: ElasticCoordTransformGraph[ScatteringRunType], -) -> WavelengthDetector[ScatteringRunType, Numerator]: - return WavelengthDetector[ScatteringRunType, Numerator]( + detector: CorrectedDetector[RunType, Numerator], + graph: ElasticCoordTransformGraph[RunType], +) -> WavelengthDetector[RunType, Numerator]: + return WavelengthDetector[RunType, Numerator]( detector.transform_coords('wavelength', graph=graph, keep_inputs=False) ) def mask_wavelength_q( - da: BinnedQ[ScatteringRunType, Numerator], mask: WavelengthMask -) -> NormalizedQ[ScatteringRunType, Numerator]: + da: BinnedQ[RunType, Numerator], mask: WavelengthMask +) -> NormalizedQ[RunType, Numerator]: if mask is not None: da = mask_range(da, mask=mask) - return NormalizedQ[ScatteringRunType, Numerator](da) + return NormalizedQ[RunType, Numerator](da) def mask_wavelength_qxy( - da: BinnedQxQy[ScatteringRunType, Numerator], mask: WavelengthMask -) -> NormalizedQxQy[ScatteringRunType, Numerator]: + da: BinnedQxQy[RunType, Numerator], mask: WavelengthMask +) -> NormalizedQxQy[RunType, Numerator]: if mask is not None: da = mask_range(da, mask=mask) - return NormalizedQxQy[ScatteringRunType, Numerator](da) + return NormalizedQxQy[RunType, Numerator](da) def mask_and_scale_wavelength_q( - da: BinnedQ[ScatteringRunType, Denominator], + da: BinnedQ[RunType, Denominator], mask: WavelengthMask, - wavelength_term: MonitorTerm[ScatteringRunType], + wavelength_term: MonitorTerm[RunType], uncertainties: UncertaintyBroadcastMode, -) -> NormalizedQ[ScatteringRunType, Denominator]: +) -> NormalizedQ[RunType, Denominator]: da = da * broadcast_uncertainties(wavelength_term, prototype=da, mode=uncertainties) if mask is not None: da = mask_range(da, mask=mask) - return NormalizedQ[ScatteringRunType, Denominator](da) + return NormalizedQ[RunType, Denominator](da) def mask_and_scale_wavelength_qxy( - da: BinnedQxQy[ScatteringRunType, Denominator], + da: BinnedQxQy[RunType, Denominator], mask: WavelengthMask, - wavelength_term: MonitorTerm[ScatteringRunType], + wavelength_term: MonitorTerm[RunType], uncertainties: UncertaintyBroadcastMode, -) -> NormalizedQxQy[ScatteringRunType, Denominator]: +) -> NormalizedQxQy[RunType, Denominator]: da = da * broadcast_uncertainties(wavelength_term, prototype=da, mode=uncertainties) if mask is not None: da = mask_range(da, mask=mask) - return NormalizedQxQy[ScatteringRunType, Denominator](da) + return NormalizedQxQy[RunType, Denominator](da) def _compute_Q( data: sc.DataArray, graph: ElasticCoordTransformGraph, target: tuple[str, ...] ) -> sc.DataArray: # Keep naming of wavelength dim, subsequent steps use a (Q[xy], wavelength) binning. - return QDetector[ScatteringRunType, IofQPart]( + return QDetector[RunType, IofQPart]( data.transform_coords( target, graph=graph, @@ -256,25 +255,25 @@ def _compute_Q( def compute_Q( - data: WavelengthDetector[ScatteringRunType, IofQPart], - graph: ElasticCoordTransformGraph[ScatteringRunType], -) -> QDetector[ScatteringRunType, IofQPart]: + data: WavelengthDetector[RunType, IofQPart], + graph: ElasticCoordTransformGraph[RunType], +) -> QDetector[RunType, IofQPart]: """ Convert a data array from wavelength to Q. """ - return QDetector[ScatteringRunType, IofQPart]( + return QDetector[RunType, IofQPart]( _compute_Q(data=data, graph=graph, target=('Q',)) ) def compute_Qxy( - data: WavelengthDetector[ScatteringRunType, IofQPart], - graph: ElasticCoordTransformGraph[ScatteringRunType], -) -> QxyDetector[ScatteringRunType, IofQPart]: + data: WavelengthDetector[RunType, IofQPart], + graph: ElasticCoordTransformGraph[RunType], +) -> QxyDetector[RunType, IofQPart]: """ Convert a data array from wavelength to Qx and Qy. """ - return QxyDetector[ScatteringRunType, IofQPart]( + return QxyDetector[RunType, IofQPart]( _compute_Q(data=data, graph=graph, target=('Qx', 'Qy')) ) diff --git a/src/ess/sans/i_of_q.py b/src/ess/sans/i_of_q.py index d1dfee57..36ad75a8 100644 --- a/src/ess/sans/i_of_q.py +++ b/src/ess/sans/i_of_q.py @@ -30,7 +30,6 @@ ReturnEvents, RunType, SampleRun, - ScatteringRunType, WavelengthBins, WavelengthMonitor, ) @@ -136,10 +135,10 @@ def resample_direct_beam( def bin_in_q( - data: QDetector[ScatteringRunType, IofQPart], + data: QDetector[RunType, IofQPart], q_bins: QBins, dims_to_keep: DimsToKeep, -) -> BinnedQ[ScatteringRunType, IofQPart]: +) -> BinnedQ[RunType, IofQPart]: """ Merges data from all pixels into a single I(Q) spectrum: @@ -162,15 +161,15 @@ def bin_in_q( The input data converted to Q and then summed over all detector pixels. """ out = _bin_in_q(data=data, edges={'Q': q_bins}, dims_to_keep=dims_to_keep) - return BinnedQ[ScatteringRunType, IofQPart](out) + return BinnedQ[RunType, IofQPart](out) def bin_in_qxy( - data: QxyDetector[ScatteringRunType, IofQPart], + data: QxyDetector[RunType, IofQPart], qx_bins: QxBins, qy_bins: QyBins, dims_to_keep: DimsToKeep, -) -> BinnedQxQy[ScatteringRunType, IofQPart]: +) -> BinnedQxQy[RunType, IofQPart]: """ Merges data from all pixels into a single I(Q) spectrum: @@ -200,7 +199,7 @@ def bin_in_qxy( edges={'Qy': qy_bins, 'Qx': qx_bins}, dims_to_keep=dims_to_keep, ) - return BinnedQxQy[ScatteringRunType, IofQPart](out) + return BinnedQxQy[RunType, IofQPart](out) def _bin_in_q( diff --git a/src/ess/sans/masking.py b/src/ess/sans/masking.py index 7bea3c34..170dc4ac 100644 --- a/src/ess/sans/masking.py +++ b/src/ess/sans/masking.py @@ -15,8 +15,8 @@ MaskedDetectorIDs, Numerator, PixelMaskFilename, + RunType, SampleRun, - ScatteringRunType, TofDetector, ) @@ -53,9 +53,9 @@ def to_detector_mask( def apply_pixel_masks( - data: TofDetector[ScatteringRunType], + data: TofDetector[RunType], masks: DetectorMasks, -) -> CorrectedDetector[ScatteringRunType, Numerator]: +) -> CorrectedDetector[RunType, Numerator]: """Apply pixel-specific masks to raw data. Parameters @@ -65,7 +65,7 @@ def apply_pixel_masks( masks: A series of masks. """ - return CorrectedDetector[ScatteringRunType, Numerator](data.assign_masks(masks)) + return CorrectedDetector[RunType, Numerator](data.assign_masks(masks)) providers = ( diff --git a/src/ess/sans/normalization.py b/src/ess/sans/normalization.py index e5c1e299..f43b34ee 100644 --- a/src/ess/sans/normalization.py +++ b/src/ess/sans/normalization.py @@ -29,7 +29,7 @@ ReducedQ, ReducedQxQy, ReturnEvents, - ScatteringRunType, + RunType, SolidAngle, Transmission, TransmissionFraction, @@ -41,11 +41,11 @@ def solid_angle( - data: EmptyDetector[ScatteringRunType], - pixel_shape: DetectorPixelShape[ScatteringRunType], - transform: NeXusTransformation[snx.NXdetector, ScatteringRunType], - sample_position: Position[snx.NXsample, ScatteringRunType], -) -> SolidAngle[ScatteringRunType]: + data: EmptyDetector[RunType], + pixel_shape: DetectorPixelShape[RunType], + transform: NeXusTransformation[snx.NXdetector, RunType], + sample_position: Position[snx.NXsample, RunType], +) -> SolidAngle[RunType]: """ Solid angle for cylindrical pixels. @@ -83,7 +83,7 @@ def solid_angle( radius=radius, length=length, ) - return SolidAngle[ScatteringRunType]( + return SolidAngle[RunType]( concepts.rewrap_reduced_data( prototype=data, data=omega, dim=set(data.dims) - set(omega.dims) ) @@ -91,12 +91,10 @@ def solid_angle( def mask_solid_angle( - solid_angle: SolidAngle[ScatteringRunType], + solid_angle: SolidAngle[RunType], masks: DetectorMasks, -) -> CorrectedDetector[ScatteringRunType, Denominator]: - return CorrectedDetector[ScatteringRunType, Denominator]( - solid_angle.assign_masks(masks) - ) +) -> CorrectedDetector[RunType, Denominator]: + return CorrectedDetector[RunType, Denominator](solid_angle.assign_masks(masks)) def _approximate_solid_angle_for_cylinder_shaped_pixel_of_detector( @@ -139,15 +137,13 @@ def _approximate_solid_angle_for_cylinder_shaped_pixel_of_detector( def transmission_fraction( - sample_incident_monitor: CorrectedMonitor[ - TransmissionRun[ScatteringRunType], Incident - ], + sample_incident_monitor: CorrectedMonitor[TransmissionRun[RunType], Incident], sample_transmission_monitor: CorrectedMonitor[ - TransmissionRun[ScatteringRunType], Transmission + TransmissionRun[RunType], Transmission ], direct_incident_monitor: CorrectedMonitor[EmptyBeamRun, Incident], direct_transmission_monitor: CorrectedMonitor[EmptyBeamRun, Transmission], -) -> TransmissionFraction[ScatteringRunType]: +) -> TransmissionFraction[RunType]: """ Approximation based on equations in `CalculateTransmission `_ @@ -176,13 +172,13 @@ def transmission_fraction( frac = (sample_transmission_monitor / direct_transmission_monitor) * ( direct_incident_monitor / sample_incident_monitor ) - return TransmissionFraction[ScatteringRunType](frac) + return TransmissionFraction[RunType](frac) def norm_monitor_term( - incident_monitor: CorrectedMonitor[ScatteringRunType, Incident], - transmission_fraction: TransmissionFraction[ScatteringRunType], -) -> MonitorTerm[ScatteringRunType]: + incident_monitor: CorrectedMonitor[RunType, Incident], + transmission_fraction: TransmissionFraction[RunType], +) -> MonitorTerm[RunType]: """ Compute the monitor-dependent contribution to the denominator term of I(Q). @@ -207,14 +203,14 @@ def norm_monitor_term( out = incident_monitor * transmission_fraction # Convert wavelength coordinate to midpoints for future histogramming out.coords['wavelength'] = sc.midpoints(out.coords['wavelength']) - return MonitorTerm[ScatteringRunType](out) + return MonitorTerm[RunType](out) def norm_detector_term( - solid_angle: CorrectedDetector[ScatteringRunType, Denominator], + solid_angle: CorrectedDetector[RunType, Denominator], direct_beam: CleanDirectBeam, uncertainties: UncertaintyBroadcastMode, -) -> WavelengthDetector[ScatteringRunType, Denominator]: +) -> WavelengthDetector[RunType, Denominator]: """ Compute the detector-dependent contribution to the denominator term of I(Q). @@ -255,7 +251,7 @@ def norm_detector_term( ) # Convert wavelength coordinate to midpoints for future histogramming out.coords['wavelength'] = sc.midpoints(out.coords['wavelength']) - return WavelengthDetector[ScatteringRunType, Denominator](out) + return WavelengthDetector[RunType, Denominator](out) def process_wavelength_bands( @@ -413,26 +409,26 @@ def _reduce(part: sc.DataArray, /, *, bands: ProcessedWavelengthBands) -> sc.Dat def reduce_q( - data: NormalizedQ[ScatteringRunType, IofQPart], + data: NormalizedQ[RunType, IofQPart], bands: ProcessedWavelengthBands, -) -> ReducedQ[ScatteringRunType, IofQPart]: - return ReducedQ[ScatteringRunType, IofQPart](_reduce(data, bands=bands)) +) -> ReducedQ[RunType, IofQPart]: + return ReducedQ[RunType, IofQPart](_reduce(data, bands=bands)) def reduce_qxy( - data: NormalizedQxQy[ScatteringRunType, IofQPart], + data: NormalizedQxQy[RunType, IofQPart], bands: ProcessedWavelengthBands, -) -> ReducedQxQy[ScatteringRunType, IofQPart]: - return ReducedQxQy[ScatteringRunType, IofQPart](_reduce(data, bands=bands)) +) -> ReducedQxQy[RunType, IofQPart]: + return ReducedQxQy[RunType, IofQPart](_reduce(data, bands=bands)) def normalize_q( - numerator: ReducedQ[ScatteringRunType, Numerator], - denominator: ReducedQ[ScatteringRunType, Denominator], + numerator: ReducedQ[RunType, Numerator], + denominator: ReducedQ[RunType, Denominator], return_events: ReturnEvents, uncertainties: UncertaintyBroadcastMode, -) -> IntensityQ[ScatteringRunType]: - return IntensityQ[ScatteringRunType]( +) -> IntensityQ[RunType]: + return IntensityQ[RunType]( _normalize( numerator=numerator, denominator=denominator, @@ -443,12 +439,12 @@ def normalize_q( def normalize_qxy( - numerator: ReducedQxQy[ScatteringRunType, Numerator], - denominator: ReducedQxQy[ScatteringRunType, Denominator], + numerator: ReducedQxQy[RunType, Numerator], + denominator: ReducedQxQy[RunType, Denominator], return_events: ReturnEvents, uncertainties: UncertaintyBroadcastMode, -) -> IntensityQxQy[ScatteringRunType]: - return IntensityQxQy[ScatteringRunType]( +) -> IntensityQxQy[RunType]: + return IntensityQxQy[RunType]( _normalize( numerator=numerator, denominator=denominator, diff --git a/src/ess/sans/types.py b/src/ess/sans/types.py index 07fe5e6c..7c434ff3 100644 --- a/src/ess/sans/types.py +++ b/src/ess/sans/types.py @@ -13,6 +13,7 @@ import scipp as sc from ess.reduce.nexus import types as reduce_t +from ess.reduce.time_of_flight import types as tof_t from ess.reduce.uncertainty import UncertaintyBroadcastMode as _UncertaintyBroadcastMode BackgroundRun = reduce_t.BackgroundRun @@ -34,12 +35,16 @@ Transmission = reduce_t.TransmissionMonitor TransmissionRun = reduce_t.TransmissionRun +TofDetector = tof_t.TofDetector +TofMonitor = tof_t.TofMonitor +TimeOfFlightLookupTableFilename = tof_t.TimeOfFlightLookupTableFilename +TimeOfFlightLookupTable = tof_t.TimeOfFlightLookupTable + DetectorBankSizes = reduce_t.DetectorBankSizes NeXusDetectorName = reduce_t.NeXusDetectorName MonitorType = TypeVar('MonitorType', Incident, Transmission) RunType = reduce_t.RunType -ScatteringRunType = TypeVar('ScatteringRunType', BackgroundRun, SampleRun) UncertaintyBroadcastMode = _UncertaintyBroadcastMode @@ -149,9 +154,7 @@ """Direct beam""" -class TransmissionFraction( - sciline.Scope[ScatteringRunType, sc.DataArray], sc.DataArray -): +class TransmissionFraction(sciline.Scope[RunType, sc.DataArray], sc.DataArray): """Transmission fraction""" @@ -159,40 +162,28 @@ class TransmissionFraction( """Direct beam after resampling to required wavelength bins, else and array of ones.""" -class DetectorPixelShape(sciline.Scope[ScatteringRunType, sc.DataGroup], sc.DataGroup): +class DetectorPixelShape(sciline.Scope[RunType, sc.DataGroup], sc.DataGroup): """Geometry of the detector from description in nexus file.""" -class SolidAngle(sciline.Scope[ScatteringRunType, sc.DataArray], sc.DataArray): +class SolidAngle(sciline.Scope[RunType, sc.DataArray], sc.DataArray): """Solid angle of detector pixels seen from sample position""" -class TofDetector(sciline.Scope[ScatteringRunType, sc.DataArray], sc.DataArray): - """Data with a time-of-flight coordinate""" - - -class TofMonitor(sciline.Scope[RunType, MonitorType, sc.DataGroup], sc.DataGroup): - """Monitor data with a time-of-flight coordinate""" - - PixelMask = NewType('PixelMask', sc.Variable) -class MonitorTerm(sciline.Scope[ScatteringRunType, sc.DataArray], sc.DataArray): +class MonitorTerm(sciline.Scope[RunType, sc.DataArray], sc.DataArray): """Monitor-dependent factor of the Normalization term (numerator) for IofQ.""" -class CorrectedDetector( - sciline.Scope[ScatteringRunType, IofQPart, sc.DataArray], sc.DataArray -): +class CorrectedDetector(sciline.Scope[RunType, IofQPart, sc.DataArray], sc.DataArray): """ Data with masks and corrections applied, used for numerator or denominator of IofQ. """ -class WavelengthDetector( - sciline.Scope[ScatteringRunType, IofQPart, sc.DataArray], sc.DataArray -): +class WavelengthDetector(sciline.Scope[RunType, IofQPart, sc.DataArray], sc.DataArray): """ Prerequisite for IofQ numerator or denominator. @@ -202,55 +193,45 @@ class WavelengthDetector( """ -class NormalizedQ( - sciline.Scope[ScatteringRunType, IofQPart, sc.DataArray], sc.DataArray -): +class NormalizedQ(sciline.Scope[RunType, IofQPart, sc.DataArray], sc.DataArray): """Result of applying wavelength scaling/masking to :py:class:`BinnedQ`""" -class NormalizedQxQy( - sciline.Scope[ScatteringRunType, IofQPart, sc.DataArray], sc.DataArray -): +class NormalizedQxQy(sciline.Scope[RunType, IofQPart, sc.DataArray], sc.DataArray): """Result of applying wavelength scaling/masking to :py:class:`BinnedQxQy`""" -class QDetector(sciline.Scope[ScatteringRunType, IofQPart, sc.DataArray], sc.DataArray): +class QDetector(sciline.Scope[RunType, IofQPart, sc.DataArray], sc.DataArray): """Result of converting :py:class:`WavelengthDetectorMasked` to Q""" -class QxyDetector( - sciline.Scope[ScatteringRunType, IofQPart, sc.DataArray], sc.DataArray -): +class QxyDetector(sciline.Scope[RunType, IofQPart, sc.DataArray], sc.DataArray): """Result of converting :py:class:`WavelengthDetectorMasked` to Qx and Qy""" -class BinnedQ(sciline.Scope[ScatteringRunType, IofQPart, sc.DataArray], sc.DataArray): +class BinnedQ(sciline.Scope[RunType, IofQPart, sc.DataArray], sc.DataArray): """Result of histogramming/binning :py:class:`QDetector` over all pixels into Q bins""" -class BinnedQxQy( - sciline.Scope[ScatteringRunType, IofQPart, sc.DataArray], sc.DataArray -): +class BinnedQxQy(sciline.Scope[RunType, IofQPart, sc.DataArray], sc.DataArray): """Result of histogramming/binning :py:class:`QxyDetector` over all pixels into Qx and Qy bins""" -class ReducedQ(sciline.Scope[ScatteringRunType, IofQPart, sc.DataArray], sc.DataArray): +class ReducedQ(sciline.Scope[RunType, IofQPart, sc.DataArray], sc.DataArray): """Result of reducing :py:class:`BinnedQ` over the wavelength dimensions""" -class ReducedQxQy( - sciline.Scope[ScatteringRunType, IofQPart, sc.DataArray], sc.DataArray -): +class ReducedQxQy(sciline.Scope[RunType, IofQPart, sc.DataArray], sc.DataArray): """Result of reducing :py:class:`BinnedQxQy` over the wavelength dimensions""" -class IntensityQ(sciline.Scope[ScatteringRunType, sc.DataArray], sc.DataArray): +class IntensityQ(sciline.Scope[RunType, sc.DataArray], sc.DataArray): """I(Q)""" -class IntensityQxQy(sciline.Scope[ScatteringRunType, sc.DataArray], sc.DataArray): +class IntensityQxQy(sciline.Scope[RunType, sc.DataArray], sc.DataArray): """I(Qx, Qy)""" diff --git a/src/ess/sans/workflow.py b/src/ess/sans/workflow.py index 71f5f73b..77944c95 100644 --- a/src/ess/sans/workflow.py +++ b/src/ess/sans/workflow.py @@ -6,8 +6,8 @@ import sciline import scipp as sc -from ess.reduce.nexus.workflow import GenericNeXusWorkflow from ess.reduce.parameter import parameter_mappers +from ess.reduce.time_of_flight import GenericTofWorkflow from . import common, conversions, i_of_q, masking, normalization from .types import ( @@ -176,7 +176,7 @@ def SansWorkflow() -> sciline.Pipeline: : SANS workflow as a sciline.Pipeline """ - workflow = GenericNeXusWorkflow( + workflow = GenericTofWorkflow( run_types=( SampleRun, EmptyBeamRun, diff --git a/tests/loki/common.py b/tests/loki/common.py deleted file mode 100644 index 5ecd2b91..00000000 --- a/tests/loki/common.py +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) -import sciline -import scipp as sc - -import ess.loki.data # noqa: F401 -from ess import loki -from ess.sans.types import ( - BackgroundRun, - CorrectForGravity, - DetectorMasks, - DirectBeam, - EmptyBeamRun, - Filename, - NeXusDetectorName, - QBins, - QxBins, - QyBins, - ReturnEvents, - SampleRun, - TransmissionRun, - UncertaintyBroadcastMode, - WavelengthBins, -) - - -def make_workflow(no_masks: bool = True) -> sciline.Pipeline: - wf = loki.LokiAtLarmorWorkflow() - - wf[NeXusDetectorName] = 'larmor_detector' - wf[Filename[SampleRun]] = loki.data.loki_tutorial_sample_run_60339() - wf[Filename[BackgroundRun]] = loki.data.loki_tutorial_background_run_60393() - wf[Filename[TransmissionRun[SampleRun]]] = ( - loki.data.loki_tutorial_sample_transmission_run() - ) - wf[Filename[TransmissionRun[BackgroundRun]]] = loki.data.loki_tutorial_run_60392() - wf[Filename[EmptyBeamRun]] = loki.data.loki_tutorial_run_60392() - - wf[WavelengthBins] = sc.linspace( - 'wavelength', start=1.0, stop=13.0, num=51, unit='angstrom' - ) - wf[CorrectForGravity] = True - wf[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound - wf[ReturnEvents] = False - - wf[QxBins] = sc.linspace('Qx', start=-0.3, stop=0.3, num=91, unit='1/angstrom') - wf[QyBins] = sc.linspace('Qy', start=-0.2, stop=0.3, num=78, unit='1/angstrom') - wf[QBins] = sc.linspace('Q', start=0.01, stop=0.3, num=101, unit='1/angstrom') - # We have no direct-beam file for Loki currently - wf[DirectBeam] = None - if no_masks: - wf[DetectorMasks] = {} - - return wf diff --git a/tests/loki/conftest.py b/tests/loki/conftest.py new file mode 100644 index 00000000..2106df67 --- /dev/null +++ b/tests/loki/conftest.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) +import pytest +import sciline +import scipp as sc + +import ess.loki.data # noqa: F401 +from ess import loki +from ess.sans.types import ( + BackgroundRun, + CorrectForGravity, + DetectorMasks, + DirectBeam, + EmptyBeamRun, + Filename, + NeXusDetectorName, + QBins, + QxBins, + QyBins, + ReturnEvents, + SampleRun, + TransmissionRun, + UncertaintyBroadcastMode, + WavelengthBins, +) + + +@pytest.fixture +def larmor_workflow() -> sciline.Pipeline: + def make_workflow(no_masks: bool = True) -> sciline.Pipeline: + wf = loki.LokiAtLarmorWorkflow() + + wf[NeXusDetectorName] = 'larmor_detector' + wf[Filename[SampleRun]] = loki.data.loki_tutorial_sample_run_60339() + wf[Filename[BackgroundRun]] = loki.data.loki_tutorial_background_run_60393() + wf[Filename[TransmissionRun[SampleRun]]] = ( + loki.data.loki_tutorial_sample_transmission_run() + ) + wf[Filename[TransmissionRun[BackgroundRun]]] = ( + loki.data.loki_tutorial_run_60392() + ) + wf[Filename[EmptyBeamRun]] = loki.data.loki_tutorial_run_60392() + + wf[WavelengthBins] = sc.linspace( + 'wavelength', start=1.0, stop=13.0, num=51, unit='angstrom' + ) + wf[CorrectForGravity] = True + wf[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound + wf[ReturnEvents] = False + + wf[QxBins] = sc.linspace('Qx', start=-0.3, stop=0.3, num=91, unit='1/angstrom') + wf[QyBins] = sc.linspace('Qy', start=-0.2, stop=0.3, num=78, unit='1/angstrom') + wf[QBins] = sc.linspace('Q', start=0.01, stop=0.3, num=101, unit='1/angstrom') + # We have no direct-beam file for Loki currently + wf[DirectBeam] = None + if no_masks: + wf[DetectorMasks] = {} + + return wf + + return make_workflow + + +@pytest.fixture +def loki_workflow() -> sciline.Pipeline: + def make_workflow() -> sciline.Pipeline: + wf = loki.LokiWorkflow() + + # For now, use a dummy file for all runs. This produces a result with all NaNs, + # but is sufficient to test that the workflow runs end-to-end. + # TODO: Replace with real test data when available. + file = loki.data.loki_coda_file_one_event() + wf[Filename[SampleRun]] = file + wf[Filename[BackgroundRun]] = file + wf[Filename[TransmissionRun[SampleRun]]] = file + wf[Filename[TransmissionRun[BackgroundRun]]] = file + wf[Filename[EmptyBeamRun]] = file + + wf[WavelengthBins] = sc.linspace( + 'wavelength', start=1.0, stop=13.0, num=51, unit='angstrom' + ) + wf[CorrectForGravity] = True + wf[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound + wf[ReturnEvents] = False + + wf[QxBins] = sc.linspace('Qx', start=-0.3, stop=0.3, num=91, unit='1/angstrom') + wf[QyBins] = sc.linspace('Qy', start=-0.2, stop=0.3, num=78, unit='1/angstrom') + wf[QBins] = sc.linspace('Q', start=0.01, stop=0.3, num=101, unit='1/angstrom') + # We have no direct-beam file for Loki currently + wf[DirectBeam] = None + wf[DetectorMasks] = {} + + return wf + + return make_workflow diff --git a/tests/loki/directbeam_test.py b/tests/loki/directbeam_test.py index 6f4009c1..118c641f 100644 --- a/tests/loki/directbeam_test.py +++ b/tests/loki/directbeam_test.py @@ -1,7 +1,5 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) -import sys -from pathlib import Path import scipp as sc from scipp.scipy.interpolate import interp1d @@ -15,9 +13,6 @@ WavelengthBins, ) -sys.path.insert(0, str(Path(__file__).resolve().parent)) -from common import make_workflow - def _get_I0(qbins: sc.Variable) -> sc.Variable: Iq_theory = sc.io.load_hdf5(loki.data.loki_tutorial_poly_gauss_I0()) @@ -25,9 +20,9 @@ def _get_I0(qbins: sc.Variable) -> sc.Variable: return f(sc.midpoints(qbins)).data[0] -def test_can_compute_direct_beam_for_all_pixels(): +def test_can_compute_direct_beam_for_all_pixels(larmor_workflow): n_wavelength_bands = 10 - pipeline = make_workflow() + pipeline = larmor_workflow() edges = pipeline.compute(WavelengthBins) pipeline[WavelengthBands] = sc.linspace( 'wavelength', edges.min(), edges.max(), n_wavelength_bands + 1 @@ -46,10 +41,10 @@ def test_can_compute_direct_beam_for_all_pixels(): assert direct_beam_function.sizes['wavelength'] == n_wavelength_bands -def test_can_compute_direct_beam_with_overlapping_wavelength_bands(): +def test_can_compute_direct_beam_with_overlapping_wavelength_bands(larmor_workflow): n_wavelength_bands = 10 # Bands have double the width - pipeline = make_workflow() + pipeline = larmor_workflow() edges = pipeline.compute(WavelengthBins) edges = sc.linspace('band', edges.min(), edges.max(), n_wavelength_bands + 2) pipeline[WavelengthBands] = sc.concat( @@ -70,9 +65,9 @@ def test_can_compute_direct_beam_with_overlapping_wavelength_bands(): assert direct_beam_function.sizes['wavelength'] == n_wavelength_bands -def test_can_compute_direct_beam_per_layer(): +def test_can_compute_direct_beam_per_layer(larmor_workflow): n_wavelength_bands = 10 - pipeline = make_workflow() + pipeline = larmor_workflow() edges = pipeline.compute(WavelengthBins) pipeline[WavelengthBands] = sc.linspace( 'wavelength', edges.min(), edges.max(), n_wavelength_bands + 1 @@ -94,9 +89,9 @@ def test_can_compute_direct_beam_per_layer(): assert direct_beam_function.sizes['layer'] == 4 -def test_can_compute_direct_beam_per_layer_and_straw(): +def test_can_compute_direct_beam_per_layer_and_straw(larmor_workflow): n_wavelength_bands = 10 - pipeline = make_workflow() + pipeline = larmor_workflow() edges = pipeline.compute(WavelengthBins) pipeline[WavelengthBands] = sc.linspace( 'wavelength', edges.min(), edges.max(), n_wavelength_bands + 1 diff --git a/tests/loki/iofq_test.py b/tests/loki/iofq_test.py index 08f3375d..270106e9 100644 --- a/tests/loki/iofq_test.py +++ b/tests/loki/iofq_test.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) import os -import sys from pathlib import Path import pytest @@ -37,18 +36,15 @@ WavelengthDetector, ) -sys.path.insert(0, str(Path(__file__).resolve().parent)) -from common import make_workflow - -def test_can_create_pipeline(): - pipeline = make_workflow() +def test_can_create_pipeline(larmor_workflow): + pipeline = larmor_workflow() pipeline[BeamCenter] = sc.vector([0, 0, 0], unit='m') pipeline.get(BackgroundSubtractedIofQ) -def test_can_create_pipeline_with_pixel_masks(): - pipeline = make_workflow(no_masks=False) +def test_can_create_pipeline_with_pixel_masks(larmor_workflow): + pipeline = larmor_workflow(no_masks=False) pipeline = sans.with_pixel_mask_filenames( pipeline, loki.data.loki_tutorial_mask_filenames() ) @@ -61,8 +57,8 @@ def test_can_create_pipeline_with_pixel_masks(): [UncertaintyBroadcastMode.drop, UncertaintyBroadcastMode.upper_bound], ) @pytest.mark.parametrize('qxy', [False, True]) -def test_pipeline_can_compute_IofQ(uncertainties, qxy: bool): - pipeline = make_workflow(no_masks=False) +def test_pipeline_can_compute_IofQ(larmor_workflow, uncertainties, qxy: bool): + pipeline = larmor_workflow(no_masks=False) pipeline[UncertaintyBroadcastMode] = uncertainties pipeline = sans.with_pixel_mask_filenames( pipeline, loki.data.loki_tutorial_mask_filenames() @@ -100,8 +96,10 @@ def test_pipeline_can_compute_IofQ(uncertainties, qxy: bool): BackgroundSubtractedIofQxy, ], ) -def test_pipeline_can_compute_IofQ_in_event_mode(uncertainties, target): - pipeline = make_workflow() +def test_pipeline_can_compute_IofQ_in_event_mode( + larmor_workflow, uncertainties, target +): + pipeline = larmor_workflow() pipeline[UncertaintyBroadcastMode] = uncertainties pipeline[BeamCenter] = sans.beam_center_from_center_of_mass(pipeline) reference = pipeline.compute(target) @@ -133,15 +131,15 @@ def test_pipeline_can_compute_IofQ_in_event_mode(uncertainties, target): @pytest.mark.parametrize('qxy', [False, True]) -def test_pipeline_can_compute_IofQ_in_wavelength_bands(qxy: bool): - pipeline = make_workflow() +def test_pipeline_can_compute_IofQ_in_wavelength_bands(larmor_workflow, qxy: bool): + pipeline = larmor_workflow() pipeline[WavelengthBands] = sc.linspace( 'wavelength', pipeline.compute(WavelengthBins).min(), pipeline.compute(WavelengthBins).max(), 11, ) - pipeline[BeamCenter] = _compute_beam_center() + pipeline[BeamCenter] = _compute_beam_center(pipeline) result = pipeline.compute( BackgroundSubtractedIofQxy if qxy else BackgroundSubtractedIofQ ) @@ -150,15 +148,17 @@ def test_pipeline_can_compute_IofQ_in_wavelength_bands(qxy: bool): @pytest.mark.parametrize('qxy', [False, True]) -def test_pipeline_can_compute_IofQ_in_overlapping_wavelength_bands(qxy: bool): - pipeline = make_workflow() +def test_pipeline_can_compute_IofQ_in_overlapping_wavelength_bands( + larmor_workflow, qxy: bool +): + pipeline = larmor_workflow() # Bands have double the width edges = pipeline.compute(WavelengthBins) edges = sc.linspace('band', edges.min(), edges.max(), 12) pipeline[WavelengthBands] = sc.concat( [edges[:-2], edges[2::]], dim='wavelength' ).transpose() - pipeline[BeamCenter] = _compute_beam_center() + pipeline[BeamCenter] = _compute_beam_center(pipeline) result = pipeline.compute( BackgroundSubtractedIofQxy if qxy else BackgroundSubtractedIofQ ) @@ -167,10 +167,10 @@ def test_pipeline_can_compute_IofQ_in_overlapping_wavelength_bands(qxy: bool): @pytest.mark.parametrize('qxy', [False, True]) -def test_pipeline_can_compute_IofQ_in_layers(qxy: bool): - pipeline = make_workflow() +def test_pipeline_can_compute_IofQ_in_layers(larmor_workflow, qxy: bool): + pipeline = larmor_workflow() pipeline[DimsToKeep] = ['layer'] - pipeline[BeamCenter] = _compute_beam_center() + pipeline[BeamCenter] = _compute_beam_center(pipeline) result = pipeline.compute( BackgroundSubtractedIofQxy if qxy else BackgroundSubtractedIofQ ) @@ -178,11 +178,11 @@ def test_pipeline_can_compute_IofQ_in_layers(qxy: bool): assert result.sizes['layer'] == 4 -def _compute_beam_center(): - return sans.beam_center_from_center_of_mass(make_workflow()) +def _compute_beam_center(wf: sciline.Pipeline) -> sc.Variable: + return sans.beam_center_from_center_of_mass(wf) -def test_pipeline_can_compute_IofQ_merging_events_from_multiple_runs(): +def test_pipeline_can_compute_IofQ_merging_events_from_multiple_runs(larmor_workflow): sample_runs = [ loki.data.loki_tutorial_sample_run_60250(), loki.data.loki_tutorial_sample_run_60339(), @@ -191,8 +191,8 @@ def test_pipeline_can_compute_IofQ_merging_events_from_multiple_runs(): loki.data.loki_tutorial_background_run_60248(), loki.data.loki_tutorial_background_run_60393(), ] - pipeline = make_workflow() - pipeline[BeamCenter] = _compute_beam_center() + pipeline = larmor_workflow() + pipeline[BeamCenter] = _compute_beam_center(pipeline) # Remove previously set runs so we can be sure that below we use the mapped ones pipeline[Filename[SampleRun]] = None @@ -206,16 +206,18 @@ def test_pipeline_can_compute_IofQ_merging_events_from_multiple_runs(): assert result.dims == ('Qy', 'Qx') -def test_pipeline_can_compute_IofQ_by_bank(): - pipeline = make_workflow() - pipeline[BeamCenter] = _compute_beam_center() +def test_pipeline_can_compute_IofQ_by_bank(larmor_workflow): + pipeline = larmor_workflow() + pipeline[BeamCenter] = _compute_beam_center(pipeline) pipeline = sans.with_banks(pipeline, banks=['larmor_detector']) results = sciline.compute_mapped(pipeline, BackgroundSubtractedIofQ) assert results['larmor_detector'].dims == ('Q',) -def test_pipeline_can_compute_IofQ_merging_events_from_multiple_runs_by_bank(): +def test_pipeline_can_compute_IofQ_merging_events_from_multiple_runs_by_bank( + larmor_workflow, +): sample_runs = [ loki.data.loki_tutorial_sample_run_60250(), loki.data.loki_tutorial_sample_run_60339(), @@ -224,8 +226,8 @@ def test_pipeline_can_compute_IofQ_merging_events_from_multiple_runs_by_bank(): loki.data.loki_tutorial_background_run_60248(), loki.data.loki_tutorial_background_run_60393(), ] - pipeline = make_workflow() - pipeline[BeamCenter] = _compute_beam_center() + pipeline = larmor_workflow() + pipeline[BeamCenter] = _compute_beam_center(pipeline) pipeline = sans.with_sample_runs(pipeline, runs=sample_runs) pipeline = sans.with_background_runs(pipeline, runs=background_runs) @@ -241,10 +243,10 @@ def test_pipeline_can_compute_IofQ_merging_events_from_multiple_runs_by_bank(): assert_identical(sc.values(results['bank1']), sc.values(reference)) -def test_pipeline_IofQ_merging_events_yields_consistent_results(): +def test_pipeline_IofQ_merging_events_yields_consistent_results(larmor_workflow): N = 3 - center = _compute_beam_center() - pipeline_single = make_workflow() + pipeline_single = larmor_workflow() + center = _compute_beam_center(pipeline_single) pipeline_single[BeamCenter] = center sample_runs = [loki.data.loki_tutorial_sample_run_60339()] * N @@ -273,8 +275,8 @@ def test_pipeline_IofQ_merging_events_yields_consistent_results(): ) -def test_beam_center_from_center_of_mass_is_close_to_verified_result(): - pipeline = make_workflow(no_masks=False) +def test_beam_center_from_center_of_mass_is_close_to_verified_result(larmor_workflow): + pipeline = larmor_workflow(no_masks=False) pipeline = sans.with_pixel_mask_filenames( pipeline, loki.data.loki_tutorial_mask_filenames() ) @@ -283,9 +285,9 @@ def test_beam_center_from_center_of_mass_is_close_to_verified_result(): assert sc.allclose(center, reference) -def test_phi_with_gravity(): - pipeline = make_workflow() - pipeline[BeamCenter] = _compute_beam_center() +def test_phi_with_gravity(larmor_workflow): + pipeline = larmor_workflow() + pipeline[BeamCenter] = _compute_beam_center(pipeline) pipeline[CorrectForGravity] = False data_no_grav = pipeline.compute(WavelengthDetector[SampleRun, Numerator]).flatten( to='pixel' diff --git a/tests/loki/workflow_test.py b/tests/loki/workflow_test.py index cb7775e6..07884e16 100644 --- a/tests/loki/workflow_test.py +++ b/tests/loki/workflow_test.py @@ -1,10 +1,10 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2024 Scipp contributors (https://github.com/scipp) -import sys -from pathlib import Path +import pytest import scipp as sc +from sciline import UnsatisfiedRequirement from ess import loki from ess.loki import LokiAtLarmorWorkflow @@ -15,16 +15,16 @@ BeamCenter, Filename, IntensityQ, + NeXusDetectorName, PixelMaskFilename, QBins, ReturnEvents, SampleRun, + TimeOfFlightLookupTableFilename, + TofDetector, UncertaintyBroadcastMode, ) -sys.path.insert(0, str(Path(__file__).resolve().parent)) -from common import make_workflow - def test_sans_workflow_registers_subclasses(): # Because it was imported @@ -38,27 +38,27 @@ class MyWorkflow: ... assert len(workflow.workflow_registry) == count + 1 -def test_loki_workflow_parameters_returns_filtered_params(): +def test_loki_larmor_workflow_parameters_returns_filtered_params(): wf = LokiAtLarmorWorkflow() parameters = workflow.get_parameters(wf, (IntensityQ[SampleRun],)) assert Filename[SampleRun] in parameters assert Filename[BackgroundRun] not in parameters -def test_loki_workflow_parameters_returns_no_params_for_no_outputs(): +def test_loki_larmor_workflow_parameters_returns_no_params_for_no_outputs(): wf = LokiAtLarmorWorkflow() parameters = workflow.get_parameters(wf, ()) assert not parameters -def test_loki_workflow_parameters_with_param_returns_param(): +def test_loki_larmor_workflow_parameters_with_param_returns_param(): wf = LokiAtLarmorWorkflow() parameters = workflow.get_parameters(wf, (ReturnEvents,)) assert parameters.keys() == {ReturnEvents} -def test_loki_workflow_compute_with_single_pixel_mask(): - wf = make_workflow(no_masks=False) +def test_loki_larmor_workflow_compute_with_single_pixel_mask(larmor_workflow): + wf = larmor_workflow(no_masks=False) wf[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop wf[PixelMaskFilename] = loki.data.loki_tutorial_mask_filenames()[0] # For simplicity, insert a fake beam center instead of computing it. @@ -68,3 +68,37 @@ def test_loki_workflow_compute_with_single_pixel_mask(): assert result.dims == ('Q',) assert sc.identical(result.coords['Q'], wf.compute(QBins)) assert result.sizes['Q'] == 100 + + +@pytest.mark.parametrize("bank", list(range(9))) +def test_loki_workflow_needs_tof_lookup_table(loki_workflow, bank): + wf = loki_workflow() + # For simplicity, insert a fake beam center instead of computing it. + wf[BeamCenter] = sc.vector([0.0, 0.0, 0.0], unit='m') + wf[NeXusDetectorName] = f'loki_detector_{bank}' + with pytest.raises(UnsatisfiedRequirement, match='TimeOfFlightLookupTableFilename'): + wf.compute(TofDetector[SampleRun]) + + +@pytest.mark.parametrize("bank", list(range(9))) +def test_loki_workflow_can_compute_tof(loki_workflow, bank): + wf = loki_workflow() + # For simplicity, insert a fake beam center instead of computing it. + wf[BeamCenter] = sc.vector([0.0, 0.0, 0.0], unit='m') + wf[NeXusDetectorName] = f'loki_detector_{bank}' + wf[TimeOfFlightLookupTableFilename] = loki.data.loki_tof_lookup_table_no_choppers() + result = wf.compute(TofDetector[SampleRun]) + assert 'tof' in result.bins.coords + + +@pytest.mark.parametrize("bank", list(range(9))) +def test_loki_workflow_can_compute_iofq(loki_workflow, bank): + wf = loki_workflow() + # For simplicity, insert a fake beam center instead of computing it. + wf[BeamCenter] = sc.vector([0.0, 0.0, 0.0], unit='m') + wf[NeXusDetectorName] = f'loki_detector_{bank}' + wf[TimeOfFlightLookupTableFilename] = loki.data.loki_tof_lookup_table_no_choppers() + + result = wf.compute(BackgroundSubtractedIofQ) + assert result.dims == ('Q',) + assert sc.identical(result.coords['Q'], wf.compute(QBins))