diff --git a/docs/user-guide/loki/index.md b/docs/user-guide/loki/index.md index cfdec5fc..8a26222d 100644 --- a/docs/user-guide/loki/index.md +++ b/docs/user-guide/loki/index.md @@ -7,4 +7,5 @@ maxdepth: 1 loki-direct-beam loki-iofq +workflow-widget-loki ``` diff --git a/docs/user-guide/loki/workflow-widget-loki.ipynb b/docs/user-guide/loki/workflow-widget-loki.ipynb new file mode 100644 index 00000000..7c8c3a83 --- /dev/null +++ b/docs/user-guide/loki/workflow-widget-loki.ipynb @@ -0,0 +1,132 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Workflow widgets example\n", + "\n", + "This notebook illustrates how we can use `essreduce`'s [workflow widgets](https://scipp.github.io/essreduce/user-guide/widget.html)\n", + "to generate a graphical interface for running the LoKI tutorial workflow.\n", + "\n", + "## Initializing the GUI\n", + "\n", + "It is as simple as importing the loki submodule and generating a GUI using `workflow_widget`\n", + "(the workflow automatically registers itself to a library of workflows when imported)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "# Import loki submodule to register workflow\n", + "from ess import loki # noqa: F401\n", + "from ess.reduce import ui\n", + "\n", + "# Prepare a container for accessing the results computed by the GUI\n", + "results = {}\n", + "\n", + "# Initialize the GUI widget\n", + "widget = ui.workflow_widget(result_registry=results)\n", + "widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "from ess.sans.types import DirectBeam, QBins\n", + "\n", + "select = widget.children[0].children[0]\n", + "keys, values = zip(*select.options, strict=True)\n", + "ind = keys.index(\"LokiAtLarmorTutorialWorkflow\")\n", + "select.value = values[ind]\n", + "# Select IofQ[SampleRun] output\n", + "wfw = widget.children[1].children[0]\n", + "outputs = wfw.output_selection_box.typical_outputs_widget\n", + "keys, values = zip(*outputs.options, strict=True)\n", + "ind = keys.index(\"IofQ[SampleRun]\")\n", + "outputs.value = (values[ind],)\n", + "# Refresh parameters\n", + "pbox = wfw.parameter_box\n", + "pbox.parameter_refresh_button.click()\n", + "# Enable DirectBeam input\n", + "pbox._input_widgets[DirectBeam].children[0].enabled = True\n", + "pbox._input_widgets[DirectBeam].children[0].wrapped._option_box.value = None\n", + "# Adjust Q range\n", + "pbox._input_widgets[QBins].children[0].fields[\"start\"].value = 0.01\n", + "# Run the workflow\n", + "rbox = wfw.result_box\n", + "rbox.run_button.click()" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Accessing the results\n", + "\n", + "We can now access the computed result in the `results` dictionary:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "results" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "The result can be plotted using" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "(da,) = results.values()\n", + "da.plot(norm=\"log\")" + ] + } + ], + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/ess/isissans/sans2d.py b/src/ess/isissans/sans2d.py index 68a9e4cf..850ae9e9 100644 --- a/src/ess/isissans/sans2d.py +++ b/src/ess/isissans/sans2d.py @@ -6,7 +6,9 @@ import scipp as sc from ess.reduce.nexus.workflow import GenericNeXusWorkflow +from ess.reduce.workflow import register_workflow from ess.sans import providers as sans_providers +from ess.sans.parameters import typical_outputs from ess.sans.types import BeamCenter, CalibratedDetector, DetectorMasks, SampleRun from .general import default_parameters @@ -82,6 +84,7 @@ def to_detector_masks( providers = (detector_edge_mask, sample_holder_mask, to_detector_masks) +@register_workflow def Sans2dWorkflow() -> sciline.Pipeline: """Create Sans2d workflow with default parameters.""" from . import providers as isis_providers @@ -93,9 +96,11 @@ def Sans2dWorkflow() -> sciline.Pipeline: workflow.insert(provider) for key, param in default_parameters().items(): workflow[key] = param + workflow.typical_outputs = typical_outputs return workflow +@register_workflow def Sans2dTutorialWorkflow() -> sciline.Pipeline: """ Create Sans2d tutorial workflow. diff --git a/src/ess/isissans/zoom.py b/src/ess/isissans/zoom.py index dcde6fb2..cb9cd4b8 100644 --- a/src/ess/isissans/zoom.py +++ b/src/ess/isissans/zoom.py @@ -3,8 +3,10 @@ import sciline from ess.reduce.nexus.workflow import GenericNeXusWorkflow +from ess.reduce.workflow import register_workflow from ess.sans import providers as sans_providers from ess.sans.io import read_xml_detector_masking +from ess.sans.parameters import typical_outputs from .general import default_parameters from .io import load_tutorial_direct_beam, load_tutorial_run @@ -21,6 +23,7 @@ def set_mantid_log_level(level: int = 3): pass +@register_workflow def ZoomWorkflow() -> sciline.Pipeline: """Create Zoom workflow with default parameters.""" from . import providers as isis_providers @@ -35,9 +38,11 @@ def ZoomWorkflow() -> sciline.Pipeline: for key, param in default_parameters().items(): workflow[key] = param workflow.insert(read_xml_detector_masking) + workflow.typical_outputs = typical_outputs return workflow +@register_workflow def ZoomTutorialWorkflow() -> sciline.Pipeline: """ Create Zoom tutorial workflow. diff --git a/src/ess/loki/general.py b/src/ess/loki/general.py index e80553bf..095858dd 100644 --- a/src/ess/loki/general.py +++ b/src/ess/loki/general.py @@ -7,9 +7,12 @@ import sciline import scipp as sc +from ess import sans from ess.reduce.nexus.workflow import GenericNeXusWorkflow +from ess.reduce.workflow import register_workflow from ess.sans import providers as sans_providers from ess.sans.io import read_xml_detector_masking +from ess.sans.parameters import typical_outputs from ..sans.types import ( CorrectForGravity, @@ -104,6 +107,7 @@ def load_direct_beam(filename: DirectBeamFilename) -> DirectBeam: ) +@register_workflow def LokiAtLarmorWorkflow() -> sciline.Pipeline: """ Workflow with default parameters for Loki test at Larmor. @@ -124,4 +128,38 @@ def LokiAtLarmorWorkflow() -> sciline.Pipeline: for key, param in default_parameters().items(): workflow[key] = param workflow.insert(read_xml_detector_masking) + workflow[sans.types.NeXusDetectorName] = 'larmor_detector' + workflow.typical_outputs = typical_outputs + return workflow + + +@register_workflow +def LokiAtLarmorTutorialWorkflow() -> sciline.Pipeline: + from ess.loki import data + + workflow = LokiAtLarmorWorkflow() + workflow[sans.types.Filename[sans.types.SampleRun]] = ( + data.loki_tutorial_sample_run_60339() + ) + # TODO This does not work with multiple + workflow[sans.types.PixelMaskFilename] = data.loki_tutorial_mask_filenames()[0] + + workflow[sans.types.Filename[sans.types.SampleRun]] = ( + data.loki_tutorial_sample_run_60339() + ) + workflow[sans.types.Filename[sans.types.BackgroundRun]] = ( + data.loki_tutorial_background_run_60393() + ) + workflow[sans.types.Filename[sans.types.TransmissionRun[sans.types.SampleRun]]] = ( + data.loki_tutorial_sample_transmission_run() + ) + workflow[ + sans.types.Filename[sans.types.TransmissionRun[sans.types.BackgroundRun]] + ] = data.loki_tutorial_run_60392() + workflow[sans.types.Filename[sans.types.EmptyBeamRun]] = ( + data.loki_tutorial_run_60392() + ) + workflow[sans.types.BeamCenter] = sc.vector( + value=[-0.02914868, -0.01816138, 0.0], unit='m' + ) return workflow diff --git a/src/ess/sans/parameters.py b/src/ess/sans/parameters.py new file mode 100644 index 00000000..4aaef525 --- /dev/null +++ b/src/ess/sans/parameters.py @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2024 Scipp contributors (https://github.com/scipp) +""" +Default parameters, providers and utility functions for the loki workflow. +""" + +from __future__ import annotations + +import scipp as sc + +from ess.reduce.parameter import ( + BinEdgesParameter, + BooleanParameter, + FilenameParameter, + MultiFilenameParameter, + ParamWithOptions, + StringParameter, + Vector2dParameter, + parameter_registry, +) + +from ..sans.types import ( + BackgroundRun, + BackgroundSubtractedIofQ, + BackgroundSubtractedIofQxy, + BeamCenter, + CorrectForGravity, + DirectBeam, + DirectBeamFilename, + EmptyBeamRun, + Filename, + Incident, + IofQ, + IofQxy, + MaskedData, + NeXusDetectorName, + NeXusMonitorName, + PixelMaskFilename, + PixelShapePath, + QBins, + QxBins, + QyBins, + ReturnEvents, + SampleRun, + TransformationPath, + Transmission, + TransmissionRun, + UncertaintyBroadcastMode, + WavelengthBins, + WavelengthMonitor, +) + +parameter_registry[CorrectForGravity] = BooleanParameter.from_type( + CorrectForGravity, default=False +) +parameter_registry[NeXusDetectorName] = StringParameter.from_type(NeXusDetectorName) +parameter_registry[NeXusMonitorName[Incident]] = StringParameter.from_type( + NeXusMonitorName[Incident], default='' +) +parameter_registry[NeXusMonitorName[Transmission]] = StringParameter.from_type( + NeXusMonitorName[Transmission], default='' +) +parameter_registry[TransformationPath] = StringParameter.from_type( + TransformationPath, default='' +) +parameter_registry[PixelMaskFilename] = MultiFilenameParameter.from_type( + PixelMaskFilename +) +parameter_registry[PixelShapePath] = StringParameter.from_type( + PixelShapePath, default='' +) +# Should this be ReductionMode (EventMode/HistogramMode)? +parameter_registry[ReturnEvents] = BooleanParameter.from_type( + ReturnEvents, default=False +) +parameter_registry[UncertaintyBroadcastMode] = ParamWithOptions.from_enum( + UncertaintyBroadcastMode, default=UncertaintyBroadcastMode.upper_bound +) +parameter_registry[Filename[SampleRun]] = FilenameParameter.from_type( + Filename[SampleRun] +) +parameter_registry[Filename[BackgroundRun]] = FilenameParameter.from_type( + Filename[BackgroundRun] +) +parameter_registry[Filename[TransmissionRun[SampleRun]]] = FilenameParameter.from_type( + Filename[TransmissionRun[SampleRun]] +) +parameter_registry[Filename[TransmissionRun[BackgroundRun]]] = ( + FilenameParameter.from_type(Filename[TransmissionRun[BackgroundRun]]) +) +parameter_registry[Filename[EmptyBeamRun]] = FilenameParameter.from_type( + Filename[EmptyBeamRun] +) +parameter_registry[WavelengthBins] = BinEdgesParameter( + WavelengthBins, dim='wavelength', start=2, stop=12.0, nbins=300, log=False +) +parameter_registry[QBins] = BinEdgesParameter( + QBins, dim='Q', start=0.1, stop=0.3, nbins=100, log=False +) +parameter_registry[QxBins] = BinEdgesParameter( + QxBins, dim='Qx', start=-0.5, stop=0.5, nbins=100 +) +parameter_registry[QyBins] = BinEdgesParameter( + QyBins, dim='Qy', start=-0.5, stop=0.5, nbins=100 +) +parameter_registry[DirectBeam] = StringParameter.from_type( + DirectBeam, switchable=True, optional=True, default=None +) +parameter_registry[DirectBeamFilename] = FilenameParameter.from_type( + DirectBeamFilename, switchable=True +) +parameter_registry[BeamCenter] = Vector2dParameter.from_type( + BeamCenter, default=sc.vector([0, 0, 0], unit='m') +) + +typical_outputs = ( + BackgroundSubtractedIofQ, + BackgroundSubtractedIofQxy, + IofQ[SampleRun], + IofQxy[SampleRun], + IofQ[BackgroundRun], + IofQxy[BackgroundRun], + MaskedData[BackgroundRun], + MaskedData[SampleRun], + WavelengthMonitor[SampleRun, Incident], + WavelengthMonitor[SampleRun, Transmission], + WavelengthMonitor[BackgroundRun, Incident], + WavelengthMonitor[BackgroundRun, Transmission], +) diff --git a/src/ess/sans/workflow.py b/src/ess/sans/workflow.py index 69406b54..1cd481ab 100644 --- a/src/ess/sans/workflow.py +++ b/src/ess/sans/workflow.py @@ -6,6 +6,8 @@ import sciline import scipp as sc +from ess.reduce.parameter import parameter_mappers + from .types import ( BackgroundRun, CleanSummedQ, @@ -123,3 +125,9 @@ def with_background_runs( List or tuple of background run filenames to set. """ return _set_runs(workflow, runs, BackgroundRun, 'background_run') + + +parameter_mappers[PixelMaskFilename] = with_pixel_mask_filenames +parameter_mappers[NeXusDetectorName] = with_banks +parameter_mappers[Filename[SampleRun]] = with_sample_runs +parameter_mappers[Filename[BackgroundRun]] = with_background_runs diff --git a/tests/loki/workflow_test.py b/tests/loki/workflow_test.py new file mode 100644 index 00000000..74900d5b --- /dev/null +++ b/tests/loki/workflow_test.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2024 Scipp contributors (https://github.com/scipp) + +import sys +from pathlib import Path + +import scipp as sc + +from ess import loki +from ess.loki import LokiAtLarmorWorkflow +from ess.reduce import workflow +from ess.sans.types import ( + BackgroundRun, + BackgroundSubtractedIofQ, + BeamCenter, + Filename, + IofQ, + PixelMaskFilename, + QBins, + ReturnEvents, + SampleRun, + 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 + assert LokiAtLarmorWorkflow in workflow.workflow_registry + count = len(workflow.workflow_registry) + + @workflow.register_workflow + class MyWorkflow: ... + + assert MyWorkflow in workflow.workflow_registry + assert len(workflow.workflow_registry) == count + 1 + + +def test_loki_workflow_parameters_returns_filtered_params(): + wf = LokiAtLarmorWorkflow() + parameters = workflow.get_parameters(wf, (IofQ[SampleRun],)) + assert Filename[SampleRun] in parameters + assert Filename[BackgroundRun] not in parameters + + +def test_loki_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(): + 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) + wf[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop + wf[PixelMaskFilename] = loki.data.loki_tutorial_mask_filenames()[0] + # For simplicity, insert a fake beam center instead of computing it. + wf[BeamCenter] = sc.vector([0.0, 0.0, 0.0], unit='m') + + result = wf.compute(BackgroundSubtractedIofQ) + assert result.dims == ('Q',) + assert sc.identical(result.coords['Q'], wf.compute(QBins)) + assert result.sizes['Q'] == 100