diff --git a/docs/user-guide/common/beam-center-finder.ipynb b/docs/user-guide/common/beam-center-finder.ipynb index d9b4d646..0a8fcbba 100644 --- a/docs/user-guide/common/beam-center-finder.ipynb +++ b/docs/user-guide/common/beam-center-finder.ipynb @@ -27,7 +27,6 @@ "source": [ "import scipp as sc\n", "import plopp as pp\n", - "import sciline\n", "from ess import sans\n", "from ess import isissans as isis\n", "from ess.sans.types import *" @@ -38,27 +37,23 @@ "id": "e84bd5a5-8bed-459b-ad8c-0d04aa6117e0", "metadata": {}, "source": [ - "## Setting up the pipeline\n", + "## Create and configure the workflow\n", "\n", - "We begin by setting some parameters for the pipeline,\n", + "We begin by setting creating a workflow and set some parameters,\n", "as well as the name of the data file we wish to use." ] }, { "cell_type": "code", "execution_count": null, - "id": "327a8b3c-fdde-45cd-9f29-dc33f9bb101d", + "id": "6bfb29ae", "metadata": {}, "outputs": [], "source": [ - "params = isis.default_parameters()\n", - "params[Filename[SampleRun]] = 'SANS2D00063114.nxs'\n", - "\n", - "# Build the pipeline\n", - "providers = list(\n", - " sans.providers + isis.providers + isis.data.providers + isis.sans2d.providers\n", - ")\n", - "pipeline = sciline.Pipeline(providers, params=params)" + "workflow = isis.sans2d.Sans2dTutorialWorkflow()\n", + "# For real data use:\n", + "# workflow = isis.sans2d.Sans2dWorkflow()\n", + "workflow[Filename[SampleRun]] = isis.data.sans2d_tutorial_sample_run()" ] }, { @@ -80,7 +75,7 @@ "metadata": {}, "outputs": [], "source": [ - "raw = pipeline.compute(RawData[SampleRun])['spectrum', :61440]\n", + "raw = workflow.compute(RawData[SampleRun])['spectrum', :61440]\n", "\n", "p = isis.plot_flat_detector_xy(raw.hist(), norm='log')\n", "p.ax.plot(0, 0, '+', color='k', ms=10)\n", @@ -132,7 +127,7 @@ "metadata": {}, "outputs": [], "source": [ - "masked = pipeline.compute(MaskedData[SampleRun])['spectrum', :61440].copy()\n", + "masked = workflow.compute(MaskedData[SampleRun])['spectrum', :61440].copy()\n", "masked.masks['low_counts'] = masked.hist().data < sc.scalar(80.0, unit='counts')\n", "\n", "p = isis.plot_flat_detector_xy(masked.hist(), norm='log')\n", @@ -161,8 +156,8 @@ "outputs": [], "source": [ "# Insert the provider that computes the center-of-mass of the pixels\n", - "pipeline.insert(sans.beam_center_finder.beam_center_from_center_of_mass)\n", - "pipeline.visualize(BeamCenter, graph_attr={'rankdir': 'LR'})" + "workflow.insert(sans.beam_center_finder.beam_center_from_center_of_mass)\n", + "workflow.visualize(BeamCenter, graph_attr={'rankdir': 'LR'})" ] }, { @@ -172,7 +167,7 @@ "metadata": {}, "outputs": [], "source": [ - "com = pipeline.compute(BeamCenter)\n", + "com = workflow.compute(BeamCenter)\n", "com" ] }, @@ -217,7 +212,7 @@ "1. Compute $I(Q)$ inside each quadrant and compute the residual difference between all 4 quadrants\n", "1. Iteratively move the center position and repeat 1. and 2. until all 4 $I(Q)$ curves lie on top of each other\n", "\n", - "For this, we need to set-up a fully-fledged pipeline that can compute $I(Q)$,\n", + "For this, we need to set-up a fully-fledged workflow that can compute $I(Q)$,\n", "which requires some additional parameters (see the [SANS2D reduction workflow](../isis/sans2d.ipynb) for more details)." ] }, @@ -239,6 +234,31 @@ "" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "eadbb74f", + "metadata": {}, + "outputs": [], + "source": [ + "workflow = isis.sans2d.Sans2dTutorialWorkflow()\n", + "# For real data use:\n", + "# workflow = isis.sans2d.Sans2dWorkflow()\n", + "\n", + "workflow.insert(isis.data.transmission_from_sample_run)\n", + "# Insert the provider that computes the center from I(Q) curves\n", + "workflow.insert(sans.beam_center_finder.beam_center_from_iofq)\n", + "workflow.visualize(BeamCenter, graph_attr={'rankdir': 'LR'})" + ] + }, + { + "cell_type": "markdown", + "id": "6e1c7413", + "metadata": {}, + "source": [ + "We set the missing input parameters:" + ] + }, { "cell_type": "code", "execution_count": null, @@ -246,35 +266,41 @@ "metadata": {}, "outputs": [], "source": [ - "params[WavelengthBins] = sc.linspace(\n", + "workflow[WavelengthBins] = sc.linspace(\n", " 'wavelength', start=2.0, stop=16.0, num=141, unit='angstrom'\n", ")\n", - "params[Filename[EmptyBeamRun]] = 'SANS2D00063091.nxs'\n", + "workflow[Filename[EmptyBeamRun]] = isis.data.sans2d_tutorial_empty_beam_run()\n", "\n", - "params[NeXusMonitorName[Incident]] = 'monitor2'\n", - "params[NeXusMonitorName[Transmission]] = 'monitor4'\n", + "workflow[NeXusMonitorName[Incident]] = 'monitor2'\n", + "workflow[NeXusMonitorName[Transmission]] = 'monitor4'\n", "\n", - "params[isis.SampleOffset] = sc.vector([0.0, 0.0, 0.053], unit='m')\n", - "params[isis.MonitorOffset[Transmission]] = sc.vector([0.0, 0.0, -6.719], unit='m')\n", + "workflow[isis.SampleOffset] = sc.vector([0.0, 0.0, 0.053], unit='m')\n", + "workflow[isis.MonitorOffset[Transmission]] = sc.vector([0.0, 0.0, -6.719], unit='m')\n", "\n", - "params[CorrectForGravity] = True\n", - "params[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound\n", - "params[sans.beam_center_finder.BeamCenterFinderQBins] = sc.linspace(\n", + "workflow[CorrectForGravity] = True\n", + "workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound\n", + "workflow[sans.beam_center_finder.BeamCenterFinderQBins] = sc.linspace(\n", " 'Q', 0.02, 0.25, 71, unit='1/angstrom'\n", - ")\n", - "\n", - "providers.append(isis.data.transmission_from_sample_run)\n", - "\n", - "# Build the pipeline\n", - "pipeline = sciline.Pipeline(providers, params=params)\n", - "\n", - "# Insert the provider that computes the center from I(Q) curves\n", - "pipeline.insert(sans.beam_center_finder.beam_center_from_iofq)\n", - "\n", - "# Override with data that contains the new mask\n", - "pipeline[MaskedData[SampleRun]] = masked\n", - "\n", - "pipeline.visualize(BeamCenter, graph_attr={'rankdir': 'LR'})" + ")" + ] + }, + { + "cell_type": "markdown", + "id": "641be8e9", + "metadata": {}, + "source": [ + "Finally, we set the data to be used, including overriding with data that contains the new mask defined earlier:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f772512", + "metadata": {}, + "outputs": [], + "source": [ + "workflow[Filename[SampleRun]] = isis.data.sans2d_tutorial_sample_run()\n", + "workflow[MaskedData[SampleRun]] = masked" ] }, { @@ -296,7 +322,7 @@ "metadata": {}, "outputs": [], "source": [ - "iofq_center = pipeline.compute(BeamCenter)\n", + "iofq_center = workflow.compute(BeamCenter)\n", "iofq_center" ] }, @@ -405,12 +431,12 @@ "source": [ "kwargs = dict(\n", " data=masked,\n", - " norm=pipeline.compute(NormWavelengthTerm[SampleRun]),\n", - " graph=pipeline.compute(sans.conversions.ElasticCoordTransformGraph),\n", - " q_bins=params[sans.beam_center_finder.BeamCenterFinderQBins],\n", - " wavelength_bins=params[WavelengthBins],\n", - " transform=pipeline.compute(LabFrameTransform[SampleRun]),\n", - " pixel_shape=pipeline.compute(DetectorPixelShape[SampleRun]),\n", + " norm=workflow.compute(NormWavelengthTerm[SampleRun]),\n", + " graph=workflow.compute(sans.conversions.ElasticCoordTransformGraph),\n", + " q_bins=workflow.compute(sans.beam_center_finder.BeamCenterFinderQBins),\n", + " wavelength_bins=workflow.compute(WavelengthBins),\n", + " transform=workflow.compute(LabFrameTransform[SampleRun]),\n", + " pixel_shape=workflow.compute(DetectorPixelShape[SampleRun]),\n", ")" ] }, @@ -572,10 +598,10 @@ "\n", "*Note*\n", "\n", - "The result obtained just above is slightly different from the one obtained earlier [using the pipeline](#Method-2:-computing-I(Q)-inside-4-quadrants).\n", + "The result obtained just above is slightly different from the one obtained earlier [using the workflow](#Method-2:-computing-I(Q)-inside-4-quadrants).\n", "\n", "This is because in our example, we used `x=0, y=0` as our initial guess,\n", - "while the pipeline uses an additional optimization where it first computes a better initial guess using method 1 (center-of-mass).\n", + "while the workflow uses an additional optimization where it first computes a better initial guess using method 1 (center-of-mass).\n", "This allows it to converge faster, with fewer iterations, and produce a slightly more accurate result.\n", "\n", "" @@ -597,7 +623,8 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" + "pygments_lexer": "ipython3", + "version": "3.10.12" } }, "nbformat": 4, diff --git a/docs/user-guide/isis/sans2d.ipynb b/docs/user-guide/isis/sans2d.ipynb index 2f097add..58ddad89 100644 --- a/docs/user-guide/isis/sans2d.ipynb +++ b/docs/user-guide/isis/sans2d.ipynb @@ -5,11 +5,11 @@ "id": "9a935df3-c816-4829-99c3-2afa979b7611", "metadata": {}, "source": [ - "# Sans2d data reduction\n", + "# Sans2d Data Reduction\n", "\n", "## Introduction\n", "\n", - "This notebook gives a concise overview of how to use the `esssans` package with Sciline, on the example of the data reduction of a Sans2d experiment.\n", + "This notebook gives an overview of how to use the `esssans` package with Sciline, on the example of the data reduction of a Sans2d experiment.\n", "We begin with relevant imports:" ] }, @@ -23,7 +23,6 @@ "outputs": [], "source": [ "import scipp as sc\n", - "import sciline\n", "import plopp as pp\n", "from ess import sans\n", "from ess import isissans as isis\n", @@ -35,9 +34,12 @@ "id": "3da2d397-6206-4ed1-a98f-11b3aaf7e5b0", "metadata": {}, "source": [ - "## Define reduction parameters\n", + "## Create and configure the workflow\n", "\n", - "We define the reduction parameters, with keys and types given by aliases or types defined in `ess.sans.types`:" + "We begin by creating the Sans2d workflow object (this is a [sciline.Pipeline](https://scipp.github.io/sciline/generated/classes/sciline.Pipeline.html) which can be consulted for advanced usage).\n", + "The Sans2d workflow uses Mantid to load files.\n", + "This tutorial comes with files that do not require Mantid, so we use a slightly modified workflow that does not require Mantid.\n", + "The workflow is otherwise identical to the full Mantid-based workflow:" ] }, { @@ -47,89 +49,122 @@ "metadata": {}, "outputs": [], "source": [ - "params = isis.default_parameters()\n", - "\n", - "params[DirectBeamFilename] = 'DIRECT_SANS2D_REAR_34327_4m_8mm_16Feb16.dat'\n", - "params[Filename[SampleRun]] = 'SANS2D00063114.nxs'\n", - "params[Filename[BackgroundRun]] = 'SANS2D00063159.nxs'\n", - "params[Filename[EmptyBeamRun]] = 'SANS2D00063091.nxs'\n", - "params[OutFilename] = 'reduced.nxs'\n", - "\n", - "params[NeXusMonitorName[Incident]] = 'monitor2'\n", - "params[NeXusMonitorName[Transmission]] = 'monitor4'\n", - "\n", - "params[isis.SampleOffset] = sc.vector([0.0, 0.0, 0.053], unit='m')\n", - "params[isis.MonitorOffset[Transmission]] = sc.vector([0.0, 0.0, -6.719], unit='m')\n", - "\n", - "params[WavelengthBins] = sc.linspace(\n", - " 'wavelength', start=2.0, stop=16.0, num=141, unit='angstrom'\n", - ")\n", - "\n", - "params[isis.sans2d.LowCountThreshold] = sc.scalar(100, unit='counts')\n", - "\n", - "mask_interval = sc.array(dims=['wavelength'], values=[2.21, 2.59], unit='angstrom')\n", - "params[WavelengthMask] = sc.DataArray(\n", - " sc.array(dims=['wavelength'], values=[True]),\n", - " coords={'wavelength': mask_interval},\n", - ")\n", - "\n", - "params[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.6, num=141, unit='1/angstrom')\n", - "params[NonBackgroundWavelengthRange] = sc.array(\n", - " dims=['wavelength'], values=[0.7, 17.1], unit='angstrom'\n", - ")\n", - "params[CorrectForGravity] = True\n", - "params[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound\n", - "params[ReturnEvents] = True" + "workflow = isis.sans2d.Sans2dTutorialWorkflow()\n", + "# For real data use:\n", + "# workflow = isis.sans2d.Sans2dWorkflow()" ] }, { "cell_type": "markdown", - "id": "c21564a8-e742-4183-9edc-2c70c51d5863", + "id": "6e08fb56", "metadata": {}, "source": [ - "## Create pipeline using Sciline\n", - "\n", - "We use all providers available in `ess.sans` as well as the `isis` and `sans2d`-specific providers, which include I/O and mask setup specific to the [Sans2d](https://www.isis.stfc.ac.uk/Pages/sans2d.aspx) instrument:" + "We can insert steps for configuring the workflow.\n", + "In this case, we would like to use the transmission monitor from the regular background and sample runs since there was no separate transmission run.\n", + "We also want to compute the beam center using a simple center-of-mass estimation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb55a3d4", + "metadata": {}, + "outputs": [], + "source": [ + "workflow.insert(isis.data.transmission_from_background_run)\n", + "workflow.insert(isis.data.transmission_from_sample_run)\n", + "workflow.insert(sans.beam_center_from_center_of_mass)" + ] + }, + { + "cell_type": "markdown", + "id": "d0dfa507", + "metadata": {}, + "source": [ + "The workflow can be visualized as a graph.\n", + "For readability we show only sub-workflow for computing `IofQ[Sample]`.\n", + "The workflow can actually compute the full `BackgroundSubtractedIofQ`, which applies and equivalent workflow to the background run, before a subtraction step:" ] }, { "cell_type": "code", "execution_count": null, - "id": "85b955ba-a54d-4760-bb35-af9cbe1ada90", + "id": "89212ffd", "metadata": {}, "outputs": [], "source": [ - "providers = (\n", - " sans.providers + isis.providers + isis.data.providers + isis.sans2d.providers\n", + "# left-right layout works better for this graph\n", + "workflow.visualize(IofQ[SampleRun], graph_attr={'rankdir': 'LR'})" + ] + }, + { + "cell_type": "markdown", + "id": "82319158", + "metadata": {}, + "source": [ + "Note the red boxes which indicate missing input parameters.\n", + "We can set these missing parameters, as well as parameters where we do not want to use the defaults:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a2df47b", + "metadata": {}, + "outputs": [], + "source": [ + "workflow[OutFilename] = 'reduced.nxs'\n", + "\n", + "workflow[NeXusMonitorName[Incident]] = 'monitor2'\n", + "workflow[NeXusMonitorName[Transmission]] = 'monitor4'\n", + "\n", + "workflow[isis.SampleOffset] = sc.vector([0.0, 0.0, 0.053], unit='m')\n", + "workflow[isis.MonitorOffset[Transmission]] = sc.vector([0.0, 0.0, -6.719], unit='m')\n", + "\n", + "workflow[WavelengthBins] = sc.linspace(\n", + " 'wavelength', start=2.0, stop=16.0, num=141, unit='angstrom'\n", ")\n", - "providers = providers + (\n", - " isis.data.transmission_from_background_run,\n", - " isis.data.transmission_from_sample_run,\n", - " sans.beam_center_from_center_of_mass,\n", + "\n", + "workflow[isis.sans2d.LowCountThreshold] = sc.scalar(100, unit='counts')\n", + "\n", + "mask_interval = sc.array(dims=['wavelength'], values=[2.21, 2.59], unit='angstrom')\n", + "workflow[WavelengthMask] = sc.DataArray(\n", + " sc.array(dims=['wavelength'], values=[True]),\n", + " coords={'wavelength': mask_interval},\n", ")\n", "\n", - "pipeline = sciline.Pipeline(providers=providers, params=params)" + "workflow[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.6, num=141, unit='1/angstrom')\n", + "workflow[NonBackgroundWavelengthRange] = sc.array(\n", + " dims=['wavelength'], values=[0.7, 17.1], unit='angstrom'\n", + ")\n", + "workflow[CorrectForGravity] = True\n", + "workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound\n", + "workflow[ReturnEvents] = True" ] }, { "cell_type": "markdown", - "id": "39f70669-0771-4a59-8e10-95c9120d0e9e", + "id": "67b18b0d", "metadata": {}, "source": [ - "## Visualize the pipeline\n", + "## Configuring data to load\n", "\n", - "Before we begin computations, we can visualize the pipeline:" + "We have not configured which files we want to load.\n", + "In this tutorial, we use helpers to fetch the tutorial data which return the filenames of the cached files.\n", + "In a real use case, you would set these parameters manually:" ] }, { "cell_type": "code", "execution_count": null, - "id": "d71780b8-56d5-445f-9d43-635d3d5f406b", + "id": "ff1658c5", "metadata": {}, "outputs": [], "source": [ - "# left-right layout works better for this graph\n", - "pipeline.visualize(BackgroundSubtractedIofQ, graph_attr={'rankdir': 'LR'})" + "workflow[DirectBeamFilename] = isis.data.sans2d_tutorial_direct_beam()\n", + "workflow[Filename[SampleRun]] = isis.data.sans2d_tutorial_sample_run()\n", + "workflow[Filename[BackgroundRun]] = isis.data.sans2d_tutorial_background_run()\n", + "workflow[Filename[EmptyBeamRun]] = isis.data.sans2d_tutorial_empty_beam_run()" ] }, { @@ -137,7 +172,7 @@ "id": "c19eeaf0", "metadata": {}, "source": [ - "## Use the pipeline\n", + "## Use the workflow\n", "\n", "### Compute final result\n", "\n", @@ -151,7 +186,7 @@ "metadata": {}, "outputs": [], "source": [ - "result = pipeline.compute(BackgroundSubtractedIofQ)\n", + "result = workflow.compute(BackgroundSubtractedIofQ)\n", "result.hist().plot(scale={'Q': 'log'}, norm='log')" ] }, @@ -190,10 +225,10 @@ "metadata": {}, "outputs": [], "source": [ - "pipeline[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop\n", - "result_drop = pipeline.compute(BackgroundSubtractedIofQ)\n", + "workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop\n", + "result_drop = workflow.compute(BackgroundSubtractedIofQ)\n", "# Reset the UnsertaintyBroadcastMode to the old value\n", - "pipeline[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound\n", + "workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound\n", "sc.DataGroup(upper_bound=result, dropped=result_drop).hist().plot(norm='log')" ] }, @@ -218,7 +253,7 @@ "source": [ "from ess.sans.io import save_background_subtracted_iofq\n", "\n", - "pipeline.bind_and_call(save_background_subtracted_iofq)" + "workflow.bind_and_call(save_background_subtracted_iofq)" ] }, { @@ -250,7 +285,7 @@ "iofqs = (IofQ[SampleRun], IofQ[BackgroundRun], BackgroundSubtractedIofQ)\n", "keys = monitors + (MaskedData[SampleRun],) + parts + iofqs\n", "\n", - "results = pipeline.compute(keys)\n", + "results = workflow.compute(keys)\n", "\n", "display(sc.plot({str(key): results[key] for key in monitors}, norm='log'))\n", "\n", @@ -260,7 +295,7 @@ " )\n", ")\n", "\n", - "wavelength = pipeline.compute(WavelengthBins)\n", + "wavelength = workflow.compute(WavelengthBins)\n", "display(\n", " results[CleanSummedQ[SampleRun, Numerator]]\n", " .hist(wavelength=wavelength)\n", @@ -310,8 +345,8 @@ " },\n", " index=[Mode('upper_bound'), Mode('drop')],\n", ")\n", - "pipeline.set_param_table(param_table)\n", - "results = pipeline.compute(sciline.Series[Mode, BackgroundSubtractedIofQ])\n", + "workflow.set_param_table(param_table)\n", + "results = workflow.compute(sciline.Series[Mode, BackgroundSubtractedIofQ])\n", "sc.DataGroup(results).hist().plot(norm='log')" ] }, @@ -336,7 +371,7 @@ "metadata": {}, "outputs": [], "source": [ - "pipeline[WavelengthBands] = sc.linspace(\n", + "workflow[WavelengthBands] = sc.linspace(\n", " 'wavelength', start=2.0, stop=16.0, num=11, unit='angstrom'\n", ")" ] @@ -356,7 +391,7 @@ "metadata": {}, "outputs": [], "source": [ - "result = pipeline.compute(BackgroundSubtractedIofQ)\n", + "result = workflow.compute(BackgroundSubtractedIofQ)\n", "result" ] }, @@ -377,100 +412,6 @@ "source": [ "pp.plot(sc.collapse(result.hist(), keep='Q'), norm='log')" ] - }, - { - "cell_type": "markdown", - "id": "0977a3dc-eb5f-4067-80c5-cc19137cc915", - "metadata": {}, - "source": [ - "## Loading local files\n", - "\n", - "The data files used above are hosted on an external server, and downloaded on-the-fly (and cached) when computing the result.\n", - "\n", - "It is also possible to load local files from your hard drive, by using the `DataFolder` parameter.\n", - "We also need to insert the `isis.io.to_path` provider which supplies the file paths to the files in the folder.\n", - "\n", - "As an example, we will save our current direct beam to disk, and then re-load it using a pipeline that reads local files.\n", - "\n", - "**Note** that is it not currently possible to mix local and cloud files in the same pipeline with the present setup." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "871743bf-32d0-4df1-a285-ba722a4198ef", - "metadata": {}, - "outputs": [], - "source": [ - "# Direct beam computation currently uses the `get_path` provider which\n", - "# fetches files from the remote server\n", - "direct_beam = pipeline.get(DirectBeam)\n", - "direct_beam.visualize()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f35f4d14-58f2-437f-826c-b9ac74570e52", - "metadata": {}, - "outputs": [], - "source": [ - "# Save the direct beam to disk\n", - "db_filename = 'my_local_direct_beam_file.h5'\n", - "direct_beam.compute().save_hdf5(db_filename)" - ] - }, - { - "cell_type": "markdown", - "id": "8f712a5d-cab9-4878-9b74-5a6c9751e403", - "metadata": {}, - "source": [ - "We now modify our pipeline by setting the `DataFolder` parameter,\n", - "as well as our new direct beam filename. Finally, we insert the local file provider `to_path`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "27432974-0f06-4549-8ea5-4f75ff42fbc5", - "metadata": {}, - "outputs": [], - "source": [ - "pipeline[DataFolder] = '.'\n", - "pipeline[DirectBeamFilename] = db_filename\n", - "\n", - "# Insert provider for local files\n", - "pipeline.insert(isis.io.to_path)" - ] - }, - { - "cell_type": "markdown", - "id": "37be7d9a-92ec-468c-a36e-1c73682b1bda", - "metadata": {}, - "source": [ - "We can now see that `to_path` uses both the file name and the local folder to create a file path:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5647adf-5606-4efa-9e18-2c2943d8250d", - "metadata": {}, - "outputs": [], - "source": [ - "db_local = pipeline.get(DirectBeam)\n", - "db_local.visualize()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "446ef2a3-bf7d-404b-b493-0c65f71c1a03", - "metadata": {}, - "outputs": [], - "source": [ - "db_local.compute().plot()" - ] } ], "metadata": { @@ -488,7 +429,8 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" + "pygments_lexer": "ipython3", + "version": "3.10.12" } }, "nbformat": 4, diff --git a/docs/user-guide/isis/zoom.ipynb b/docs/user-guide/isis/zoom.ipynb index 378efd70..ea5c39f5 100644 --- a/docs/user-guide/isis/zoom.ipynb +++ b/docs/user-guide/isis/zoom.ipynb @@ -22,17 +22,9 @@ "There are a few things that are not yet handled:\n", "\n", "- TOF or wavelength masks\n", - "- Position corrections from user file (not automatically, have manual sample and detector bank offsets)" - ] - }, - { - "cell_type": "markdown", - "id": "09234b87", - "metadata": {}, - "source": [ - "## Setup\n", + "- Position corrections from user file (not automatically, have manual sample and detector bank offsets)\n", "\n", - "### Imports and configuration" + "We begin with relevant imports:" ] }, { @@ -43,7 +35,6 @@ "outputs": [], "source": [ "import scipp as sc\n", - "import sciline\n", "from ess import sans\n", "from ess import isissans as isis\n", "from ess.sans.types import *" @@ -51,160 +42,143 @@ }, { "cell_type": "markdown", - "id": "64783c51", + "id": "d1bb87a9", "metadata": {}, "source": [ - "### Setup input files" + "## Create and configure the workflow\n", + "\n", + "We begin by creating the Zoom workflow object (this is a [sciline.Pipeline](https://scipp.github.io/sciline/generated/classes/sciline.Pipeline.html) which can be consulted for advanced usage).\n", + "The Zoom workflow uses Mantid to load files.\n", + "This tutorial comes with files that do not require Mantid, so we use a slightly modified workflow that does not require Mantid.\n", + "The workflow is otherwise identical to the full Mantid-based workflow:" ] }, { "cell_type": "code", "execution_count": null, - "id": "9e7cfc82", + "id": "955bbc91", "metadata": {}, "outputs": [], "source": [ - "params = isis.default_parameters()\n", - "params.update(\n", - " {\n", - " DirectBeamFilename: 'Direct_Zoom_4m_8mm_100522.txt',\n", - " isis.CalibrationFilename: '192tubeCalibration_11-02-2019_r5_10lines.nxs',\n", - " Filename[SampleRun]: 'ZOOM00034786.nxs',\n", - " Filename[EmptyBeamRun]: 'ZOOM00034787.nxs',\n", - " isis.SampleOffset: sc.vector([0.0, 0.0, 0.11], unit='m'),\n", - " isis.DetectorBankOffset: sc.vector([0.0, 0.0, 0.5], unit='m'),\n", - " }\n", - ")\n", - "masks = [\n", - " 'andru_test.xml',\n", - " 'left_beg_18_2.xml',\n", - " 'right_beg_18_2.xml',\n", - " 'small_bs_232.xml',\n", - " 'small_BS_31032023.xml',\n", - " 'tube_1120_bottom.xml',\n", - " 'tubes_beg_18_2.xml',\n", - "]" + "workflow = isis.zoom.ZoomTutorialWorkflow()\n", + "# For real data use:\n", + "# workflow = isis.zoom.ZoomWorkflow()" ] }, { "cell_type": "markdown", - "id": "8a8a4e01", + "id": "be9f1b06", "metadata": {}, "source": [ - "### Setup reduction parameters" + "We can insert steps for configuring the workflow.\n", + "In this case, we would like to use the transmission monitor from the regular background and sample runs since there was no separate transmission run.\n", + "We also want to compute the beam center using a simple center-of-mass estimation:" ] }, { "cell_type": "code", "execution_count": null, - "id": "50f11eb2", + "id": "0f199e59", "metadata": {}, "outputs": [], "source": [ - "params[NeXusMonitorName[Incident]] = 'monitor3'\n", - "params[NeXusMonitorName[Transmission]] = 'monitor5'\n", - "\n", - "params[WavelengthBins] = sc.geomspace(\n", - " 'wavelength', start=1.75, stop=16.5, num=141, unit='angstrom'\n", - ")\n", - "\n", - "params[QBins] = sc.geomspace(dim='Q', start=0.004, stop=0.8, num=141, unit='1/angstrom')\n", - "\n", - "params[NonBackgroundWavelengthRange] = sc.array(\n", - " dims=['wavelength'], values=[0.7, 17.1], unit='angstrom'\n", - ")\n", - "params[CorrectForGravity] = True\n", - "params[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound\n", - "params[ReturnEvents] = False" + "workflow.insert(isis.data.transmission_from_background_run)\n", + "workflow.insert(isis.data.transmission_from_sample_run)\n", + "workflow.insert(sans.beam_center_from_center_of_mass)" ] }, { "cell_type": "markdown", - "id": "0b47024f", + "id": "b2a3c480", "metadata": {}, "source": [ - "### Setup reduction pipeline" + "The workflow lacks some input parameters, as well as parameters where we do not want to use the defaults, which we can set now:" ] }, { "cell_type": "code", "execution_count": null, - "id": "fdcaec37", + "id": "50f11eb2", "metadata": {}, "outputs": [], "source": [ - "providers = sans.providers + isis.providers + (isis.io.read_xml_detector_masking,)\n", - "providers = providers + (\n", - " isis.data.transmission_from_background_run,\n", - " isis.data.transmission_from_sample_run,\n", - " sans.beam_center_from_center_of_mass,\n", + "workflow[NeXusMonitorName[Incident]] = 'monitor3'\n", + "workflow[NeXusMonitorName[Transmission]] = 'monitor5'\n", + "\n", + "workflow[WavelengthBins] = sc.geomspace(\n", + " 'wavelength', start=1.75, stop=16.5, num=141, unit='angstrom'\n", + ")\n", + "\n", + "workflow[QBins] = sc.geomspace(\n", + " dim='Q', start=0.004, stop=0.8, num=141, unit='1/angstrom'\n", + ")\n", + "\n", + "workflow[NonBackgroundWavelengthRange] = sc.array(\n", + " dims=['wavelength'], values=[0.7, 17.1], unit='angstrom'\n", ")\n", - "pipeline = sciline.Pipeline(providers, params=params)\n", - "pipeline.set_param_series(PixelMaskFilename, masks)" + "workflow[CorrectForGravity] = True\n", + "workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound\n", + "workflow[ReturnEvents] = False" ] }, { "cell_type": "markdown", - "id": "951aec10", + "id": "1f22567f", "metadata": {}, "source": [ - "If Mantid is available, we can use it to load data files.\n", - "**You must configure the** `DataFolder` **below to point to the directory containing the data files.**\n", - "Otherwise, we fall back to load intermediate data files that have been prepared for the concrete example in this notebook.\n", - "If you want to use the workflow with different files you must have Mantid installed:" + "## Configuring data to load\n", + "\n", + "We have not configured which files we want to load.\n", + "In this tutorial, we use helpers to fetch the tutorial data which return the filenames of the cached files.\n", + "In a real use case, you would set these parameters manually:" ] }, { "cell_type": "code", "execution_count": null, - "id": "66237b3e", + "id": "9e7cfc82", "metadata": {}, "outputs": [], "source": [ - "try:\n", - " from mantid import ConfigService\n", - " import ess.isissans.mantidio\n", - "\n", - " cfg = ConfigService.Instance()\n", - " cfg.setLogLevel(3) # Silence verbose load via Mantid\n", - "\n", - " pipeline[DataFolder] = 'zoom_data'\n", - " for provider in isis.mantidio.providers:\n", - " pipeline.insert(provider)\n", - "except ImportError:\n", - " import ess.isissans.io\n", - "\n", - " for provider in isis.data.providers:\n", - " pipeline.insert(provider)" + "workflow[DirectBeamFilename] = isis.data.zoom_tutorial_direct_beam()\n", + "workflow[isis.CalibrationFilename] = isis.data.zoom_tutorial_calibration()\n", + "workflow[Filename[SampleRun]] = isis.data.zoom_tutorial_sample_run()\n", + "workflow[Filename[EmptyBeamRun]] = isis.data.zoom_tutorial_empty_beam_run()\n", + "workflow[isis.SampleOffset] = sc.vector([0.0, 0.0, 0.11], unit='m')\n", + "workflow[isis.DetectorBankOffset] = sc.vector([0.0, 0.0, 0.5], unit='m')\n", + "masks = isis.data.zoom_tutorial_mask_filenames()\n", + "workflow.set_param_series(PixelMaskFilename, masks)" ] }, { "cell_type": "markdown", - "id": "703ffc1e", + "id": "a75ee3ca", "metadata": {}, "source": [ - "## Reduction\n", - "\n", - "### The reduction workflow" + "The workflow can be visualized as a graph:" ] }, { "cell_type": "code", "execution_count": null, - "id": "029e7c10-3428-4c10-8a5d-fa49b807a785", + "id": "cb572094", "metadata": {}, "outputs": [], "source": [ - "iofq = pipeline.get(IofQ[SampleRun])\n", - "iofq.visualize(graph_attr={'rankdir': 'LR'}, compact=True)" + "# left-right layout works better for this graph\n", + "workflow.visualize(IofQ[SampleRun], graph_attr={'rankdir': 'LR'})" ] }, { "cell_type": "markdown", - "id": "77687728", + "id": "02c5969c", "metadata": {}, "source": [ - "### Running the workflow" + "## Use the workflow\n", + "\n", + "### Compute final result\n", + "\n", + "We can now compute $I(Q)$:" ] }, { @@ -214,7 +188,7 @@ "metadata": {}, "outputs": [], "source": [ - "da = iofq.compute()\n", + "da = workflow.compute(IofQ[SampleRun])\n", "da.plot(norm='log', scale={'Q': 'log'})" ] }, @@ -223,7 +197,7 @@ "id": "5a7526fc", "metadata": {}, "source": [ - "### Inspecting intermediate results" + "### Compute intermediate results" ] }, { @@ -241,7 +215,7 @@ "iofqs = (IofQ[SampleRun],)\n", "keys = monitors + (MaskedData[SampleRun],) + parts + iofqs\n", "\n", - "results = pipeline.compute(keys)\n", + "results = workflow.compute(keys)\n", "\n", "display(sc.plot({str(key): results[key] for key in monitors}, norm='log'))\n", "\n", @@ -251,7 +225,7 @@ " )\n", ")\n", "\n", - "wavelength = pipeline.compute(WavelengthBins)\n", + "wavelength = workflow.compute(WavelengthBins)\n", "display(\n", " results[CleanSummedQ[SampleRun, Numerator]]\n", " .hist(wavelength=wavelength)\n", @@ -289,10 +263,10 @@ "metadata": {}, "outputs": [], "source": [ - "pipeline[QxBins] = sc.linspace('Qx', start=-0.5, stop=0.5, num=101, unit='1/angstrom')\n", - "pipeline[QyBins] = sc.linspace('Qy', start=-0.8, stop=0.8, num=101, unit='1/angstrom')\n", + "workflow[QxBins] = sc.linspace('Qx', start=-0.5, stop=0.5, num=101, unit='1/angstrom')\n", + "workflow[QyBins] = sc.linspace('Qy', start=-0.8, stop=0.8, num=101, unit='1/angstrom')\n", "\n", - "iqxqy = pipeline.compute(IofQxy[SampleRun])\n", + "iqxqy = workflow.compute(IofQxy[SampleRun])\n", "iqxqy.plot(norm='log', aspect='equal')" ] } diff --git a/docs/user-guide/loki/loki-direct-beam.ipynb b/docs/user-guide/loki/loki-direct-beam.ipynb index 733e747f..f7339008 100644 --- a/docs/user-guide/loki/loki-direct-beam.ipynb +++ b/docs/user-guide/loki/loki-direct-beam.ipynb @@ -9,7 +9,7 @@ "\n", "## Introduction\n", "\n", - "This notebook is used to compute the direct beam function for the LoKI detectors.\n", + "This notebook is used to compute the direct beam function for the [LoKI](https://europeanspallationsource.se/instruments/loki) detectors.\n", "It uses data recorded during the detector test at the Larmor instrument.\n", "\n", "**Description of the procedure:**\n", @@ -20,10 +20,10 @@ "\n", "In the following notebook, we will:\n", "\n", - "1. Create a pipeline to compute $I(Q)$ inside a set of wavelength bands (the number of wavelength bands will be the number of data points in the final direct beam function)\n", + "1. Create a workflow to compute $I(Q)$ inside a set of wavelength bands (the number of wavelength bands will be the number of data points in the final direct beam function)\n", "1. Create a flat direct beam function, as a function of wavelength, with wavelength bins corresponding to the wavelength bands\n", "1. Calculate inside each band by how much one would have to multiply the final $I(Q)$ so that the curve would overlap with the full-range curve\n", - " (we compute the full-range data by making a copy of the pipeline but setting only a single wavelength band that contains all wavelengths)\n", + " (we compute the full-range data by making a copy of the workflow but setting only a single wavelength band that contains all wavelengths)\n", "1. Multiply the direct beam values inside each wavelength band by this factor\n", "1. Compare the full-range $I(Q)$ to a theoretical reference and add the corresponding additional scaling to the direct beam function\n", "1. Iterate until the changes to the direct beam function become small" @@ -51,104 +51,131 @@ }, { "cell_type": "markdown", - "id": "c21564a8-e742-4183-9edc-2c70c51d5863", + "id": "446d14b9", "metadata": {}, "source": [ - "## Define reduction parameters\n", + "## Create and configure the workflow\n", "\n", - "We define a dictionary containing the reduction parameters, with keys and types given by aliases or types defined in `ess.sans.types`:" + "We begin by creating the Loki workflow object (this is a [sciline.Pipeline](https://scipp.github.io/sciline/generated/classes/sciline.Pipeline.html) which can be consulted for advanced usage).\n", + "The files we use here come from a Loki detector test at Larmor, so we use the corresponding workflow:" ] }, { "cell_type": "code", "execution_count": null, - "id": "ce2841f0-bd9e-43c3-8cc5-52bb45f392ad", + "id": "63ceda7f", "metadata": {}, "outputs": [], "source": [ - "params = loki.default_parameters()\n", - "\n", - "# File names\n", - "params[Filename[SampleRun]] = '60339-2022-02-28_2215.nxs'\n", - "params[Filename[BackgroundRun]] = '60393-2022-02-28_2215.nxs'\n", - "params[Filename[TransmissionRun[SampleRun]]] = '60394-2022-02-28_2215.nxs'\n", - "params[Filename[TransmissionRun[BackgroundRun]]] = '60392-2022-02-28_2215.nxs'\n", - "params[Filename[EmptyBeamRun]] = '60392-2022-02-28_2215.nxs'\n", - "\n", - "# Wavelength binning parameters\n", - "wavelength_min = sc.scalar(1.0, unit='angstrom')\n", - "wavelength_max = sc.scalar(13.0, unit='angstrom')\n", - "n_wavelength_bins = 50\n", - "n_wavelength_bands = 50\n", - "\n", - "params[WavelengthBins] = sc.linspace(\n", - " 'wavelength', wavelength_min, wavelength_max, n_wavelength_bins + 1\n", - ")\n", - "params[WavelengthBands] = sc.linspace(\n", - " 'wavelength', wavelength_min, wavelength_max, n_wavelength_bands + 1\n", - ")\n", - "\n", - "masks = ['mask_new_July2022.xml']\n", + "workflow = loki.LokiAtLarmorWorkflow()" + ] + }, + { + "cell_type": "markdown", + "id": "d8fe14da", + "metadata": {}, + "source": [ + "We configure the workflow be defining the series of masks filenames and bank names to reduce.\n", + "In this case there is just a single bank:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8f42605", + "metadata": {}, + "outputs": [], + "source": [ + "masks = loki.data.loki_tutorial_mask_filenames()\n", "banks = ['larmor_detector']\n", - "\n", - "params[CorrectForGravity] = True\n", - "params[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound\n", - "params[ReturnEvents] = False\n", - "\n", - "params[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.3, num=101, unit='1/angstrom')" + "workflow.insert(sans.merge_banks)\n", + "workflow.set_param_series(PixelMaskFilename, masks)\n", + "workflow.set_param_series(NeXusDetectorName, banks)" ] }, { "cell_type": "markdown", - "id": "f2c3b542", + "id": "eb90e677", "metadata": {}, "source": [ - "## Create pipeline using Sciline\n", - "\n", - "We use all providers available in `esssans` as well as the `loki`-specific providers,\n", - "which include I/O and mask setup specific to the [LoKI](https://europeanspallationsource.se/instruments/loki) instrument.\n", - "\n", - "We then build the pipeline which can be used to compute the (background subtracted) $I(Q)$." + "The workflow can be visualized as a graph.\n", + "For readability we show only sub-workflow for computing `IofQ[Sample]`.\n", + "The workflow can actually compute the full `BackgroundSubtractedIofQ`, which applies and equivalent workflow to the background run, before a subtraction step:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc988a34", + "metadata": {}, + "outputs": [], + "source": [ + "workflow.visualize(IofQ[SampleRun], compact=True, graph_attr={'rankdir': 'LR'})" + ] + }, + { + "cell_type": "markdown", + "id": "abde8696", + "metadata": {}, + "source": [ + "Note the red boxes which indicate missing input parameters.\n", + "We can set these missing parameters, as well as parameters where we do not want to use the defaults:" ] }, { "cell_type": "code", "execution_count": null, - "id": "0c1e0e90-da33-45e7-a464-4799f4fbc657", + "id": "ce2841f0-bd9e-43c3-8cc5-52bb45f392ad", "metadata": {}, "outputs": [], "source": [ - "providers = sans.providers + loki.providers + (isis.io.read_xml_detector_masking,)\n", + "# Wavelength binning parameters\n", + "wavelength_min = sc.scalar(1.0, unit='angstrom')\n", + "wavelength_max = sc.scalar(13.0, unit='angstrom')\n", + "n_wavelength_bins = 50\n", + "n_wavelength_bands = 50\n", + "\n", + "workflow[WavelengthBins] = sc.linspace(\n", + " 'wavelength', wavelength_min, wavelength_max, n_wavelength_bins + 1\n", + ")\n", + "workflow[WavelengthBands] = sc.linspace(\n", + " 'wavelength', wavelength_min, wavelength_max, n_wavelength_bands + 1\n", + ")\n", "\n", - "pipeline = sciline.Pipeline(providers, params=params)\n", - "pipeline.insert(sans.merge_banks)\n", - "pipeline.set_param_series(PixelMaskFilename, masks)\n", - "pipeline.set_param_series(NeXusDetectorName, banks)\n", "\n", - "# Add providers that fetch data from online resource\n", - "for provider in loki.data.providers:\n", - " pipeline.insert(provider)\n", + "workflow[CorrectForGravity] = True\n", + "workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound\n", + "workflow[ReturnEvents] = False\n", "\n", - "# In the present file, there is no sample information so we use a dummy sample provider\n", - "pipeline.insert(loki.io.dummy_load_sample)" + "workflow[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.3, num=101, unit='1/angstrom')" ] }, { "cell_type": "markdown", - "id": "805c22ea-216f-4ae4-bcbb-9eb31656cfef", + "id": "11e6d962", "metadata": {}, "source": [ - "Before we begin computations, we can visualize the pipeline:" + "## Configuring data to load\n", + "\n", + "We have not configured which files we want to load.\n", + "In this tutorial, we use helpers to fetch the tutorial data which return the filenames of the cached files.\n", + "In a real use case, you would set these parameters manually:" ] }, { "cell_type": "code", "execution_count": null, - "id": "6eca12c4-e28c-4e45-8d3d-c5869746086b", + "id": "153ae0ca", "metadata": {}, "outputs": [], "source": [ - "pipeline.visualize(BackgroundSubtractedIofQ, compact=True, graph_attr={'rankdir': 'LR'})" + "workflow[Filename[SampleRun]] = loki.data.loki_tutorial_sample_run_60339()\n", + "workflow[Filename[BackgroundRun]] = loki.data.loki_tutorial_background_run_60393()\n", + "workflow[Filename[TransmissionRun[SampleRun]]] = (\n", + " loki.data.loki_tutorial_sample_transmission_run()\n", + ")\n", + "workflow[Filename[TransmissionRun[BackgroundRun]]] = loki.data.loki_tutorial_run_60392()\n", + "workflow[Filename[EmptyBeamRun]] = loki.data.loki_tutorial_run_60392()" ] }, { @@ -158,13 +185,13 @@ "source": [ "## Finding the beam center\n", "\n", - "Looking carefully at the pipeline above,\n", - "one will notice that there is a missing parameter from the pipeline: the red box that contains the `BeamCenter` type.\n", + "Looking carefully at the workflow above,\n", + "one will notice that there is a missing parameter from the workflow: the red box that contains the `BeamCenter` type.\n", "Before we can proceed with computing the direct beam function,\n", "we therefore have to first determine the center of the beam.\n", "\n", "There are more details on how this is done in the [beam center finder notebook](../common/beam-center-finder.ipynb),\n", - "but for now we simply reuse the pipeline (by making a copy),\n", + "but for now we simply reuse the workflow (by making a copy),\n", "and inserting the provider that will compute the beam center.\n", "\n", "For now, we compute the beam center only for the rear detector (named 'larmor_detector') but apply it to all banks (currently there is only one bank).\n", @@ -178,10 +205,10 @@ "metadata": {}, "outputs": [], "source": [ - "bc_pipeline = pipeline.copy()\n", - "bc_pipeline.del_param_table(NeXusDetectorName)\n", - "bc_pipeline[NeXusDetectorName] = 'larmor_detector'\n", - "bc_pipeline.insert(sans.beam_center_from_center_of_mass)" + "bc_workflow = workflow.copy()\n", + "bc_workflow.del_param_table(NeXusDetectorName)\n", + "bc_workflow[NeXusDetectorName] = 'larmor_detector'\n", + "bc_workflow.insert(sans.beam_center_from_center_of_mass)" ] }, { @@ -191,7 +218,7 @@ "metadata": {}, "outputs": [], "source": [ - "bc_pipeline.visualize(BeamCenter, compact=True, graph_attr={'rankdir': 'LR'})" + "bc_workflow.visualize(BeamCenter, compact=True, graph_attr={'rankdir': 'LR'})" ] }, { @@ -209,7 +236,7 @@ "metadata": {}, "outputs": [], "source": [ - "center = bc_pipeline.compute(BeamCenter)\n", + "center = bc_workflow.compute(BeamCenter)\n", "center" ] }, @@ -218,7 +245,7 @@ "id": "0814805a-9611-4951-a015-c12ae254b099", "metadata": {}, "source": [ - "and set that value onto our original pipeline" + "and set that value onto our original workflow" ] }, { @@ -228,7 +255,7 @@ "metadata": {}, "outputs": [], "source": [ - "pipeline[BeamCenter] = center" + "workflow[BeamCenter] = center" ] }, { @@ -253,11 +280,10 @@ "outputs": [], "source": [ "from scipp.scipy.interpolate import interp1d\n", - "from ess.loki.data import get_path\n", "\n", - "Iq_theory = sc.io.load_hdf5(get_path('PolyGauss_I0-50_Rg-60.h5'))\n", + "Iq_theory = sc.io.load_hdf5(loki.data.loki_tutorial_poly_gauss_I0())\n", "f = interp1d(Iq_theory, 'Q')\n", - "I0 = f(sc.midpoints(params[QBins])).data[0]\n", + "I0 = f(sc.midpoints(workflow.compute(QBins))).data[0]\n", "I0" ] }, @@ -282,7 +308,7 @@ "metadata": {}, "outputs": [], "source": [ - "results = sans.direct_beam(pipeline=pipeline, I0=I0, niter=6)\n", + "results = sans.direct_beam(workflow=workflow, I0=I0, niter=6)\n", "# Unpack the final result\n", "iofq_full = results[-1]['iofq_full']\n", "iofq_bands = results[-1]['iofq_bands']\n", @@ -378,7 +404,7 @@ "metadata": {}, "outputs": [], "source": [ - "pipeline[DimsToKeep] = ['layer']" + "workflow[DimsToKeep] = ['layer']" ] }, { @@ -396,7 +422,7 @@ "metadata": {}, "outputs": [], "source": [ - "results_layers = sans.direct_beam(pipeline=pipeline, I0=I0, niter=6)\n", + "results_layers = sans.direct_beam(workflow=workflow, I0=I0, niter=6)\n", "# Unpack the final result\n", "iofq_full_layers = results_layers[-1]['iofq_full']\n", "iofq_bands_layers = results_layers[-1]['iofq_bands']\n", @@ -503,11 +529,14 @@ "metadata": {}, "outputs": [], "source": [ - "del params[Filename[SampleRun]]\n", - "del params[Filename[BackgroundRun]]\n", - "\n", - "sample_runs = ['60250-2022-02-28_2215.nxs', '60339-2022-02-28_2215.nxs']\n", - "background_runs = ['60248-2022-02-28_2215.nxs', '60393-2022-02-28_2215.nxs']" + "sample_runs = [\n", + " loki.data.loki_tutorial_sample_run_60250(),\n", + " loki.data.loki_tutorial_sample_run_60339(),\n", + "]\n", + "background_runs = [\n", + " loki.data.loki_tutorial_background_run_60248(),\n", + " loki.data.loki_tutorial_background_run_60393(),\n", + "]" ] }, { @@ -515,7 +544,7 @@ "id": "c3e2ff3d-2b3d-4ba7-b4ca-f0a5617d22dc", "metadata": {}, "source": [ - "We now construct a new pipeline, inserting parameter series and merging providers:" + "We update the workflow, inserting parameter series and merging providers:" ] }, { @@ -525,24 +554,15 @@ "metadata": {}, "outputs": [], "source": [ - "# Same as original pipeline, but without sample and background run file names\n", - "params[BeamCenter] = center\n", - "pipeline = sciline.Pipeline(providers, params=params)\n", - "for provider in loki.data.providers:\n", - " pipeline.insert(provider)\n", - "pipeline.insert(sans.merge_banks)\n", - "pipeline.set_param_series(PixelMaskFilename, masks)\n", - "pipeline.set_param_series(NeXusDetectorName, banks)\n", + "# Reset workflow\n", + "workflow[DimsToKeep] = []\n", "\n", "# Set parameter series for file names\n", - "pipeline.set_param_series(Filename[SampleRun], sample_runs)\n", - "pipeline.set_param_series(Filename[BackgroundRun], background_runs)\n", + "workflow.set_param_series(Filename[SampleRun], sample_runs)\n", + "workflow.set_param_series(Filename[BackgroundRun], background_runs)\n", "\n", "# Add event merging provider\n", - "pipeline.insert(sans.merge_runs)\n", - "\n", - "# Add dummy sample provider\n", - "pipeline.insert(loki.io.dummy_load_sample)" + "workflow.insert(sans.merge_runs)" ] }, { @@ -550,7 +570,7 @@ "id": "6a50ef4f-a15c-4ae4-a8e7-666483a93da0", "metadata": {}, "source": [ - "If we now visualize the pipeline,\n", + "If we now visualize the workflow,\n", "we can see that every step for the `SampleRun` and `BackgroundRun` branches are now series of types (3D-looking boxes instead of flat rectangles).\n", "There is also the new `merge_multiple_runs` step that combines the events from the two runs,\n", "just before the normalization." @@ -563,7 +583,7 @@ "metadata": {}, "outputs": [], "source": [ - "pipeline.visualize(BackgroundSubtractedIofQ, compact=True, graph_attr={'rankdir': 'LR'})" + "workflow.visualize(BackgroundSubtractedIofQ, compact=True, graph_attr={'rankdir': 'LR'})" ] }, { @@ -581,7 +601,7 @@ "metadata": {}, "outputs": [], "source": [ - "results = sans.direct_beam(pipeline=pipeline, I0=I0, niter=6)\n", + "results = sans.direct_beam(workflow=workflow, I0=I0, niter=6)\n", "# Unpack the final result\n", "iofq_full_new = results[-1]['iofq_full']\n", "iofq_bands_new = results[-1]['iofq_bands']\n", diff --git a/src/ess/isissans/__init__.py b/src/ess/isissans/__init__.py index b5cf95af..698d08e7 100644 --- a/src/ess/isissans/__init__.py +++ b/src/ess/isissans/__init__.py @@ -3,7 +3,7 @@ import importlib.metadata -from . import components, data, general, io, sans2d +from . import components, data, general, io, sans2d, zoom from .components import DetectorBankOffset, MonitorOffset, SampleOffset from .general import default_parameters from .io import CalibrationFilename @@ -21,7 +21,6 @@ __all__ = [ 'CalibrationFilename', 'DetectorBankOffset', - 'apply_component_user_offsets_to_raw_data', 'data', 'io', 'MonitorOffset', @@ -30,4 +29,5 @@ 'plot_flat_detector_xy', 'sans2d', 'default_parameters', + 'zoom', ] diff --git a/src/ess/isissans/data.py b/src/ess/isissans/data.py index 5dc44ce3..ec89faa6 100644 --- a/src/ess/isissans/data.py +++ b/src/ess/isissans/data.py @@ -1,115 +1,133 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2024 Scipp contributors (https://github.com/scipp) - -from dataclasses import dataclass -from typing import Dict - import sciline import scipp as sc -from ..sans.data import Registry -from ..sans.types import ( +from ess.sans.data import Registry +from ess.sans.types import ( BackgroundRun, DirectBeam, DirectBeamFilename, + EmptyBeamRun, Filename, - FilenameType, - FilePath, + PixelMaskFilename, RunType, SampleRun, TransmissionRun, ) - -@dataclass -class IsisRegistry: - registry: Registry - mapping: Dict[str, str] - - -_sans2d_registry = IsisRegistry( - registry=Registry( - instrument='sans2d', - files={ - # Direct beam file (efficiency of detectors as a function of wavelength) - 'DIRECT_SANS2D_REAR_34327_4m_8mm_16Feb16.dat.h5': 'md5:43f4188301d709aa49df0631d03a67cb', # noqa: E501 - # Empty beam run (no sample and no sample holder/can) - 'SANS2D00063091.nxs.h5': 'md5:ec7f78d51a4abc643bbe1965b7a876b9', - # Sample run (sample and sample holder/can) - 'SANS2D00063114.nxs.h5': 'md5:d0701afe88c09e6a714b31ecfbd79c0c', - # Background run (no sample, sample holder/can only) - 'SANS2D00063159.nxs.h5': 'md5:8d740b29d8965c8d9ca4f20f1e68ec15', - # Solid angles of the SANS2D detector pixels computed by Mantid (for tests) - 'SANS2D00063091.SolidAngle_from_mantid.h5': 'md5:d57b82db377cb1aea0beac7202713861', # noqa: E501 - }, - version='1', - ), - mapping={ - 'DIRECT_SANS2D_REAR_34327_4m_8mm_16Feb16.dat': 'DIRECT_SANS2D_REAR_34327_4m_8mm_16Feb16.dat.h5', # noqa: E501 - 'SANS2D00063091.nxs': 'SANS2D00063091.nxs.h5', - 'SANS2D00063114.nxs': 'SANS2D00063114.nxs.h5', - 'SANS2D00063159.nxs': 'SANS2D00063159.nxs.h5', +from .io import CalibrationFilename + +_sans2d_registry = Registry( + instrument='sans2d', + files={ + # Direct beam file (efficiency of detectors as a function of wavelength) + 'DIRECT_SANS2D_REAR_34327_4m_8mm_16Feb16.dat.h5': 'md5:43f4188301d709aa49df0631d03a67cb', # noqa: E501 + # Empty beam run (no sample and no sample holder/can) + 'SANS2D00063091.nxs.h5': 'md5:ec7f78d51a4abc643bbe1965b7a876b9', + # Sample run (sample and sample holder/can) + 'SANS2D00063114.nxs.h5': 'md5:d0701afe88c09e6a714b31ecfbd79c0c', + # Background run (no sample, sample holder/can only) + 'SANS2D00063159.nxs.h5': 'md5:8d740b29d8965c8d9ca4f20f1e68ec15', + # Solid angles of the SANS2D detector pixels computed by Mantid (for tests) + 'SANS2D00063091.SolidAngle_from_mantid.h5': 'md5:d57b82db377cb1aea0beac7202713861', # noqa: E501 }, + version='1', ) -_zoom_registry = IsisRegistry( - registry=Registry( - instrument='zoom', - files={ - # Sample run (sample and sample holder/can) with applied 192tubeCalibration_11-02-2019_r5_10lines.nxs # noqa: E501 - 'ZOOM00034786.nxs.h5.zip': 'md5:e1c53bf826dd87545df1b3629f424762', - # Empty beam run (no sample and no sample holder/can) - Scipp-hdf5 format - 'ZOOM00034787.nxs.h5': 'md5:27e563d4e57621518658307acbbc3413', - # Calibration file, Mantid processed NeXus - '192tubeCalibration_11-02-2019_r5_10lines.nxs': 'md5:ca1e0e3c387903be445d0dfd0a784ed6', # noqa: E501 - # Direct beam file (efficiency of detectors as a function of wavelength) - 'Direct_Zoom_4m_8mm_100522.txt.h5': 'md5:bbe813580676a9ad170934ffb7c99617', - # Moderator file (used for computing Q-resolution) - 'ModeratorStdDev_TS2_SANS_LETexptl_07Aug2015.txt': 'md5:5fc389340d453b9095a5dfcc33608dae', # noqa: E501 - # ISIS user file configuring the data reduction - 'USER_ZOOM_Cabral_4m_TJump_233G_8x8mm_Small_BEAMSTOP_v1_M5.toml': 'md5:4423ecb7d924c79711aba5b0a30a23e7', # noqa: E501 - # 7 pixel mask files for the ZOOM00034786.nxs run - 'andru_test.xml': 'md5:c59e0c4a80640a387df7beca4857e66f', - 'left_beg_18_2.xml': 'md5:5b24a8954d4d8a291f59f5392cd61681', - 'right_beg_18_2.xml': 'md5:fae95a5056e5f5ba4996c8dff83ec109', - 'small_bs_232.xml': 'md5:6d67dea9208193c9f0753ffcbb50ed83', - 'small_BS_31032023.xml': 'md5:3c644e8c75105809ab521773f9c0c85b', - 'tube_1120_bottom.xml': 'md5:fe577bf73c16bf5ac909516fa67360e9', - 'tubes_beg_18_2.xml': 'md5:2debde8d82c383cc3d592ea000552300', - }, - version='2', - ), - mapping={ - 'Direct_Zoom_4m_8mm_100522.txt': 'Direct_Zoom_4m_8mm_100522.txt.h5', - 'ZOOM00034786.nxs': 'ZOOM00034786.nxs.h5.zip', - 'ZOOM00034787.nxs': 'ZOOM00034787.nxs.h5', +def sans2d_solid_angle_reference() -> str: + """Solid angles of the SANS2D detector pixels computed by Mantid (for tests)""" + return _sans2d_registry.get_path('SANS2D00063091.SolidAngle_from_mantid.h5') + + +_zoom_registry = Registry( + instrument='zoom', + files={ + # Sample run (sample and sample holder/can) with applied 192tubeCalibration_11-02-2019_r5_10lines.nxs # noqa: E501 + 'ZOOM00034786.nxs.h5.zip': 'md5:e1c53bf826dd87545df1b3629f424762', + # Empty beam run (no sample and no sample holder/can) - Scipp-hdf5 format + 'ZOOM00034787.nxs.h5': 'md5:27e563d4e57621518658307acbbc3413', + # Calibration file, Mantid processed NeXus + '192tubeCalibration_11-02-2019_r5_10lines.nxs': 'md5:ca1e0e3c387903be445d0dfd0a784ed6', # noqa: E501 + # Direct beam file (efficiency of detectors as a function of wavelength) + 'Direct_Zoom_4m_8mm_100522.txt.h5': 'md5:bbe813580676a9ad170934ffb7c99617', + # Moderator file (used for computing Q-resolution) + 'ModeratorStdDev_TS2_SANS_LETexptl_07Aug2015.txt': 'md5:5fc389340d453b9095a5dfcc33608dae', # noqa: E501 + # ISIS user file configuring the data reduction + 'USER_ZOOM_Cabral_4m_TJump_233G_8x8mm_Small_BEAMSTOP_v1_M5.toml': 'md5:4423ecb7d924c79711aba5b0a30a23e7', # noqa: E501 + # 7 pixel mask files for the ZOOM00034786.nxs run + 'andru_test.xml': 'md5:c59e0c4a80640a387df7beca4857e66f', + 'left_beg_18_2.xml': 'md5:5b24a8954d4d8a291f59f5392cd61681', + 'right_beg_18_2.xml': 'md5:fae95a5056e5f5ba4996c8dff83ec109', + 'small_bs_232.xml': 'md5:6d67dea9208193c9f0753ffcbb50ed83', + 'small_BS_31032023.xml': 'md5:3c644e8c75105809ab521773f9c0c85b', + 'tube_1120_bottom.xml': 'md5:fe577bf73c16bf5ac909516fa67360e9', + 'tubes_beg_18_2.xml': 'md5:2debde8d82c383cc3d592ea000552300', }, + version='2', ) -_registries = (_sans2d_registry, _zoom_registry) +def sans2d_tutorial_direct_beam() -> DirectBeamFilename: + return DirectBeamFilename( + _sans2d_registry.get_path('DIRECT_SANS2D_REAR_34327_4m_8mm_16Feb16.dat.h5') + ) + + +def sans2d_tutorial_sample_run() -> Filename[SampleRun]: + return Filename[SampleRun](_sans2d_registry.get_path('SANS2D00063114.nxs.h5')) + + +def sans2d_tutorial_background_run() -> Filename[BackgroundRun]: + return Filename[BackgroundRun](_sans2d_registry.get_path('SANS2D00063159.nxs.h5')) + + +def sans2d_tutorial_empty_beam_run() -> Filename[EmptyBeamRun]: + return Filename[EmptyBeamRun](_sans2d_registry.get_path('SANS2D00063091.nxs.h5')) + +def zoom_tutorial_direct_beam() -> DirectBeamFilename: + return DirectBeamFilename( + _zoom_registry.get_path('Direct_Zoom_4m_8mm_100522.txt.h5') + ) -def get_path(filename: FilenameType) -> FilePath[FilenameType]: - """Translate any filename to a path to the file obtained from pooch registries.""" - for reg in _registries: - filename = reg.mapping.get(filename, filename) - if filename in reg.registry: - if filename.endswith('.zip'): - return reg.registry.get_path(filename, unzip=True)[0] - return reg.registry.get_path(filename) + +def zoom_tutorial_calibration() -> Filename[CalibrationFilename]: + return Filename[CalibrationFilename]( + _zoom_registry.get_path('192tubeCalibration_11-02-2019_r5_10lines.nxs') + ) + + +def zoom_tutorial_sample_run() -> Filename[SampleRun]: + return _zoom_registry.get_path('ZOOM00034786.nxs.h5.zip', unzip=True)[0] + + +def zoom_tutorial_empty_beam_run() -> Filename[EmptyBeamRun]: + return Filename[EmptyBeamRun](_zoom_registry.get_path('ZOOM00034787.nxs.h5')) + + +def zoom_tutorial_mask_filenames() -> list[PixelMaskFilename]: + return [ + PixelMaskFilename(_zoom_registry.get_path('andru_test.xml')), + PixelMaskFilename(_zoom_registry.get_path('left_beg_18_2.xml')), + PixelMaskFilename(_zoom_registry.get_path('right_beg_18_2.xml')), + PixelMaskFilename(_zoom_registry.get_path('small_bs_232.xml')), + PixelMaskFilename(_zoom_registry.get_path('small_BS_31032023.xml')), + PixelMaskFilename(_zoom_registry.get_path('tube_1120_bottom.xml')), + PixelMaskFilename(_zoom_registry.get_path('tubes_beg_18_2.xml')), + ] class LoadedFileContents(sciline.Scope[RunType, sc.DataGroup], sc.DataGroup): """Contents of a loaded file.""" -def load_run(filename: FilePath[Filename[RunType]]) -> LoadedFileContents[RunType]: +def load_tutorial_run(filename: Filename[RunType]) -> LoadedFileContents[RunType]: return LoadedFileContents[RunType](sc.io.load_hdf5(filename)) -def load_direct_beam(filename: FilePath[DirectBeamFilename]) -> DirectBeam: +def load_tutorial_direct_beam(filename: DirectBeamFilename) -> DirectBeam: return DirectBeam(sc.io.load_hdf5(filename)) @@ -129,6 +147,3 @@ def transmission_from_background_run( Use transmission from a background run, instead of dedicated run. """ return LoadedFileContents[TransmissionRun[BackgroundRun]](data) - - -providers = (get_path, load_run, load_direct_beam) diff --git a/src/ess/isissans/io.py b/src/ess/isissans/io.py index 126bb06d..4a5f739a 100644 --- a/src/ess/isissans/io.py +++ b/src/ess/isissans/io.py @@ -7,24 +7,12 @@ import scipp as sc -from ..sans.types import ( - DataFolder, - FilenameType, - FilePath, - MaskedDetectorIDs, - PixelMaskFilename, -) +from ess.sans.types import MaskedDetectorIDs, PixelMaskFilename CalibrationFilename = NewType('CalibrationFilename', str) -def to_path(filename: FilenameType, path: DataFolder) -> FilePath[FilenameType]: - return f'{path}/{filename}' - - -def read_xml_detector_masking( - filename: FilePath[PixelMaskFilename], -) -> MaskedDetectorIDs: +def read_xml_detector_masking(filename: PixelMaskFilename) -> MaskedDetectorIDs: """Read a pixel mask from an XML file. The format is as follows, where the detids are inclusive ranges of detector IDs: @@ -64,4 +52,4 @@ def read_xml_detector_masking( ) -providers = (read_xml_detector_masking, to_path) +providers = (read_xml_detector_masking,) diff --git a/src/ess/isissans/mantidio.py b/src/ess/isissans/mantidio.py index 81e398ff..3e0634d6 100644 --- a/src/ess/isissans/mantidio.py +++ b/src/ess/isissans/mantidio.py @@ -10,9 +10,10 @@ import scippneutron as scn from scipp.constants import g -from ..sans.types import DirectBeam, DirectBeamFilename, Filename, RunType, SampleRun +from ess.sans.types import DirectBeam, DirectBeamFilename, Filename, RunType, SampleRun + from .data import LoadedFileContents -from .io import CalibrationFilename, FilePath +from .io import CalibrationFilename try: import mantid.api as _mantid_api @@ -59,12 +60,12 @@ def _get_detector_ids(ws: DataWorkspace[SampleRun]) -> sc.Variable: return da.data.rename_dims(detector='spectrum') -def load_calibration(filename: FilePath[CalibrationFilename]) -> CalibrationWorkspace: +def load_calibration(filename: CalibrationFilename) -> CalibrationWorkspace: ws = _mantid_simpleapi.Load(Filename=str(filename), StoreInADS=False) return CalibrationWorkspace(ws) -def load_direct_beam(filename: FilePath[DirectBeamFilename]) -> DirectBeam: +def load_direct_beam(filename: DirectBeamFilename) -> DirectBeam: dg = scn.load_with_mantid( filename=filename, mantid_alg="LoadRKH", @@ -76,8 +77,7 @@ def load_direct_beam(filename: FilePath[DirectBeamFilename]) -> DirectBeam: def from_data_workspace( - ws: DataWorkspace[RunType], - calibration: Optional[CalibrationWorkspace], + ws: DataWorkspace[RunType], calibration: Optional[CalibrationWorkspace] ) -> LoadedFileContents[RunType]: if calibration is not None: _mantid_simpleapi.CopyInstrumentParameters( @@ -99,7 +99,7 @@ def from_data_workspace( def load_run( - filename: FilePath[Filename[RunType]], period: Optional[Period] + filename: Filename[RunType], period: Optional[Period] ) -> DataWorkspace[RunType]: loaded = _mantid_simpleapi.Load( Filename=str(filename), LoadMonitors=True, StoreInADS=False diff --git a/src/ess/isissans/sans2d.py b/src/ess/isissans/sans2d.py index a7098b1b..a05692de 100644 --- a/src/ess/isissans/sans2d.py +++ b/src/ess/isissans/sans2d.py @@ -2,9 +2,15 @@ # Copyright (c) 2024 Scipp contributors (https://github.com/scipp) from typing import NewType, Optional +import sciline import scipp as sc -from ..sans.types import MaskedData, SampleRun, ScatteringRunType, TofData +from ess.sans import providers as sans_providers +from ess.sans.types import MaskedData, SampleRun, ScatteringRunType, TofData + +from .data import load_tutorial_direct_beam, load_tutorial_run +from .general import default_parameters +from .mantidio import providers as mantid_providers DetectorEdgeMask = NewType('DetectorEdgeMask', sc.Variable) """Detector edge mask""" @@ -66,3 +72,25 @@ def mask_detectors( providers = (detector_edge_mask, sample_holder_mask, mask_detectors) + + +def Sans2dWorkflow() -> sciline.Pipeline: + """Create Sans2d workflow with default parameters.""" + from . import providers as isis_providers + + params = default_parameters() + sans2d_providers = sans_providers + isis_providers + mantid_providers + providers + return sciline.Pipeline(providers=sans2d_providers, params=params) + + +def Sans2dTutorialWorkflow() -> sciline.Pipeline: + """ + Create Sans2d tutorial workflow. + + Equivalent to :func:`Sans2dWorkflow`, but with loaders for tutorial data instead + of Mantid-based loaders. + """ + workflow = Sans2dWorkflow() + workflow.insert(load_tutorial_run) + workflow.insert(load_tutorial_direct_beam) + return workflow diff --git a/src/ess/isissans/zoom.py b/src/ess/isissans/zoom.py new file mode 100644 index 00000000..2bb91e28 --- /dev/null +++ b/src/ess/isissans/zoom.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2024 Scipp contributors (https://github.com/scipp) +import sciline + +from ess.sans import providers as sans_providers + +from .data import load_tutorial_direct_beam, load_tutorial_run +from .general import default_parameters +from .io import read_xml_detector_masking +from .mantidio import providers as mantid_providers + + +def set_mantid_log_level(level: int = 3): + try: + from mantid import ConfigService + + cfg = ConfigService.Instance() + cfg.setLogLevel(level) # Silence verbose load via Mantid + except ImportError: + pass + + +def ZoomWorkflow() -> sciline.Pipeline: + """Create Zoom workflow with default parameters.""" + from . import providers as isis_providers + + set_mantid_log_level() + + params = default_parameters() + zoom_providers = sans_providers + isis_providers + mantid_providers + workflow = sciline.Pipeline(providers=zoom_providers, params=params) + workflow.insert(read_xml_detector_masking) + return workflow + + +def ZoomTutorialWorkflow() -> sciline.Pipeline: + """ + Create Zoom tutorial workflow. + + Equivalent to :func:`ZoomWorkflow`, but with loaders for tutorial data instead + of Mantid-based loaders. + """ + workflow = ZoomWorkflow() + workflow.insert(load_tutorial_run) + workflow.insert(load_tutorial_direct_beam) + return workflow diff --git a/src/ess/loki/__init__.py b/src/ess/loki/__init__.py index 97b6b949..694717cf 100644 --- a/src/ess/loki/__init__.py +++ b/src/ess/loki/__init__.py @@ -4,7 +4,7 @@ import importlib.metadata from . import data, general, io -from .general import default_parameters +from .general import LokiAtLarmorWorkflow, default_parameters try: __version__ = importlib.metadata.version(__package__ or __name__) @@ -15,4 +15,11 @@ del importlib -__all__ = ['data', 'general', 'io', 'providers', 'default_parameters'] +__all__ = [ + 'data', + 'general', + 'io', + 'providers', + 'default_parameters', + 'LokiAtLarmorWorkflow', +] diff --git a/src/ess/loki/data.py b/src/ess/loki/data.py index 732a383d..e95453d9 100644 --- a/src/ess/loki/data.py +++ b/src/ess/loki/data.py @@ -1,9 +1,15 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) +from pathlib import Path - -from ..sans.data import Registry -from ..sans.types import FilenameType, FilePath +from ess.sans.data import Registry +from ess.sans.types import ( + BackgroundRun, + Filename, + PixelMaskFilename, + SampleRun, + TransmissionRun, +) _registry = Registry( instrument='loki', @@ -31,9 +37,48 @@ ) -def get_path(filename: FilenameType) -> FilePath[FilenameType]: - """Translate any filename to a path to the file obtained from pooch registries.""" - return FilePath[FilenameType](_registry.get_path(filename)) +def loki_tutorial_sample_run_60250() -> Filename[SampleRun]: + """Sample run with sample and sample holder/can, no transmission monitor in beam.""" + return Filename[SampleRun](_registry.get_path('60250-2022-02-28_2215.nxs')) + + +def loki_tutorial_sample_run_60339() -> Filename[SampleRun]: + """Sample run with sample and sample holder/can, no transmission monitor in beam.""" + return Filename[SampleRun](_registry.get_path('60339-2022-02-28_2215.nxs')) + + +def loki_tutorial_background_run_60248() -> Filename[BackgroundRun]: + """Background run with sample holder/can only, no transmission monitor.""" + return Filename[BackgroundRun](_registry.get_path('60248-2022-02-28_2215.nxs')) + + +def loki_tutorial_background_run_60393() -> Filename[BackgroundRun]: + """Background run with sample holder/can only, no transmission monitor.""" + return Filename[BackgroundRun](_registry.get_path('60393-2022-02-28_2215.nxs')) + + +def loki_tutorial_sample_transmission_run() -> Filename[TransmissionRun[SampleRun]]: + """Sample transmission run (sample + sample holder/can + transmission monitor).""" + return Filename[TransmissionRun[SampleRun]]( + _registry.get_path('60394-2022-02-28_2215.nxs') + ) + + +def loki_tutorial_run_60392() -> Filename[TransmissionRun[BackgroundRun]]: + """Background transmission run (sample holder/can + transmission monitor), also + used as empty beam run.""" + return Filename[TransmissionRun[BackgroundRun]]( + _registry.get_path('60392-2022-02-28_2215.nxs') + ) + + +def loki_tutorial_mask_filenames() -> list[PixelMaskFilename]: + """List of pixel mask filenames for the LoKI@Larmor detector test experiment.""" + return [ + PixelMaskFilename(_registry.get_path('mask_new_July2022.xml')), + ] -providers = (get_path,) +def loki_tutorial_poly_gauss_I0() -> Path: + """Analytical model for the I(Q) of the Poly-Gauss sample.""" + return Path(_registry.get_path('PolyGauss_I0-50_Rg-60.h5')) diff --git a/src/ess/loki/general.py b/src/ess/loki/general.py index 19ff752f..511e17a5 100644 --- a/src/ess/loki/general.py +++ b/src/ess/loki/general.py @@ -5,9 +5,12 @@ """ from typing import Optional +import sciline import scipp as sc from ess.reduce import nexus +from ess.sans import providers as sans_providers + from ..sans.common import gravity_vector from ..sans.types import ( ConfiguredReducibleDataData, @@ -36,6 +39,7 @@ TransformationPath, Transmission, ) +from .io import dummy_load_sample def default_parameters() -> dict: @@ -49,6 +53,28 @@ def default_parameters() -> dict: } +def LokiAtLarmorWorkflow() -> sciline.Pipeline: + """ + Workflow with default parameters for Loki test at Larmor. + + This version of the Loki workflow: + + - Uses ISIS XML files to define masks. + - Sets a dummy sample position [0,0,0] since files do not contain this information. + """ + from ess.isissans.io import read_xml_detector_masking + + from . import providers as loki_providers + + params = default_parameters() + loki_providers = sans_providers + loki_providers + workflow = sciline.Pipeline(providers=loki_providers, params=params) + workflow.insert(read_xml_detector_masking) + # No sample information in the Loki@Larmor files, so we use a dummy sample provider + workflow.insert(dummy_load_sample) + return workflow + + DETECTOR_BANK_RESHAPING = { 'larmor_detector': lambda x: x.fold( dim='detector_number', sizes=dict(layer=4, tube=32, straw=7, pixel=512) diff --git a/src/ess/loki/io.py b/src/ess/loki/io.py index 4713552e..57c3bcbd 100644 --- a/src/ess/loki/io.py +++ b/src/ess/loki/io.py @@ -6,11 +6,8 @@ import scipp as sc from ess.reduce import nexus -from ..sans.types import ( - DataFolder, +from ess.sans.types import ( Filename, - FilenameType, - FilePath, LoadedNeXusDetector, LoadedNeXusMonitor, MonitorType, @@ -22,22 +19,22 @@ ) -def load_nexus_sample(file_path: FilePath[Filename[RunType]]) -> RawSample[RunType]: +def load_nexus_sample(file_path: Filename[RunType]) -> RawSample[RunType]: return RawSample[RunType](nexus.load_sample(file_path)) -def dummy_load_sample(file_path: FilePath[Filename[RunType]]) -> RawSample[RunType]: +def dummy_load_sample(file_path: Filename[RunType]) -> RawSample[RunType]: return RawSample[RunType]( sc.DataGroup({'position': sc.vector(value=[0, 0, 0], unit='m')}) ) -def load_nexus_source(file_path: FilePath[Filename[RunType]]) -> RawSource[RunType]: +def load_nexus_source(file_path: Filename[RunType]) -> RawSource[RunType]: return RawSource[RunType](nexus.load_source(file_path)) def load_nexus_detector( - file_path: FilePath[Filename[RunType]], detector_name: NeXusDetectorName + file_path: Filename[RunType], detector_name: NeXusDetectorName ) -> LoadedNeXusDetector[RunType]: return LoadedNeXusDetector[RunType]( nexus.load_detector(file_path=file_path, detector_name=detector_name) @@ -45,7 +42,7 @@ def load_nexus_detector( def load_nexus_monitor( - file_path: FilePath[Filename[RunType]], + file_path: Filename[RunType], monitor_name: NeXusMonitorName[MonitorType], ) -> LoadedNeXusMonitor[RunType, MonitorType]: return LoadedNeXusMonitor[RunType, MonitorType]( @@ -53,15 +50,10 @@ def load_nexus_monitor( ) -def to_path(filename: FilenameType, path: DataFolder) -> FilePath[FilenameType]: - return FilePath[FilenameType](f'{path}/{filename}') - - providers = ( load_nexus_detector, load_nexus_monitor, load_nexus_sample, load_nexus_source, - to_path, ) """Providers for loading single files.""" diff --git a/src/ess/sans/direct_beam.py b/src/ess/sans/direct_beam.py index aed87ece..443f113e 100644 --- a/src/ess/sans/direct_beam.py +++ b/src/ess/sans/direct_beam.py @@ -54,7 +54,7 @@ def _compute_efficiency_correction( return out.rename_dims({wavelength_band_dim: 'wavelength'}) -def direct_beam(pipeline: Pipeline, I0: sc.Variable, niter: int = 5) -> List[dict]: +def direct_beam(*, workflow: Pipeline, I0: sc.Variable, niter: int = 5) -> List[dict]: """ Compute the direct beam function. @@ -96,20 +96,20 @@ def direct_beam(pipeline: Pipeline, I0: sc.Variable, niter: int = 5) -> List[dic """ direct_beam_function = None - bands = pipeline.compute(ProcessedWavelengthBands) + bands = workflow.compute(ProcessedWavelengthBands) band_dim = (set(bands.dims) - {'wavelength'}).pop() full_wavelength_range = sc.concat([bands.min(), bands.max()], dim='wavelength') - pipeline = pipeline.copy() + workflow = workflow.copy() - wavelength_bins = pipeline.compute(WavelengthBins) + wavelength_bins = workflow.compute(WavelengthBins) parts = ( FinalSummedQ[SampleRun, Numerator], FinalSummedQ[SampleRun, Denominator], FinalSummedQ[BackgroundRun, Numerator], FinalSummedQ[BackgroundRun, Denominator], ) - parts = pipeline.compute(parts) + parts = workflow.compute(parts) # Convert events to histograms to make normalization (in every iteration) cheap for key in [ FinalSummedQ[SampleRun, Numerator], @@ -121,7 +121,7 @@ def direct_beam(pipeline: Pipeline, I0: sc.Variable, niter: int = 5) -> List[dic # computed without variances anyway. parts = {key: sc.values(result) for key, result in parts.items()} for key, part in parts.items(): - pipeline[key] = part + workflow[key] = part sample0 = parts[FinalSummedQ[SampleRun, Denominator]] background0 = parts[FinalSummedQ[BackgroundRun, Denominator]] @@ -132,10 +132,10 @@ def direct_beam(pipeline: Pipeline, I0: sc.Variable, niter: int = 5) -> List[dic # parameters, nor given by any providers, so it will be considered flat. # TODO: Should we have a check that DirectBeam cannot be computed from the # pipeline? - pipeline[WavelengthBands] = full_wavelength_range - iofq_full = pipeline.compute(BackgroundSubtractedIofQ) - pipeline[WavelengthBands] = bands - iofq_bands = pipeline.compute(BackgroundSubtractedIofQ) + workflow[WavelengthBands] = full_wavelength_range + iofq_full = workflow.compute(BackgroundSubtractedIofQ) + workflow[WavelengthBands] = bands + iofq_bands = workflow.compute(BackgroundSubtractedIofQ) if direct_beam_function is None: # Make a flat direct beam @@ -161,8 +161,8 @@ def direct_beam(pipeline: Pipeline, I0: sc.Variable, niter: int = 5) -> List[dic db.coords['wavelength'] = sc.midpoints( db.coords['wavelength'], dim='wavelength' ) - pipeline[FinalSummedQ[SampleRun, Denominator]] = sample0 * db - pipeline[FinalSummedQ[BackgroundRun, Denominator]] = background0 * db + workflow[FinalSummedQ[SampleRun, Denominator]] = sample0 * db + workflow[FinalSummedQ[BackgroundRun, Denominator]] = background0 * db results.append( { diff --git a/src/ess/sans/types.py b/src/ess/sans/types.py index 55b19c46..6e63edf1 100644 --- a/src/ess/sans/types.py +++ b/src/ess/sans/types.py @@ -160,13 +160,6 @@ class NeXusMonitorName(sciline.Scope[MonitorType, str], str): FilenameType = TypeVar('FilenameType', bound=str) -DataFolder = NewType('DataFolder', str) - - -class FilePath(sciline.Scope[FilenameType, str], str): - """Path to a file""" - - class Filename(sciline.Scope[RunType, str], str): """Filename of a run""" diff --git a/tests/isissans/sans2d_reduction_test.py b/tests/isissans/sans2d_reduction_test.py index 1335937f..4ec92f36 100644 --- a/tests/isissans/sans2d_reduction_test.py +++ b/tests/isissans/sans2d_reduction_test.py @@ -55,10 +55,10 @@ def make_params() -> dict: params[QBins] = sc.linspace( dim='Q', start=0.01, stop=0.55, num=141, unit='1/angstrom' ) - params[DirectBeamFilename] = 'DIRECT_SANS2D_REAR_34327_4m_8mm_16Feb16.dat' - params[Filename[SampleRun]] = 'SANS2D00063114.nxs' - params[Filename[BackgroundRun]] = 'SANS2D00063159.nxs' - params[Filename[EmptyBeamRun]] = 'SANS2D00063091.nxs' + params[DirectBeamFilename] = isis.data.sans2d_tutorial_direct_beam() + params[Filename[SampleRun]] = isis.data.sans2d_tutorial_sample_run() + params[Filename[BackgroundRun]] = isis.data.sans2d_tutorial_background_run() + params[Filename[EmptyBeamRun]] = isis.data.sans2d_tutorial_empty_beam_run() params[NeXusMonitorName[Incident]] = 'monitor2' params[NeXusMonitorName[Transmission]] = 'monitor4' @@ -79,11 +79,13 @@ def sans2d_providers(): return list( sans.providers + isis.providers - + isis.data.providers + isis.sans2d.providers + + isis.mantidio.providers + ( isis.data.transmission_from_background_run, isis.data.transmission_from_sample_run, + isis.data.load_tutorial_direct_beam, + isis.data.load_tutorial_run, sans.beam_center_finder.beam_center_from_center_of_mass, ) ) @@ -186,7 +188,7 @@ def as_dict(funcs: List[Callable[..., type]]) -> dict: def pixel_dependent_direct_beam( filename: DirectBeamFilename, shape: RawData[SampleRun] ) -> DirectBeam: - direct_beam = isis.data.load_direct_beam(isis.data.get_path(filename)) + direct_beam = isis.data.load_tutorial_direct_beam(filename) sizes = {'spectrum': shape.sizes['spectrum'], **direct_beam.sizes} return DirectBeam(direct_beam.broadcast(sizes=sizes).copy()) diff --git a/tests/isissans/zoom_reduction_test.py b/tests/isissans/zoom_reduction_test.py index 592c5ffd..da66a184 100644 --- a/tests/isissans/zoom_reduction_test.py +++ b/tests/isissans/zoom_reduction_test.py @@ -27,10 +27,10 @@ def make_params() -> dict: params = { **isis.default_parameters(), - sans.types.DirectBeamFilename: 'Direct_Zoom_4m_8mm_100522.txt', - isis.CalibrationFilename: '192tubeCalibration_11-02-2019_r5_10lines.nxs', - Filename[sans.types.SampleRun]: 'ZOOM00034786.nxs', - Filename[sans.types.EmptyBeamRun]: 'ZOOM00034787.nxs', + sans.types.DirectBeamFilename: isis.data.zoom_tutorial_direct_beam(), + isis.CalibrationFilename: isis.data.zoom_tutorial_calibration(), + Filename[sans.types.SampleRun]: isis.data.zoom_tutorial_sample_run(), + Filename[sans.types.EmptyBeamRun]: isis.data.zoom_tutorial_empty_beam_run(), isis.SampleOffset: sc.vector([0.0, 0.0, 0.11], unit='m'), isis.DetectorBankOffset: sc.vector([0.0, 0.0, 0.5], unit='m'), } @@ -56,15 +56,7 @@ def make_params() -> dict: def make_masks_table() -> sciline.ParamTable: - masks = [ - 'andru_test.xml', - 'left_beg_18_2.xml', - 'right_beg_18_2.xml', - 'small_bs_232.xml', - 'small_BS_31032023.xml', - 'tube_1120_bottom.xml', - 'tubes_beg_18_2.xml', - ] + masks = isis.data.zoom_tutorial_mask_filenames() return sciline.ParamTable(PixelMaskFilename, columns={}, index=masks) @@ -72,10 +64,12 @@ def zoom_providers(): return list( sans.providers + isis.providers - + isis.data.providers + + isis.mantidio.providers + ( isis.data.transmission_from_background_run, isis.data.transmission_from_sample_run, + isis.data.load_tutorial_direct_beam, + isis.data.load_tutorial_run, sans.beam_center_finder.beam_center_from_center_of_mass, ) ) diff --git a/tests/loki/common.py b/tests/loki/common.py index 92321147..1a17bd42 100644 --- a/tests/loki/common.py +++ b/tests/loki/common.py @@ -26,11 +26,15 @@ def make_params() -> dict: params = loki.default_parameters() params[NeXusDetectorName] = 'larmor_detector' - params[Filename[SampleRun]] = '60339-2022-02-28_2215.nxs' - params[Filename[BackgroundRun]] = '60393-2022-02-28_2215.nxs' - params[Filename[TransmissionRun[SampleRun]]] = '60394-2022-02-28_2215.nxs' - params[Filename[TransmissionRun[BackgroundRun]]] = '60392-2022-02-28_2215.nxs' - params[Filename[EmptyBeamRun]] = '60392-2022-02-28_2215.nxs' + params[Filename[SampleRun]] = loki.data.loki_tutorial_sample_run_60339() + params[Filename[BackgroundRun]] = loki.data.loki_tutorial_background_run_60393() + params[ + Filename[TransmissionRun[SampleRun]] + ] = loki.data.loki_tutorial_sample_transmission_run() + params[ + Filename[TransmissionRun[BackgroundRun]] + ] = loki.data.loki_tutorial_run_60392() + params[Filename[EmptyBeamRun]] = loki.data.loki_tutorial_run_60392() params[WavelengthBins] = sc.linspace( 'wavelength', start=1.0, stop=13.0, num=51, unit='angstrom' @@ -52,7 +56,6 @@ def loki_providers_no_beam_center_finder() -> List[Callable]: return list( sans.providers + loki.providers - + loki.data.providers + ( read_xml_detector_masking, loki.io.dummy_load_sample, diff --git a/tests/loki/directbeam_test.py b/tests/loki/directbeam_test.py index 866c7a41..d4864b27 100644 --- a/tests/loki/directbeam_test.py +++ b/tests/loki/directbeam_test.py @@ -7,8 +7,7 @@ import scipp as sc from scipp.scipy.interpolate import interp1d -from ess import sans -from ess.loki.data import get_path +from ess import loki, sans from ess.sans.types import ( DimsToKeep, PixelMaskFilename, @@ -22,7 +21,7 @@ def _get_I0(qbins: sc.Variable) -> sc.Variable: - Iq_theory = sc.io.load_hdf5(get_path('PolyGauss_I0-50_Rg-60.h5')) + Iq_theory = sc.io.load_hdf5(loki.data.loki_tutorial_poly_gauss_I0()) f = interp1d(Iq_theory, 'Q') return f(sc.midpoints(qbins)).data[0] @@ -41,7 +40,7 @@ def test_can_compute_direct_beam_for_all_pixels(): pipeline.set_param_series(PixelMaskFilename, []) I0 = _get_I0(qbins=params[QBins]) - results = sans.direct_beam(pipeline=pipeline, I0=I0, niter=4) + results = sans.direct_beam(workflow=pipeline, I0=I0, niter=4) iofq_full = results[-1]['iofq_full'] iofq_bands = results[-1]['iofq_bands'] direct_beam_function = results[-1]['direct_beam'] @@ -71,7 +70,7 @@ def test_can_compute_direct_beam_with_overlapping_wavelength_bands(): pipeline.set_param_series(PixelMaskFilename, []) I0 = _get_I0(qbins=params[QBins]) - results = sans.direct_beam(pipeline=pipeline, I0=I0, niter=4) + results = sans.direct_beam(workflow=pipeline, I0=I0, niter=4) iofq_full = results[-1]['iofq_full'] iofq_bands = results[-1]['iofq_bands'] direct_beam_function = results[-1]['direct_beam'] @@ -97,7 +96,7 @@ def test_can_compute_direct_beam_per_layer(): pipeline.set_param_series(PixelMaskFilename, []) I0 = _get_I0(qbins=params[QBins]) - results = sans.direct_beam(pipeline=pipeline, I0=I0, niter=4) + results = sans.direct_beam(workflow=pipeline, I0=I0, niter=4) iofq_full = results[-1]['iofq_full'] iofq_bands = results[-1]['iofq_bands'] direct_beam_function = results[-1]['direct_beam'] @@ -125,7 +124,7 @@ def test_can_compute_direct_beam_per_layer_and_straw(): pipeline.set_param_series(PixelMaskFilename, []) I0 = _get_I0(qbins=params[QBins]) - results = sans.direct_beam(pipeline=pipeline, I0=I0, niter=4) + results = sans.direct_beam(workflow=pipeline, I0=I0, niter=4) iofq_full = results[-1]['iofq_full'] iofq_bands = results[-1]['iofq_bands'] direct_beam_function = results[-1]['direct_beam'] diff --git a/tests/loki/iofq_test.py b/tests/loki/iofq_test.py index e5a40c08..ab4b7947 100644 --- a/tests/loki/iofq_test.py +++ b/tests/loki/iofq_test.py @@ -2,12 +2,13 @@ # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) import sys from pathlib import Path +from typing import NewType import pytest import sciline import scipp as sc -from ess import sans +from ess import loki, sans from ess.sans.conversions import ElasticCoordTransformGraph from ess.sans.types import ( BackgroundRun, @@ -20,8 +21,6 @@ Denominator, DimsToKeep, Filename, - FilenameType, - FilePath, FinalSummedQ, IofQ, IofQxy, @@ -48,7 +47,9 @@ def test_can_create_pipeline(): pipeline = sciline.Pipeline(loki_providers(), params=make_params()) - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) pipeline.get(BackgroundSubtractedIofQ) @@ -61,7 +62,9 @@ def test_pipeline_can_compute_IofQ(uncertainties, qxy: bool): params = make_params() params[UncertaintyBroadcastMode] = uncertainties pipeline = sciline.Pipeline(loki_providers(), params=params) - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) if qxy: result = pipeline.compute(BackgroundSubtractedIofQxy) assert result.dims == ('Qy', 'Qx') @@ -93,7 +96,9 @@ def test_pipeline_can_compute_IofQ_in_event_mode(uncertainties, target): params = make_params() params[UncertaintyBroadcastMode] = uncertainties pipeline = sciline.Pipeline(loki_providers(), params=params) - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) reference = pipeline.compute(target) pipeline[ReturnEvents] = True result = pipeline.compute(target) @@ -132,7 +137,9 @@ def test_pipeline_can_compute_IofQ_in_wavelength_bands(qxy: bool): 11, ) pipeline = sciline.Pipeline(loki_providers(), params=params) - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) result = pipeline.compute( BackgroundSubtractedIofQxy if qxy else BackgroundSubtractedIofQ ) @@ -151,7 +158,9 @@ def test_pipeline_can_compute_IofQ_in_overlapping_wavelength_bands(qxy: bool): [edges[:-2], edges[2::]], dim='wavelength' ).transpose() pipeline = sciline.Pipeline(loki_providers(), params=params) - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) result = pipeline.compute( BackgroundSubtractedIofQxy if qxy else BackgroundSubtractedIofQ ) @@ -164,7 +173,9 @@ def test_pipeline_can_compute_IofQ_in_layers(qxy: bool): params = make_params() params[DimsToKeep] = ['layer'] pipeline = sciline.Pipeline(loki_providers(), params=params) - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) result = pipeline.compute( BackgroundSubtractedIofQxy if qxy else BackgroundSubtractedIofQ ) @@ -174,7 +185,9 @@ def test_pipeline_can_compute_IofQ_in_layers(qxy: bool): def _compute_beam_center(): pipeline = sciline.Pipeline(loki_providers(), params=make_params()) - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) center = pipeline.compute(BeamCenter) return center @@ -184,11 +197,19 @@ def test_pipeline_can_compute_IofQ_merging_events_from_multiple_runs(): del params[Filename[SampleRun]] del params[Filename[BackgroundRun]] - sample_runs = ['60250-2022-02-28_2215.nxs', '60339-2022-02-28_2215.nxs'] - background_runs = ['60248-2022-02-28_2215.nxs', '60393-2022-02-28_2215.nxs'] + sample_runs = [ + loki.data.loki_tutorial_sample_run_60250(), + loki.data.loki_tutorial_sample_run_60339(), + ] + background_runs = [ + loki.data.loki_tutorial_background_run_60248(), + loki.data.loki_tutorial_background_run_60393(), + ] pipeline = sciline.Pipeline(loki_providers_no_beam_center_finder(), params=params) pipeline[BeamCenter] = _compute_beam_center() - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) # Set parameter series for file names pipeline.set_param_series(Filename[SampleRun], sample_runs) @@ -205,7 +226,9 @@ def test_pipeline_can_compute_IofQ_merging_events_from_banks(): pipeline = sciline.Pipeline(loki_providers_no_beam_center_finder(), params=params) pipeline[BeamCenter] = _compute_beam_center() - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) pipeline.set_param_series(NeXusDetectorName, ['larmor_detector']) pipeline.insert(sans.merge_banks) @@ -218,11 +241,19 @@ def test_pipeline_can_compute_IofQ_merging_events_from_multiple_runs_and_banks() del params[Filename[SampleRun]] del params[Filename[BackgroundRun]] - sample_runs = ['60250-2022-02-28_2215.nxs', '60339-2022-02-28_2215.nxs'] - background_runs = ['60248-2022-02-28_2215.nxs', '60393-2022-02-28_2215.nxs'] + sample_runs = [ + loki.data.loki_tutorial_sample_run_60250(), + loki.data.loki_tutorial_sample_run_60339(), + ] + background_runs = [ + loki.data.loki_tutorial_background_run_60248(), + loki.data.loki_tutorial_background_run_60393(), + ] pipeline = sciline.Pipeline(loki_providers_no_beam_center_finder(), params=params) pipeline[BeamCenter] = _compute_beam_center() - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) pipeline.insert(sans.merge_runs) pipeline.set_param_series(Filename[SampleRun], sample_runs) @@ -245,7 +276,9 @@ def test_pipeline_IofQ_merging_events_yields_consistent_results(): loki_providers_no_beam_center_finder(), params=params ) pipeline_single[BeamCenter] = center - pipeline_single.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline_single.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) del params[Filename[SampleRun]] del params[Filename[BackgroundRun]] @@ -253,35 +286,59 @@ def test_pipeline_IofQ_merging_events_yields_consistent_results(): loki_providers_no_beam_center_finder(), params=params ) pipeline_triple[BeamCenter] = center - pipeline_triple.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline_triple.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) + + DummySampleFilename = NewType('DummySampleFilename', str) + DummyBackgroundFilename = NewType('DummyBackgroundFilename', str) # `set_param_series` does not allow multiple identical values, so we need to # map the file names to different ones. - def get_mapped_path(filename: FilenameType) -> FilePath[FilenameType]: - """Mapping file paths to allow loading same run multiple times.""" - from ess.loki.data import get_path - - mapping = { - 'sample_0.nxs': '60339-2022-02-28_2215.nxs', - 'sample_1.nxs': '60339-2022-02-28_2215.nxs', - 'sample_2.nxs': '60339-2022-02-28_2215.nxs', - 'background_0.nxs': '60393-2022-02-28_2215.nxs', - 'background_1.nxs': '60393-2022-02-28_2215.nxs', - 'background_2.nxs': '60393-2022-02-28_2215.nxs', - } - filename = mapping.get(filename, filename) - return FilePath[FilenameType](get_path(filename)) - - pipeline_triple.insert(get_mapped_path) + + def get_sample_filename(_: DummySampleFilename) -> Filename[SampleRun]: + return loki.data.loki_tutorial_sample_run_60339() + + def get_background_filename(_: DummyBackgroundFilename) -> Filename[BackgroundRun]: + return loki.data.loki_tutorial_background_run_60393() + + pipeline_triple.insert(get_sample_filename) + pipeline_triple.insert(get_background_filename) pipeline_triple.set_param_series( - Filename[SampleRun], [f'sample_{i}.nxs' for i in range(N)] + DummySampleFilename, [f'sample_{i}.nxs' for i in range(N)] ) pipeline_triple.set_param_series( - Filename[BackgroundRun], [f'background_{i}.nxs' for i in range(N)] + DummyBackgroundFilename, [f'background_{i}.nxs' for i in range(N)] ) + + # We want to use `merge` runs (defined in ess.sans.i_of_q), but its ParamSeries + # depends on Filename, which we cannot use due to the mapping hack above. We need + # to define our own wrappers. This will go away once the Sciline ParamTable support + # is replaced. + def merge_sample_runs( + runs: sciline.Series[ + DummySampleFilename, + sans.types.CleanSummedQMergedBanks[SampleRun, sans.types.IofQPart], + ], + ) -> FinalSummedQ[SampleRun, sans.types.IofQPart]: + return FinalSummedQ[SampleRun, sans.types.IofQPart]( + sans.i_of_q._merge_contributions(list(runs.values())) + ) + + def merge_background_runs( + runs: sciline.Series[ + DummyBackgroundFilename, + sans.types.CleanSummedQMergedBanks[BackgroundRun, sans.types.IofQPart], + ], + ) -> FinalSummedQ[BackgroundRun, sans.types.IofQPart]: + return FinalSummedQ[BackgroundRun, sans.types.IofQPart]( + sans.i_of_q._merge_contributions(list(runs.values())) + ) + # Add event merging provider - pipeline_triple.insert(sans.merge_runs) + pipeline_triple.insert(merge_sample_runs) + pipeline_triple.insert(merge_background_runs) iofq1 = pipeline_single.compute(BackgroundSubtractedIofQ) iofq3 = pipeline_triple.compute(BackgroundSubtractedIofQ) @@ -307,7 +364,9 @@ def get_mapped_path(filename: FilenameType) -> FilePath[FilenameType]: def test_beam_center_from_center_of_mass_is_close_to_verified_result(): params = make_params() pipeline = sciline.Pipeline(loki_providers(), params=params) - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) center = pipeline.compute(BeamCenter) reference = sc.vector([-0.0291487, -0.0181614, 0], unit='m') assert sc.allclose(center, reference) @@ -316,7 +375,9 @@ def test_beam_center_from_center_of_mass_is_close_to_verified_result(): def test_phi_with_gravity(): params = make_params() pipeline = sciline.Pipeline(loki_providers(), params=params) - pipeline.set_param_series(PixelMaskFilename, ['mask_new_July2022.xml']) + pipeline.set_param_series( + PixelMaskFilename, loki.data.loki_tutorial_mask_filenames() + ) pipeline[CorrectForGravity] = False data_no_grav = pipeline.compute( CleanWavelengthMasked[SampleRun, Numerator] diff --git a/tests/normalization_test.py b/tests/normalization_test.py index d2189379..5d62578e 100644 --- a/tests/normalization_test.py +++ b/tests/normalization_test.py @@ -5,13 +5,12 @@ import pytest import scipp as sc -from ess.isissans.data import get_path +from ess.isissans.data import sans2d_solid_angle_reference from ess.sans import normalization # See https://github.com/mantidproject/mantid/blob/main/instrument/SANS2D_Definition_Tubes.xml # noqa: E501 _SANS2D_PIXEL_RADIUS = 0.00405 * sc.Unit('m') _SANS2D_PIXEL_LENGTH = 0.002033984375 * sc.Unit('m') -_SANS2D_SOLID_ANGLE_REFERENCE_FILE = 'SANS2D00063091.SolidAngle_from_mantid.h5' def _sans2d_geometry(): @@ -37,7 +36,7 @@ def _mantid_sans2d_solid_angle_data(): simpleapi = pytest.importorskip("mantid.simpleapi") scippneutron = pytest.importorskip("scippneutron") - ws = simpleapi.Load(get_path('SANS2D00063091.nxs')) + ws = simpleapi.Load('SANS2D00063091.nxs') radius = _SANS2D_PIXEL_RADIUS length = _SANS2D_PIXEL_LENGTH @@ -56,8 +55,8 @@ def _mantid_sans2d_solid_angle_data(): outWs = simpleapi.SolidAngle(ws, method='HorizontalTube') da = scippneutron.from_mantid(outWs)['data']['spectrum', :120000:100] # Create new reference file: - # sc.io.hdf5.save_hdf5(da, _SANS2D_SOLID_ANGLE_REFERENCE_FILE) - # Note, also update the name, don't overwrite old reference files. + # sc.io.hdf5.save_hdf5(da, 'SANS2D00063091.SolidAngle_from_mantid.h5') + # Note, also update the registry version, don't overwrite old reference files. # Overwriting reference files breaks old versions of the repository. return da @@ -75,7 +74,7 @@ def test_solid_angle_compare_to_mantid(): def test_solid_angle_compare_to_reference_file(): - da = sc.io.load_hdf5(filename=get_path(_SANS2D_SOLID_ANGLE_REFERENCE_FILE)) + da = sc.io.load_hdf5(filename=sans2d_solid_angle_reference()) solid_angle = normalization.solid_angle( da, **_sans2d_geometry(),