diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index 993e2925..de7b271c 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -7,4 +7,5 @@ maxdepth: 1 installation amor/index estia/index +offspec/index ``` diff --git a/docs/user-guide/offspec/index.md b/docs/user-guide/offspec/index.md new file mode 100644 index 00000000..dc3f3ef5 --- /dev/null +++ b/docs/user-guide/offspec/index.md @@ -0,0 +1,8 @@ +# Offspec + +```{toctree} +--- +maxdepth: 1 +--- +offspec_reduction +``` diff --git a/docs/user-guide/offspec/offspec_reduction.ipynb b/docs/user-guide/offspec/offspec_reduction.ipynb new file mode 100644 index 00000000..35981c58 --- /dev/null +++ b/docs/user-guide/offspec/offspec_reduction.ipynb @@ -0,0 +1,447 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Collimated data reduction for OFFSPEC" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "This notebook implements a reduction workflow for reflectometry data collected from the ISIS instrument OFFSPEC using a collimated beam. This workflow implements the same procedure as the corresponding workflow in Mantid, see https://docs.mantidproject.org/nightly/techniques/ISIS_Reflectometry.html." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib widget\n", + "from datetime import datetime\n", + "import platform\n", + "\n", + "import scipp as sc\n", + "from orsopy import fileio\n", + "\n", + "from ess import reflectometry, offspec\n", + "from ess.reflectometry.types import *\n", + "from ess.offspec.types import *" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Loading some data\n", + "\n", + "In this example, we load some test data provided by the `offspec` package. We need a sample measurement (the sample is `Air | Si(790 A) | Cu(300 A) | SiO2`) and a direct beam measurement. The latter was obtained by positioning the detector directly in the beam of incident neutrons and moving the sample out of the way. It gives an estimate for the ISIS pulse structure as a function of time-of-flight." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "wf = offspec.OffspecWorkflow()\n", + "wf[Filename[SampleRun]] = offspec.data.offspec_sample_run()\n", + "wf[Filename[ReferenceRun]] = offspec.data.offspec_direct_beam_run()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "wf.visualize(ReflectivityOverQ, graph_attr={'rankdir': 'LR'})" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Populating the ORSO header\n", + "\n", + "We will write the reduced data file following the ORSO `.ort`` standard `__, to enable a metadata rich header. We will create an empty header and then populate this." + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "### The data source information" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "header = fileio.orso.Orso.empty()\n", + "\n", + "header.data_source.owner = fileio.base.Person(\n", + " name=\"Joshanial F. K. Cooper\",\n", + " affiliation=\"ISIS Neutron and Muon Source\",\n", + ")\n", + "header.data_source.experiment = fileio.data_source.Experiment(\n", + " title=\"OFFSPEC Sample Data\",\n", + " instrument=\"OFFSPEC\",\n", + " start_date=\"2020-12-14T10:34:02\",\n", + " probe=\"neutron\",\n", + " facility=\"RAL/ISIS/OFFSPEC\",\n", + ")\n", + "header.data_source.sample = fileio.data_source.Sample(\n", + " name=\"QCS sample\",\n", + " category=\"gas/solid\",\n", + " composition=\"Air | Si(790 A) | Cu(300 A) | SiO2\",\n", + ")\n", + "header.data_source.measurement = fileio.data_source.Measurement(\n", + " instrument_settings=fileio.data_source.InstrumentSettings(\n", + " incident_angle=fileio.base.Value(\n", + " wf.compute(RawDetectorData[SampleRun]).coords[\"theta\"].value,\n", + " wf.compute(RawDetectorData[SampleRun]).coords[\"theta\"].unit\n", + " ),\n", + " wavelength=None,\n", + " polarization=\"unpolarized\",\n", + " ),\n", + " data_files=[\n", + " offspec.data.offspec_sample_run().rsplit(\"/\", 1)[-1],\n", + " offspec.data.offspec_direct_beam_run().rsplit(\"/\", 1)[-1],\n", + " ],\n", + " scheme=\"energy-dispersive\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "### The reduction details\n", + "\n", + "The `reduction` section can start to be populated also. Entries such as `corrections` will be filled up through the reduction process." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "header.reduction.software = fileio.reduction.Software(\n", + " name=\"essreflectometry\", version=reflectometry.__version__, platform=platform.platform()\n", + ")\n", + "header.reduction.timestamp = datetime.now() # noqa: DTZ005\n", + "header.reduction.creator = fileio.base.Person(\n", + " name=\"I. D. Scientist\",\n", + " affiliation=\"European Spallation Source\",\n", + " contact=\"i.d.scientist@ess.eu\",\n", + ")\n", + "header.reduction.corrections = []\n", + "header.reduction.computer = platform.node()\n", + "header.reduction.script = \"offspec_reduction.ipynb\"" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "To ensure that the header object is carried through the process, we assign it to the sample `scipp.DataArray`. The direct beam header object will be overwritten at the normalisation step so we will keep this empty." + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "### Determining the region of interest\n", + "\n", + "To determine what region of the detector contains the specular peak intensity we plot the intensity distribution of the sample measurement over `spectrum` (detector pixel) and `time-of-flight`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "wf.compute(RawDetectorData[SampleRun]).hist(tof=50).plot(norm='log') \\\n", + "+ wf.compute(RawDetectorData[ReferenceRun]).hist(tof=50).plot(norm='log')" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "The region of interest is set in the workflow by setting `SpectrumLimits`. In this case it seems the specular peak is in the region `[389, 414]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "wf[SpectrumLimits] = (sc.scalar(389, unit=None), sc.scalar(414, unit=None))\n", + "header.reduction.corrections += ['region of interest defined as spectrum 389:415']" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## Coordinate transform graph\n", + "\n", + "To compute the wavelength $\\lambda$ we can use a coordinate transform graph. The OFFSPEC graph is the standard reflectometry graph, shown below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "sc.show_graph(wf.compute(CoordTransformationGraph[SampleRun]), simplified=True)" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "Since the direct beam measurement is __not__ a reflectometry measurement, we use the `no_scatter_graph` to convert this to wavelength." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "sc.show_graph(wf.compute(CoordTransformationGraph[ReferenceRun]), simplified=True)" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "## Normalization by monitor\n", + "It is necessary to normalize the sample and direct beam measurements by the summed monitor counts, which accounts for different lengths of measurement and long-timescale natural variation in the pulse. This will ensure that the final data has the correct scaling when the reflectivity data is normalized. First, we convert the data to wavelength, using the `no_scatter_graph` used previously for the direct beam.\n", + "\n", + "The most reliable monitor for the OFFSPEC instrument is 'monitor2' in the file, therefore this is used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "wf.compute(MonitorData[SampleRun]).plot()" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "A background subtraction is then performed on the monitor data, where the background is taken as any counts at wavelengths greater than 15 Å. We also mask all events in the sample- and direct-beam measurements that fall outside of the wavelength range we expect for the instrument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "wf[BackgroundMinWavelength] = sc.scalar(15, unit='angstrom')\n", + "wf[WavelengthBins] = sc.linspace(dim='wavelength', start=2, stop=14, num=2, unit='angstrom')\n", + "header.reduction.corrections += ['monitor background subtraction, background above 15 Å']" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "## Normalisation of sample by direct beam\n", + "The sample and direct beam measurements (which have been normalised by monitor counts) are then histogrammed in $Q$ to 100 geometrically spaced points. The histogrammed direct beam is then used to normalised the sample.\n", + "\n", + "Importantly, some relevant metadata (including the ORSO header object) is carried over." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "wf[QBins] = sc.geomspace('Q', 0.005, 0.033, 101, unit='1/angstrom')\n", + "header.reduction.corrections += [\"normalisation by direct beam\"]" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "We will assume a 3 % of $Q$ resolution function to be included in our file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "wf[QResolution] = 0.03" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "### Conversion to $Q$\n", + "This normalised data can then be used to compute the reflectivity as a function of the scattering vector $Q$.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "Roq = wf.compute(ReflectivityOverQ).hist()\n", + "Roq.plot(norm='log')" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "## Saving the scipp-reduced data as .ort\n", + "We constructed the ORSO header through the reduction process. We can now make use of this when we save our .ort file." + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "And it is necessary to add the column for our uncertainties, which details the **meaning** of the uncertainty values we have given." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "header.columns.append(fileio.base.ErrorColumn(error_of='R', error_type='uncertainty', value_is='sigma'))\n", + "header.columns.append(fileio.base.ErrorColumn(error_of='Q', error_type='resolution', value_is='sigma'))" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "Finally, we can save the file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "ds = fileio.orso.OrsoDataset(\n", + " header,\n", + " np.array([\n", + " sc.midpoints(Roq.coords['Q']).values,\n", + " Roq.data.values,\n", + " sc.stddevs(Roq.data).values,\n", + " Roq.coords['Q_resolution'].values]\n", + " ).T\n", + ")\n", + "\n", + "fileio.save_orso([ds], 'offspec.ort')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "!head -n 50 offspec.ort" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": {}, + "outputs": [], + "source": [ + "header.columns" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/ess/estia/maskings.py b/src/ess/estia/maskings.py index e1ed09fb..dfdeb40c 100644 --- a/src/ess/estia/maskings.py +++ b/src/ess/estia/maskings.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) import scipp as sc from ..reflectometry.types import ( diff --git a/src/ess/offspec/__init__.py b/src/ess/offspec/__init__.py new file mode 100644 index 00000000..e537a7d6 --- /dev/null +++ b/src/ess/offspec/__init__.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +from . import conversions, data, load, maskings, normalization, types, workflow +from .workflow import OffspecWorkflow + +__all__ = ( + "OffspecWorkflow", + "conversions", + "data", + "load", + "maskings", + "normalization", + "types", + "workflow", +) diff --git a/src/ess/offspec/conversions.py b/src/ess/offspec/conversions.py new file mode 100644 index 00000000..7b29ec31 --- /dev/null +++ b/src/ess/offspec/conversions.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +from scippneutron.conversion.graph import beamline, tof + +from ..reflectometry.types import ( + ReferenceRun, + SampleRun, +) +from .types import CoordTransformationGraph + + +def coordinate_transformation_graph_sample() -> CoordTransformationGraph[SampleRun]: + return { + **beamline.beamline(scatter=True), + **tof.elastic_wavelength("tof"), + } + + +def coordinate_transformation_graph_reference() -> ( + CoordTransformationGraph[ReferenceRun] +): + return { + **beamline.beamline(scatter=False), + **tof.elastic_wavelength("tof"), + } + + +providers = ( + coordinate_transformation_graph_sample, + coordinate_transformation_graph_reference, +) diff --git a/src/ess/offspec/corrections.py b/src/ess/offspec/corrections.py new file mode 100644 index 00000000..0d87bedb --- /dev/null +++ b/src/ess/offspec/corrections.py @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +import scipp as sc + + +def correct_by_monitor( + da: sc.DataArray, + mon: sc.DataArray, + wlims: tuple[sc.Variable, sc.Variable], + wbmin: sc.Variable, +) -> sc.DataArray: + "Corrects the data by the monitor intensity" + mon = mon - sc.values(mon['wavelength', wbmin:].mean()) + return da / sc.values(mon['wavelength', wlims[0] : wlims[-1]].sum()) diff --git a/src/ess/offspec/data.py b/src/ess/offspec/data.py new file mode 100644 index 00000000..b90aa368 --- /dev/null +++ b/src/ess/offspec/data.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +from ..reflectometry.types import Filename, ReferenceRun, SampleRun + +_version = "1" + + +def _make_pooch(): + import pooch + + return pooch.create( + path=pooch.os_cache("ess/offspec"), + env="ESS_AMOR_DATA_DIR", + base_url="https://public.esss.dk/groups/scipp/ess/offspec/{version}/", + version=_version, + registry={ + "sample.h5": "md5:02b8703230b6b1e6282c0d39eb94523c", + "direct_beam.h5": "md5:1c4e56afbd35edd96c7607e357981ccf", + }, + ) + + +_pooch = _make_pooch() + + +def offspec_sample_run() -> Filename[SampleRun]: + return Filename[SampleRun](_pooch.fetch("sample.h5")) + + +def offspec_direct_beam_run() -> Filename[ReferenceRun]: + return Filename[ReferenceRun](_pooch.fetch("direct_beam.h5")) + + +__all__ = [ + "offspec_direct_beam_run", + "offspec_sample_run", +] diff --git a/src/ess/offspec/load.py b/src/ess/offspec/load.py new file mode 100644 index 00000000..66c3771f --- /dev/null +++ b/src/ess/offspec/load.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +import scipp as sc + +from ..reflectometry.types import Filename, RawDetectorData, ReferenceRun, RunType +from .types import CoordTransformationGraph, MonitorData, NeXusMonitorName + + +def load_offspec_events( + filename: Filename[RunType], +) -> RawDetectorData[RunType]: + full = sc.io.load_hdf5(filename) + da = full['data'] + da.coords['theta'] = full.pop('Theta')[-1].data + da = da.bins.concat('tof') + return da + + +def load_offspec_monitor( + filename: Filename[RunType], + graph: CoordTransformationGraph[ReferenceRun], + monitor_name: NeXusMonitorName, +) -> MonitorData[RunType]: + full = sc.io.load_hdf5(filename) + mon = full["monitors"][monitor_name]["data"].transform_coords( + "wavelength", graph=graph + ) + return mon + + +providers = ( + load_offspec_events, + load_offspec_monitor, +) diff --git a/src/ess/offspec/maskings.py b/src/ess/offspec/maskings.py new file mode 100644 index 00000000..e498b397 --- /dev/null +++ b/src/ess/offspec/maskings.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +import scipp as sc + +from ..reflectometry.types import ( + WavelengthBins, +) +from .types import SpectrumLimits + + +def _not_between(v, a, b): + return (v < a) | (v > b) + + +def add_masks( + da: sc.DataArray, + spectrum_limits: SpectrumLimits, + wbins: WavelengthBins, +) -> sc.DataArray: + """ + Masks the data by range in the detector spectrum and by wavelength. + """ + da = da.assign_masks( + not_specularly_reflected_signal=_not_between( + da.coords['spectrum'], *spectrum_limits + ) + ) + da = da.bins.assign_masks( + wavelength=_not_between( + da.bins.coords['wavelength'], + wbins[0], + wbins[-1], + ), + ) + return da + + +providers = () diff --git a/src/ess/offspec/normalization.py b/src/ess/offspec/normalization.py new file mode 100644 index 00000000..8ab2ca00 --- /dev/null +++ b/src/ess/offspec/normalization.py @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +from ..reflectometry.conversions import reflectometry_q +from ..reflectometry.types import ( + QResolution, + ReducibleData, + Reference, + ReferenceRun, + Sample, + SampleRun, +) + + +def evaluate_reference( + reference: ReducibleData[ReferenceRun], + sample: ReducibleData[SampleRun], + qresolution: QResolution, +) -> Reference: + """ + Adds a :math:`Q` coordinate computed as if the data came from + the sample measurement, that is, they use the ``sample_rotation`` + of the sample measurement. + """ + ref = reference.copy(deep=False) + ref.coords.pop("theta") + ref.bins.coords['Q'] = reflectometry_q( + wavelength=ref.bins.coords['wavelength'], theta=sample.coords['theta'] + ) + ref.bins.coords['Q_resolution'] = qresolution * ref.bins.coords['Q'] + return ref + + +def evaluate_sample( + reference: ReducibleData[ReferenceRun], + sample: ReducibleData[SampleRun], +) -> Sample: + """ + Adds the :math:`Q` coordinate. + """ + sample = sample.copy(deep=False) + sample.bins.coords['Q'] = reflectometry_q( + wavelength=sample.bins.coords['wavelength'], theta=sample.coords['theta'] + ) + return sample + + +providers = ( + evaluate_reference, + evaluate_sample, +) diff --git a/src/ess/offspec/types.py b/src/ess/offspec/types.py new file mode 100644 index 00000000..388de10f --- /dev/null +++ b/src/ess/offspec/types.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +from typing import NewType + +import sciline +import scipp as sc + +from ess.reduce.nexus import types as reduce_t + +from ..reflectometry.types import RunType + +SpectrumLimits = NewType("SpectrumLimits", tuple[sc.Variable, sc.Variable]) +BackgroundMinWavelength = NewType("BackgroundMinWavelength", sc.Variable) + + +class CoordTransformationGraph(sciline.Scope[RunType, dict], dict): + """Coordinate transformation for the runtype""" + + +class MonitorData(sciline.Scope[RunType, sc.DataArray], sc.DataArray): + """ "Monitor data from the run file, with background subtracted""" + + +NeXusMonitorName = reduce_t.NeXusName diff --git a/src/ess/offspec/workflow.py b/src/ess/offspec/workflow.py new file mode 100644 index 00000000..8963813c --- /dev/null +++ b/src/ess/offspec/workflow.py @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +import sciline + +from ..reflectometry import providers as reflectometry_providers +from ..reflectometry.types import ( + RawDetectorData, + ReducibleData, + RunType, + WavelengthBins, +) +from . import conversions, load, maskings, normalization +from .corrections import correct_by_monitor +from .maskings import add_masks +from .types import ( + BackgroundMinWavelength, + CoordTransformationGraph, + MonitorData, + NeXusMonitorName, + SpectrumLimits, +) + + +def OffspecWorkflow() -> sciline.Pipeline: + """ + Workflow with default parameters for the Offspec instrument. + """ + ps = ( + *providers, + *reflectometry_providers, + *load.providers, + *conversions.providers, + *maskings.providers, + *normalization.providers, + ) + return sciline.Pipeline(providers=ps, params={NeXusMonitorName: 'monitor2'}) + + +def add_coords_masks_and_apply_corrections( + da: RawDetectorData[RunType], + spectrum_limits: SpectrumLimits, + wlims: WavelengthBins, + wbmin: BackgroundMinWavelength, + monitor: MonitorData[RunType], + graph: CoordTransformationGraph[RunType], +) -> ReducibleData[RunType]: + """ + Computes coordinates, masks and corrections that are + the same for the sample measurement and the reference measurement. + """ + da = da.transform_coords(('wavelength',), graph=graph) + da = add_masks(da, spectrum_limits, wlims) + da = correct_by_monitor(da, monitor, wlims, wbmin) + return da + + +providers = (add_coords_masks_and_apply_corrections,) diff --git a/src/ess/reflectometry/gui.py b/src/ess/reflectometry/gui.py index 87c9b8e1..02fd6d09 100644 --- a/src/ess/reflectometry/gui.py +++ b/src/ess/reflectometry/gui.py @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) import glob import os import uuid diff --git a/src/ess/reflectometry/normalization.py b/src/ess/reflectometry/normalization.py index b1e249dc..cdd5a396 100644 --- a/src/ess/reflectometry/normalization.py +++ b/src/ess/reflectometry/normalization.py @@ -60,16 +60,27 @@ def reduce_sample_over_q( Returns reflectivity as a function of :math:`Q`. """ s = sample.bins.concat().bin(Q=qbins) - h = sc.values(reference.hist(Q=s.coords['Q'])) + h = sc.values( + (reference if reference.bins is None else reference.bins.concat()).hist( + Q=s.coords['Q'] + ) + ) R = s / h.data - R.coords['Q_resolution'] = sc.sqrt( - ( - (sc.values(reference) * reference.coords['Q_resolution'] ** 2) - .flatten(to='Q') - .hist(Q=s.coords['Q']) + if 'Q_resolution' in reference.coords or 'Q_resolution' in reference.bins.coords: + resolution = ( + reference.coords['Q_resolution'] + if 'Q_resolution' in reference.coords + else reference.bins.coords['Q_resolution'] ) - / h - ).data + weighted_resolution = sc.values(reference) * resolution**2 + R.coords['Q_resolution'] = sc.sqrt( + ( + weighted_resolution + if weighted_resolution.bins is None + else weighted_resolution.bins.concat() + ).hist(Q=s.coords['Q']) + / h + ).data return R