From 827ad5940c7a3c61c4f221bdb9977c4748ee439d Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Fri, 2 Feb 2024 11:04:44 +0100 Subject: [PATCH 1/2] fix: separate path and filename providers The old version did not allow loading local files into the workflow, everything had to go through pooch. With this change the user can directly specify a local file path or the name of a file that is in the pooch data repository. --- docs/examples/amor.ipynb | 61 ++++++++++----------------- src/essreflectometry/amor/__init__.py | 2 +- src/essreflectometry/amor/data.py | 10 ++++- src/essreflectometry/amor/load.py | 21 ++++----- src/essreflectometry/orso.py | 12 +++--- src/essreflectometry/types.py | 8 +++- tests/amor/pipeline_test.py | 7 +-- tests/orso_test.py | 6 ++- 8 files changed, 60 insertions(+), 67 deletions(-) diff --git a/docs/examples/amor.ipynb b/docs/examples/amor.ipynb index 151e85b..0d8e604 100644 --- a/docs/examples/amor.ipynb +++ b/docs/examples/amor.ipynb @@ -20,7 +20,8 @@ "import scipp as sc\n", "import sciline\n", "from essreflectometry.amor import providers, default_parameters\n", - "from essreflectometry.types import *" + "from essreflectometry.types import *\n", + "from essreflectometry.amor.data import providers as amor_data" ] }, { @@ -29,27 +30,17 @@ "metadata": {}, "outputs": [], "source": [ - "params = {\n", - " **default_parameters,\n", - " QBins: sc.geomspace(dim='Q', start=0.008, stop=0.075, num=200, unit='1/angstrom'),\n", - " SampleRotation[Sample]: sc.scalar(0.7989, unit='deg'),\n", - " Filename[Sample]: \"sample.nxs\",\n", - " SampleRotation[Reference]: sc.scalar(0.8389, unit='deg'),\n", - " Filename[Reference]: \"reference.nxs\",\n", - " WavelengthEdges: sc.array(dims=['wavelength'], values=[2.4, 16.0], unit='angstrom'),\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pipeline = sciline.Pipeline(\n", - " providers,\n", - " params=params\n", - ")" + "pl = sciline.Pipeline(\n", + " (*providers, *amor_data),\n", + " params=default_parameters\n", + ")\n", + "\n", + "pl[QBins] = sc.geomspace(dim='Q', start=0.008, stop=0.075, num=200, unit='1/angstrom')\n", + "pl[SampleRotation[Sample]] = sc.scalar(0.7989, unit='deg')\n", + "pl[PoochFilename[Sample]] = \"sample.nxs\"\n", + "pl[SampleRotation[Reference]] = sc.scalar(0.8389, unit='deg')\n", + "pl[PoochFilename[Reference]] = \"reference.nxs\"\n", + "pl[WavelengthEdges] = sc.array(dims=['wavelength'], values=[2.4, 16.0], unit='angstrom')" ] }, { @@ -58,7 +49,7 @@ "metadata": {}, "outputs": [], "source": [ - "pipeline.visualize((NormalizedIofQ, QResolution), graph_attr={'rankdir': 'LR'})" + "pl.visualize((NormalizedIofQ, QResolution), graph_attr={'rankdir': 'LR'})" ] }, { @@ -68,7 +59,7 @@ "outputs": [], "source": [ "# Compute I over Q and the standard deviation of Q\n", - "ioq, qstd = pipeline.compute((NormalizedIofQ, QResolution)).values()" + "ioq, qstd = pl.compute((NormalizedIofQ, QResolution)).values()" ] }, { @@ -103,7 +94,7 @@ "outputs": [], "source": [ "from essreflectometry.types import ThetaData\n", - "pipeline.compute(ThetaData[Sample])\\\n", + "pl.compute(ThetaData[Sample])\\\n", " .bins.concat('detector_number')\\\n", " .hist(\n", " theta=sc.linspace(dim='theta', start=0.0, stop=1.2, num=165, unit='deg').to(unit='rad'),\n", @@ -149,22 +140,14 @@ "metadata": {}, "outputs": [], "source": [ - "providers_with_metadata = (\n", - " *providers,\n", - " *orso.providers,\n", - " *amor_orso.providers,\n", - ")\n", + "for p in (*orso.providers, *amor_orso.providers):\n", + " pl.insert(p)\n", "\n", - "params[orso.OrsoCreator] = orso.OrsoCreator(fileio.base.Person(\n", + "pl[orso.OrsoCreator] = orso.OrsoCreator(fileio.base.Person(\n", " name='Max Mustermann',\n", " affiliation='European Spallation Source ERIC',\n", " contact='max.mustermann@ess.eu',\n", - "))\n", - "\n", - "metadata_pipeline = sciline.Pipeline(\n", - " providers_with_metadata,\n", - " params=params\n", - ")" + "))" ] }, { @@ -180,7 +163,7 @@ "metadata": {}, "outputs": [], "source": [ - "iofq_dataset = metadata_pipeline.compute(orso.OrsoIofQDataset)" + "iofq_dataset = pl.compute(orso.OrsoIofQDataset)" ] }, { @@ -243,7 +226,7 @@ "metadata": {}, "outputs": [], "source": [ - "iofq_dataset.info.reduction.corrections = orso.find_corrections(metadata_pipeline.get(orso.OrsoIofQDataset))" + "iofq_dataset.info.reduction.corrections = orso.find_corrections(pl.get(orso.OrsoIofQDataset))" ] }, { diff --git a/src/essreflectometry/amor/__init__.py b/src/essreflectometry/amor/__init__.py index de9d5c0..33e1587 100644 --- a/src/essreflectometry/amor/__init__.py +++ b/src/essreflectometry/amor/__init__.py @@ -13,7 +13,7 @@ SampleSize, WavelengthEdges, ) -from . import beamline, conversions, load, resolution +from . import beamline, conversions, data, load, resolution from .beamline import instrument_view_components from .instrument_view import instrument_view from .types import ( diff --git a/src/essreflectometry/amor/data.py b/src/essreflectometry/amor/data.py index a6f608f..4b73a63 100644 --- a/src/essreflectometry/amor/data.py +++ b/src/essreflectometry/amor/data.py @@ -1,5 +1,8 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) + +from ..types import FilePath, PoochFilename, Run + _version = '1' __all__ = ['get_path'] @@ -23,11 +26,14 @@ def _make_pooch(): _pooch = _make_pooch() -def get_path(name: str) -> str: +def get_path(filename: PoochFilename[Run]) -> FilePath[Run]: """ Return the path to a data file bundled with scippneutron. This function only works with example data and cannot handle paths to custom files. """ - return _pooch.fetch(name) + return FilePath[Run](_pooch.fetch(filename)) + + +providers = (get_path,) diff --git a/src/essreflectometry/amor/load.py b/src/essreflectometry/amor/load.py index fa84aca..d1bd6c8 100644 --- a/src/essreflectometry/amor/load.py +++ b/src/essreflectometry/amor/load.py @@ -7,8 +7,7 @@ import scippnexus as snx from ..logging import get_logger -from ..types import ChopperCorrectedTofEvents, Filename, RawData, RawEvents, Run -from .data import get_path +from ..types import ChopperCorrectedTofEvents, FilePath, RawData, RawEvents, Run from .types import BeamlineParams @@ -79,35 +78,34 @@ def _assemble_event_data(dg: sc.DataGroup) -> sc.DataArray: return events -def _load_nexus_entry(filename: Union[str, Path]) -> sc.DataGroup: +def _load_nexus_entry(filepath: Union[str, Path]) -> sc.DataGroup: """Load the single entry of a nexus file.""" - with snx.File(filename, 'r') as f: + with snx.File(filepath, 'r') as f: if len(f.keys()) != 1: raise snx.NexusStructureError( - f"Expected a single entry in file {filename}, got {len(f.keys())}" + f"Expected a single entry in file {filepath}, got {len(f.keys())}" ) return f['entry'][()] -def load_raw_nexus(filename: Filename[Run]) -> RawData[Run]: +def load_raw_nexus(filepath: FilePath[Run]) -> RawData[Run]: """Load unprocessed data and metadata from an Amor NeXus file. Parameters ---------- - filename: - Filename of the NeXus file. + filepath: + File path of the NeXus file. Returns ------- : Data and metadata. """ - filename = get_path(filename) get_logger('amor').info( "Loading '%s' as an Amor NeXus file", - filename.filename if hasattr(filename, 'filename') else filename, + filepath.filename if hasattr(filepath, 'filename') else filepath, ) - return RawData(_load_nexus_entry(filename)) + return RawData(_load_nexus_entry(filepath)) def extract_events( @@ -128,7 +126,6 @@ def extract_events( Data array object for Amor dataset. """ data = _assemble_event_data(raw_data) - # Recent versions of scippnexus no longer add variances for events by default, so # we add them here if they are missing. if data.bins.constituents['data'].data.variances is None: diff --git a/src/essreflectometry/orso.py b/src/essreflectometry/orso.py index b04ea93..8111360 100644 --- a/src/essreflectometry/orso.py +++ b/src/essreflectometry/orso.py @@ -19,7 +19,7 @@ from .supermirror import SupermirrorCalibrationFactor from .types import ( ChopperCorrectedTofEvents, - Filename, + FilePath, FootprintCorrectedData, IofQ, RawData, @@ -93,18 +93,18 @@ def parse_orso_sample(raw_data: RawData[Sample]) -> OrsoSample: def build_orso_measurement( - sample_filename: Filename[Sample], - reference_filename: Optional[Filename[Reference]], + sample_filepath: FilePath[Sample], + reference_filepath: Optional[FilePath[Reference]], instrument: Optional[OrsoInstrument], ) -> OrsoMeasurement: """Assemble ORSO measurement metadata.""" # TODO populate timestamp # doesn't work with a local file because we need the timestamp of the original, # SciCat can provide that - if reference_filename: + if reference_filepath: additional_files = [ orso_base.File( - file=os.path.basename(reference_filename), comment='supermirror' + file=os.path.basename(reference_filepath), comment='supermirror' ) ] else: @@ -112,7 +112,7 @@ def build_orso_measurement( return OrsoMeasurement( data_source.Measurement( instrument_settings=instrument, - data_files=[orso_base.File(file=os.path.basename(sample_filename))], + data_files=[orso_base.File(file=os.path.basename(sample_filepath))], additional_files=additional_files, ) ) diff --git a/src/essreflectometry/types.py b/src/essreflectometry/types.py index e646829..fd87b42 100644 --- a/src/essreflectometry/types.py +++ b/src/essreflectometry/types.py @@ -82,8 +82,12 @@ class IofQ(sciline.Scope[Run, sc.DataArray], sc.DataArray): '''Include only events within the specified edges.''' -class Filename(sciline.Scope[Run, str], str): - """Filename of the event data nexus file.""" +class PoochFilename(sciline.Scope[Run, str], str): + """Name of an event data nexus file in the pooch data repository.""" + + +class FilePath(sciline.Scope[Run, str], str): + """File path of an event data nexus file.""" class SampleRotation(sciline.Scope[Run, sc.Variable], sc.Variable): diff --git a/tests/amor/pipeline_test.py b/tests/amor/pipeline_test.py index 161fd01..dd8cfc5 100644 --- a/tests/amor/pipeline_test.py +++ b/tests/amor/pipeline_test.py @@ -12,6 +12,7 @@ from essreflectometry.amor import default_parameters from essreflectometry.amor import orso as amor_orso from essreflectometry.amor import providers +from essreflectometry.amor.data import get_path from essreflectometry.types import * @@ -23,9 +24,9 @@ def amor_pipeline() -> sciline.Pipeline: dim='Q', start=0.008, stop=0.075, num=200, unit='1/angstrom' ), SampleRotation[Sample]: sc.scalar(0.7989, unit='deg'), - Filename[Sample]: "sample.nxs", + PoochFilename[Sample]: "sample.nxs", SampleRotation[Reference]: sc.scalar(0.8389, unit='deg'), - Filename[Reference]: "reference.nxs", + PoochFilename[Reference]: "reference.nxs", WavelengthEdges: sc.array( dims=['wavelength'], values=[2.4, 16.0], unit='angstrom' ), @@ -38,7 +39,7 @@ def amor_pipeline() -> sciline.Pipeline: ), } return sciline.Pipeline( - (*providers, *orso.providers, *amor_orso.providers), params=params + (*providers, *orso.providers, *amor_orso.providers, get_path), params=params ) diff --git a/tests/orso_test.py b/tests/orso_test.py index 51b16f1..536ad13 100644 --- a/tests/orso_test.py +++ b/tests/orso_test.py @@ -7,17 +7,19 @@ import essreflectometry from essreflectometry import orso +from essreflectometry.amor.data import providers as amor_data_providers from essreflectometry.amor.load import providers as amor_load_providers -from essreflectometry.types import Filename, Sample +from essreflectometry.types import PoochFilename, Sample def test_build_orso_data_source(): pipeline = sciline.Pipeline( ( + *amor_data_providers, *amor_load_providers, *orso.providers, ), - params={Filename[Sample]: 'sample.nxs'}, + params={PoochFilename[Sample]: 'sample.nxs'}, ) data_source = pipeline.compute(orso.OrsoDataSource) expected = fileio.data_source.DataSource( From f62978df55fa8f244b0f8575f97f196e095273d2 Mon Sep 17 00:00:00 2001 From: Johannes Kasimir Date: Tue, 12 Mar 2024 15:41:32 +0100 Subject: [PATCH 2/2] refactor: load detector separately, explicitly pass pararameters to providers --- conda/meta.yaml | 1 + pyproject.toml | 1 + requirements/base.in | 1 + requirements/base.txt | 31 +++-- requirements/basetest.txt | 4 +- requirements/ci.txt | 6 +- requirements/dev.txt | 28 ++-- requirements/docs.txt | 14 +- requirements/mypy.txt | 4 +- requirements/nightly.in | 1 + requirements/nightly.txt | 24 ++-- requirements/wheels.txt | 4 +- src/essreflectometry/amor/__init__.py | 6 +- src/essreflectometry/amor/conversions.py | 27 ++-- src/essreflectometry/amor/load.py | 165 +++++++---------------- src/essreflectometry/amor/resolution.py | 88 +++++------- src/essreflectometry/conversions.py | 25 +++- src/essreflectometry/corrections.py | 14 +- src/essreflectometry/load.py | 7 + src/essreflectometry/orso.py | 33 +++-- src/essreflectometry/types.py | 29 +++- 21 files changed, 240 insertions(+), 273 deletions(-) create mode 100644 src/essreflectometry/load.py diff --git a/conda/meta.yaml b/conda/meta.yaml index f62f25e..2574f28 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -20,6 +20,7 @@ requirements: - sciline>=23.9.1 - scipp>=23.8.0 - scippneutron>=23.9.0 + - essreduce - python>=3.10 test: diff --git a/pyproject.toml b/pyproject.toml index 8f3b407..96ccc84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dependencies = [ "sciline>=23.9.1", "scipp>=23.8.0", "scippneutron>=23.9.0", + "essreduce", ] dynamic = ["version"] diff --git a/requirements/base.in b/requirements/base.in index ae88a22..2a4e6f5 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -11,3 +11,4 @@ orsopy sciline>=23.9.1 scipp>=23.8.0 scippneutron>=23.9.0 +essreduce diff --git a/requirements/base.txt b/requirements/base.txt index e795ad9..63683c1 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:697621007a6df52e05884bd9bf630052e4aa6fbe +# SHA1:1b431955cc8f1e9144b3b02f13d86b0693935140 # # This file is autogenerated by pip-compile-multi # To update, run: @@ -15,16 +15,18 @@ click==8.1.7 # via dask cloudpickle==3.0.0 # via dask -comm==0.2.1 +comm==0.2.2 # via ipywidgets contourpy==1.2.0 # via matplotlib cycler==0.12.1 # via matplotlib -dask==2024.2.0 +dask==2024.3.0 # via -r base.in decorator==5.1.1 # via ipython +essreduce==24.3.11 + # via -r base.in exceptiongroup==1.2.0 # via ipython executing==2.0.1 @@ -41,11 +43,11 @@ h5py==3.10.0 # scippnexus idna==3.6 # via requests -importlib-metadata==7.0.1 +importlib-metadata==7.0.2 # via dask ipydatawidgets==4.3.5 # via pythreejs -ipython==8.22.1 +ipython==8.22.2 # via ipywidgets ipywidgets==8.1.2 # via @@ -73,9 +75,9 @@ numpy==1.26.4 # scipp # scippneutron # scipy -orsopy==1.1.0 +orsopy==1.2.0 # via -r base.in -packaging==23.2 +packaging==24.0 # via # dask # matplotlib @@ -102,9 +104,9 @@ pure-eval==0.2.2 # via stack-data pygments==2.17.2 # via ipython -pyparsing==3.1.1 +pyparsing==3.1.2 # via matplotlib -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 # via # -r base.in # matplotlib @@ -122,12 +124,15 @@ sciline==24.2.1 scipp==24.2.0 # via # -r base.in + # essreduce # scippneutron # scippnexus scippneutron==24.1.0 # via -r base.in -scippnexus==23.12.1 - # via scippneutron +scippnexus==24.3.1 + # via + # essreduce + # scippneutron scipy==1.12.0 # via # scippneutron @@ -142,7 +147,7 @@ toolz==0.12.1 # via # dask # partd -traitlets==5.14.1 +traitlets==5.14.2 # via # comm # ipython @@ -158,5 +163,5 @@ wcwidth==0.2.13 # via prompt-toolkit widgetsnbextension==4.0.10 # via ipywidgets -zipp==3.17.0 +zipp==3.18.0 # via importlib-metadata diff --git a/requirements/basetest.txt b/requirements/basetest.txt index bc93620..6eabd7a 100644 --- a/requirements/basetest.txt +++ b/requirements/basetest.txt @@ -9,11 +9,11 @@ exceptiongroup==1.2.0 # via pytest iniconfig==2.0.0 # via pytest -packaging==23.2 +packaging==24.0 # via pytest pluggy==1.4.0 # via pytest -pytest==8.0.1 +pytest==8.1.1 # via -r basetest.in tomli==2.0.1 # via pytest diff --git a/requirements/ci.txt b/requirements/ci.txt index eeef86e..2f27a0e 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -5,7 +5,7 @@ # # pip-compile-multi # -cachetools==5.3.2 +cachetools==5.3.3 # via tox certifi==2024.2.2 # via requests @@ -27,7 +27,7 @@ gitpython==3.1.42 # via -r ci.in idna==3.6 # via requests -packaging==23.2 +packaging==24.0 # via # -r ci.in # pyproject-api @@ -48,7 +48,7 @@ tomli==2.0.1 # via # pyproject-api # tox -tox==4.13.0 +tox==4.14.1 # via -r ci.in urllib3==2.2.1 # via requests diff --git a/requirements/dev.txt b/requirements/dev.txt index 52c5b20..49766c4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -46,7 +46,7 @@ isoduration==20.11.0 # via jsonschema jinja2-ansible-filters==1.3.2 # via copier -json5==0.9.17 +json5==0.9.22 # via jupyterlab-server jsonpointer==2.4 # via jsonschema @@ -55,21 +55,21 @@ jsonschema[format-nongpl]==4.21.1 # jupyter-events # jupyterlab-server # nbformat -jupyter-events==0.9.0 +jupyter-events==0.9.1 # via jupyter-server -jupyter-lsp==2.2.2 +jupyter-lsp==2.2.4 # via jupyterlab -jupyter-server==2.12.5 +jupyter-server==2.13.0 # via # jupyter-lsp # jupyterlab # jupyterlab-server # notebook-shim -jupyter-server-terminals==0.5.2 +jupyter-server-terminals==0.5.3 # via jupyter-server -jupyterlab==4.1.2 +jupyterlab==4.1.4 # via -r dev.in -jupyterlab-server==2.25.3 +jupyterlab-server==2.25.4 # via jupyterlab notebook-shim==0.2.4 # via jupyterlab @@ -79,7 +79,7 @@ pathspec==0.12.1 # via copier pip-compile-multi==2.6.3 # via -r dev.in -pip-tools==7.4.0 +pip-tools==7.4.1 # via pip-compile-multi plumbum==1.8.2 # via copier @@ -87,9 +87,9 @@ prometheus-client==0.20.0 # via jupyter-server pycparser==2.21 # via cffi -pydantic==2.6.1 +pydantic==2.6.4 # via copier -pydantic-core==2.16.2 +pydantic-core==2.16.3 # via pydantic python-json-logger==2.0.7 # via jupyter-events @@ -107,17 +107,17 @@ rfc3986-validator==0.1.1 # jupyter-events send2trash==1.8.2 # via jupyter-server -sniffio==1.3.0 +sniffio==1.3.1 # via # anyio # httpx -terminado==0.18.0 +terminado==0.18.1 # via # jupyter-server # jupyter-server-terminals toposort==1.10 # via pip-compile-multi -types-python-dateutil==2.8.19.20240106 +types-python-dateutil==2.8.19.20240311 # via arrow uri-template==1.3.0 # via jsonschema @@ -125,7 +125,7 @@ webcolors==1.13 # via jsonschema websocket-client==1.7.0 # via jupyter-server -wheel==0.42.0 +wheel==0.43.0 # via pip-tools # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/docs.txt b/requirements/docs.txt index 5269f4b..9e6c644 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -38,7 +38,7 @@ fastjsonschema==2.19.1 # via nbformat imagesize==1.4.1 # via sphinx -ipykernel==6.29.2 +ipykernel==6.29.3 # via -r docs.in jinja2==3.1.3 # via @@ -50,11 +50,11 @@ jsonschema==4.21.1 # via nbformat jsonschema-specifications==2023.12.1 # via jsonschema -jupyter-client==8.6.0 +jupyter-client==8.6.1 # via # ipykernel # nbclient -jupyter-core==5.7.1 +jupyter-core==5.7.2 # via # ipykernel # jupyter-client @@ -79,11 +79,11 @@ mistune==3.0.2 # via nbconvert myst-parser==2.0.0 # via -r docs.in -nbclient==0.9.0 +nbclient==0.10.0 # via nbconvert -nbconvert==7.16.1 +nbconvert==7.16.2 # via nbsphinx -nbformat==5.9.2 +nbformat==5.10.2 # via # nbclient # nbconvert @@ -147,7 +147,7 @@ tornado==6.4 # via # ipykernel # jupyter-client -typing-extensions==4.9.0 +typing-extensions==4.10.0 # via pydata-sphinx-theme webencodings==0.5.1 # via diff --git a/requirements/mypy.txt b/requirements/mypy.txt index ac28568..066d33a 100644 --- a/requirements/mypy.txt +++ b/requirements/mypy.txt @@ -6,9 +6,9 @@ # pip-compile-multi # -r test.txt -mypy==1.8.0 +mypy==1.9.0 # via -r mypy.in mypy-extensions==1.0.0 # via mypy -typing-extensions==4.9.0 +typing-extensions==4.10.0 # via mypy diff --git a/requirements/nightly.in b/requirements/nightly.in index abd07ec..cc84493 100644 --- a/requirements/nightly.in +++ b/requirements/nightly.in @@ -6,6 +6,7 @@ python-dateutil graphviz pythreejs orsopy +essreduce plopp @ git+https://github.com/scipp/plopp@main sciline @ git+https://github.com/scipp/sciline@main scippneutron @ git+https://github.com/scipp/scippneutron@main diff --git a/requirements/nightly.txt b/requirements/nightly.txt index bc2e020..2321e8b 100644 --- a/requirements/nightly.txt +++ b/requirements/nightly.txt @@ -1,4 +1,4 @@ -# SHA1:32b8662e93850ff7332a96796cff863431faf020 +# SHA1:0d2767c7e7d393923911be667ad07d7dd7cd5e79 # # This file is autogenerated by pip-compile-multi # To update, run: @@ -16,16 +16,18 @@ click==8.1.7 # via dask cloudpickle==3.0.0 # via dask -comm==0.2.1 +comm==0.2.2 # via ipywidgets contourpy==1.2.0 # via matplotlib cycler==0.12.1 # via matplotlib -dask==2024.2.0 +dask==2024.3.0 # via -r nightly.in decorator==5.1.1 # via ipython +essreduce==24.3.11 + # via -r nightly.in executing==2.0.1 # via stack-data fonttools==4.49.0 @@ -40,11 +42,11 @@ h5py==3.10.0 # scippnexus idna==3.6 # via requests -importlib-metadata==7.0.1 +importlib-metadata==7.0.2 # via dask ipydatawidgets==4.3.5 # via pythreejs -ipython==8.22.1 +ipython==8.22.2 # via ipywidgets ipywidgets==8.1.2 # via @@ -72,7 +74,7 @@ numpy==1.26.4 # scipp # scippneutron # scipy -orsopy==1.1.0 +orsopy==1.2.0 # via -r nightly.in parso==0.8.3 # via jedi @@ -96,9 +98,9 @@ pure-eval==0.2.2 # via stack-data pygments==2.17.2 # via ipython -pyparsing==3.1.1 +pyparsing==3.1.2 # via matplotlib -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 # via # -r nightly.in # matplotlib @@ -116,6 +118,7 @@ sciline @ git+https://github.com/scipp/sciline@main scipp @ https://github.com/scipp/scipp/releases/download/nightly/scipp-nightly-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl # via # -r nightly.in + # essreduce # scippneutron # scippnexus scippneutron @ git+https://github.com/scipp/scippneutron@main @@ -123,6 +126,7 @@ scippneutron @ git+https://github.com/scipp/scippneutron@main scippnexus @ git+https://github.com/scipp/scippnexus@main # via # -r nightly.in + # essreduce # scippneutron scipy==1.12.0 # via @@ -138,7 +142,7 @@ toolz==0.12.1 # via # dask # partd -traitlets==5.14.1 +traitlets==5.14.2 # via # comm # ipython @@ -154,5 +158,5 @@ wcwidth==0.2.13 # via prompt-toolkit widgetsnbextension==4.0.10 # via ipywidgets -zipp==3.17.0 +zipp==3.18.0 # via importlib-metadata diff --git a/requirements/wheels.txt b/requirements/wheels.txt index 2e33cfa..23d6d31 100644 --- a/requirements/wheels.txt +++ b/requirements/wheels.txt @@ -5,9 +5,9 @@ # # pip-compile-multi # -build==1.0.3 +build==1.1.1 # via -r wheels.in -packaging==23.2 +packaging==24.0 # via build pyproject-hooks==1.0.0 # via build diff --git a/src/essreflectometry/amor/__init__.py b/src/essreflectometry/amor/__init__.py index 33e1587..83c41d0 100644 --- a/src/essreflectometry/amor/__init__.py +++ b/src/essreflectometry/amor/__init__.py @@ -9,7 +9,10 @@ BeamSize, DetectorSpatialResolution, Gravity, + NeXusDetectorName, + RawDetector, Run, + SamplePosition, SampleSize, WavelengthEdges, ) @@ -18,7 +21,6 @@ from .instrument_view import instrument_view from .types import ( AngularResolution, - BeamlineParams, Chopper1Position, Chopper2Position, ChopperFrequency, @@ -52,6 +54,8 @@ ChopperPhase[Run]: sc.scalar(-8.0, unit='deg'), Chopper1Position[Run]: sc.vector(value=[0, 0, -15.5], unit='m'), Chopper2Position[Run]: sc.vector(value=[0, 0, -14.5], unit='m'), + SamplePosition[Run]: sc.vector([0, 0, 0], unit='m'), + NeXusDetectorName[Run]: 'multiblade_detector', } del sc diff --git a/src/essreflectometry/amor/conversions.py b/src/essreflectometry/amor/conversions.py index 4c94148..1347e84 100644 --- a/src/essreflectometry/amor/conversions.py +++ b/src/essreflectometry/amor/conversions.py @@ -2,34 +2,23 @@ # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) import scipp as sc -from ..conversions import specular_reflection as spec_relf_graph -from ..types import SpecularReflectionCoordTransformGraph +from ..types import IncidentBeam, Run, SamplePosition +from .types import Chopper1Position, Chopper2Position def incident_beam( - *, - source_chopper_1: sc.Variable, - source_chopper_2: sc.Variable, - sample_position: sc.Variable, -) -> sc.Variable: + source_chopper_1_position: Chopper1Position[Run], + source_chopper_2_position: Chopper2Position[Run], + sample_position: SamplePosition[Run], +) -> IncidentBeam[Run]: """ Compute the incident beam vector from the source chopper position vector, instead of the source_position vector. """ chopper_midpoint = ( - source_chopper_1.value['position'].data - + source_chopper_2.value['position'].data + source_chopper_1_position + source_chopper_2_position ) * sc.scalar(0.5) return sample_position - chopper_midpoint -def specular_reflection() -> SpecularReflectionCoordTransformGraph: - """ - Generate a coordinate transformation graph for Amor reflectometry. - """ - graph = spec_relf_graph() - graph['incident_beam'] = incident_beam - return SpecularReflectionCoordTransformGraph(graph) - - -providers = (specular_reflection,) +providers = (incident_beam,) diff --git a/src/essreflectometry/amor/load.py b/src/essreflectometry/amor/load.py index d1bd6c8..0a4410e 100644 --- a/src/essreflectometry/amor/load.py +++ b/src/essreflectometry/amor/load.py @@ -1,40 +1,49 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) -from pathlib import Path -from typing import Union - import scipp as sc -import scippnexus as snx +from ess.reduce import nexus -from ..logging import get_logger -from ..types import ChopperCorrectedTofEvents, FilePath, RawData, RawEvents, Run -from .types import BeamlineParams +from ..types import ( + ChopperCorrectedTofEvents, + DetectorPosition, + FilePath, + NeXusDetectorName, + RawDetector, + RawEvents, + Run, + SampleRotation, +) +from .types import ChopperFrequency, ChopperPhase -def chopper_tof_correction(data: RawEvents[Run]) -> ChopperCorrectedTofEvents[Run]: - """ - A correction for the presence of the chopper with respect to the "true" ToF. - Also fold the two pulses. - TODO: generalize mechanism to fold any number of pulses. +def load_detector( + file_path: FilePath[Run], detector_name: NeXusDetectorName[Run] +) -> RawDetector[Run]: + return nexus.load_detector(file_path=file_path, detector_name=detector_name) - Parameters - ---------- - data: - Input data array to correct. - Returns - ------- - : - ToF corrected data array. - """ +def load_events(detector: RawDetector[Run]) -> RawEvents[Run]: + # Recent versions of scippnexus no longer add variances for events by default, so + # we add them here if they are missing. + data = nexus.extract_detector_data(detector) + if data.bins.constituents['data'].data.variances is None: + data.bins.constituents['data'].data.variances = data.bins.constituents[ + 'data' + ].data.values + return RawEvents[Run](data) + + +def compute_tof( + events: RawEvents[Run], phase: ChopperPhase[Run], frequency: ChopperFrequency[Run] +) -> ChopperCorrectedTofEvents[Run]: + data = events.copy(deep=False) dim = 'tof' - tof_unit = data.bins.coords[dim].bins.unit - tau = sc.to_unit( - 1 / (2 * data.coords['source_chopper_2'].value['frequency'].data), - tof_unit, + data.bins.coords[dim] = data.bins.coords.pop('event_time_offset').to( + unit='us', dtype='float64', copy=False ) - chopper_phase = data.coords['source_chopper_2'].value['phase'].data - tof_offset = tau * chopper_phase / (180.0 * sc.units.deg) + tof_unit = data.bins.coords[dim].bins.unit + tau = sc.to_unit(1 / (2 * frequency), tof_unit) + tof_offset = tau * phase / (180.0 * sc.units.deg) # Make 2 bins, one for each pulse edges = sc.concat([-tof_offset, tau - tof_offset, 2 * tau - tof_offset], dim) data = data.bin({dim: sc.to_unit(edges, tof_unit)}) @@ -44,105 +53,23 @@ def chopper_tof_correction(data: RawEvents[Run]) -> ChopperCorrectedTofEvents[Ru data.bins.coords[dim] += offset # Rebin to exclude second (empty) pulse range data = data.bin({dim: sc.concat([0.0 * sc.units.us, tau], dim)}) - - # Ad-hoc correction described in - # https://scipp.github.io/ess/instruments/amor/amor_reduction.html - data.coords['position'].fields.y += data.coords['position'].fields.z * sc.tan( - 2.0 * data.coords['sample_rotation'] - (0.955 * sc.units.deg) - ) - return ChopperCorrectedTofEvents[Run](data) -def _assemble_event_data(dg: sc.DataGroup) -> sc.DataArray: - """Extract the events as a data array with all required coords. - - Parameters - ---------- - dg: - A data group with the structure of an Amor NeXus file. - - Returns - ------- - : - A data array with the events extracted from ``dg``. - """ - events = dg['instrument']['multiblade_detector']['data'].copy(deep=False) - events.bins.coords['tof'] = events.bins.coords.pop('event_time_offset') - events.coords['position'] = sc.spatial.as_vectors( +def detector_position( + events: RawEvents[Run], sample_rotation: SampleRotation[Run] +) -> DetectorPosition[Run]: + position = sc.spatial.as_vectors( events.coords.pop('x_pixel_offset'), events.coords.pop('y_pixel_offset'), events.coords.pop('z_pixel_offset'), ) - events.coords['sample_position'] = sc.vector([0, 0, 0], unit='m') - return events - - -def _load_nexus_entry(filepath: Union[str, Path]) -> sc.DataGroup: - """Load the single entry of a nexus file.""" - with snx.File(filepath, 'r') as f: - if len(f.keys()) != 1: - raise snx.NexusStructureError( - f"Expected a single entry in file {filepath}, got {len(f.keys())}" - ) - return f['entry'][()] - - -def load_raw_nexus(filepath: FilePath[Run]) -> RawData[Run]: - """Load unprocessed data and metadata from an Amor NeXus file. - - Parameters - ---------- - filepath: - File path of the NeXus file. - - Returns - ------- - : - Data and metadata. - """ - get_logger('amor').info( - "Loading '%s' as an Amor NeXus file", - filepath.filename if hasattr(filepath, 'filename') else filepath, - ) - return RawData(_load_nexus_entry(filepath)) - - -def extract_events( - raw_data: RawData[Run], beamline: BeamlineParams[Run] -) -> RawEvents[Run]: - """Extract the events from unprocessed NeXus data. - - Parameters - ---------- - raw_data: - Data in a form representing an Amor NeXus file. - beamline: - A dict defining the beamline parameters. - - Returns - ------- - : - Data array object for Amor dataset. - """ - data = _assemble_event_data(raw_data) - # Recent versions of scippnexus no longer add variances for events by default, so - # we add them here if they are missing. - if data.bins.constituents['data'].data.variances is None: - data.bins.constituents['data'].data.variances = data.bins.constituents[ - 'data' - ].data.values - - # Convert tof nanoseconds to microseconds for convenience - data.bins.coords['tof'] = data.bins.coords['tof'].to( - unit='us', dtype='float64', copy=False + # Ad-hoc correction described in + # https://scipp.github.io/ess/instruments/amor/amor_reduction.html + position.fields.y += position.fields.z * sc.tan( + 2.0 * sample_rotation - (0.955 * sc.units.deg) ) - - # Add beamline parameters - for key, value in beamline.items(): - data.coords[key] = value - - return RawEvents[Run](data) + return DetectorPosition[Run](position) -providers = (extract_events, load_raw_nexus, chopper_tof_correction) +providers = (load_detector, load_events, compute_tof, detector_position) diff --git a/src/essreflectometry/amor/resolution.py b/src/essreflectometry/amor/resolution.py index 02f6ca7..e7c5d59 100644 --- a/src/essreflectometry/amor/resolution.py +++ b/src/essreflectometry/amor/resolution.py @@ -3,44 +3,29 @@ import scipp as sc from ..tools import fwhm_to_std -from ..types import QBins, QData, QResolution, Sample -from .types import AngularResolution, SampleSizeResolution, WavelengthResolution - - -def wavelength_resolution(da: QData[Sample]) -> WavelengthResolution: - return WavelengthResolution( - _wavelength_resolution( - chopper_1_position=da.coords['source_chopper_1'].value['position'], - chopper_2_position=da.coords['source_chopper_2'].value['position'], - pixel_position=da.coords['position'], - ) - ) - - -def angular_resolution(da: QData[Sample]) -> AngularResolution: - return AngularResolution( - _angular_resolution( - pixel_position=da.coords['position'], - theta=da.bins.coords['theta'], - detector_spatial_resolution=da.coords['detector_spatial_resolution'], - ) - ) - - -def sample_size_resolution(da: QData[Sample]) -> SampleSizeResolution: - return SampleSizeResolution( - _sample_size_resolution( - pixel_position=da.coords['position'], - sample_size=da.coords['sample_size'], - ) - ) - - -def _wavelength_resolution( - chopper_1_position: sc.Variable, - chopper_2_position: sc.Variable, - pixel_position: sc.Variable, -) -> sc.Variable: +from ..types import ( + DetectorPosition, + DetectorSpatialResolution, + QBins, + QData, + QResolution, + Sample, + SampleSize, +) +from .types import ( + AngularResolution, + Chopper1Position, + Chopper2Position, + SampleSizeResolution, + WavelengthResolution, +) + + +def wavelength_resolution( + chopper_1_position: Chopper1Position[Sample], + chopper_2_position: Chopper2Position[Sample], + pixel_position: DetectorPosition[Sample], +) -> WavelengthResolution: """ Find the wavelength resolution contribution as described in Section 4.3.3 of the Amor publication (doi: 10.1016/j.nima.2016.03.007). @@ -60,18 +45,18 @@ def _wavelength_resolution( The angular resolution variable, as standard deviation. """ distance_between_choppers = ( - chopper_2_position.data.fields.z - chopper_1_position.data.fields.z - ) - chopper_midpoint = (chopper_1_position.data + chopper_2_position.data) * sc.scalar( - 0.5 + chopper_2_position.fields.z - chopper_1_position.fields.z ) + chopper_midpoint = (chopper_1_position + chopper_2_position) * sc.scalar(0.5) chopper_detector_distance = pixel_position.fields.z - chopper_midpoint.fields.z - return fwhm_to_std(distance_between_choppers / chopper_detector_distance) + return WavelengthResolution( + fwhm_to_std(distance_between_choppers / chopper_detector_distance) + ) -def _sample_size_resolution( - pixel_position: sc.Variable, sample_size: sc.Variable -) -> sc.Variable: +def sample_size_resolution( + pixel_position: DetectorPosition[Sample], sample_size: SampleSize[Sample] +) -> SampleSizeResolution: """ The resolution from the projected sample size, where it may be bigger than the detector pixel resolution as described in Section 4.3.3 of the Amor @@ -95,11 +80,11 @@ def _sample_size_resolution( ) -def _angular_resolution( - pixel_position: sc.Variable, - theta: sc.Variable, - detector_spatial_resolution: sc.Variable, -) -> sc.Variable: +def angular_resolution( + da: QData[Sample], + pixel_position: DetectorPosition[Sample], + detector_spatial_resolution: DetectorSpatialResolution[Sample], +) -> AngularResolution: """ Determine the angular resolution as described in Section 4.3.3 of the Amor publication (doi: 10.1016/j.nima.2016.03.007). @@ -118,6 +103,7 @@ def _angular_resolution( : Angular resolution standard deviation """ + theta = da.bins.coords['theta'] theta_unit = theta.bins.unit if theta.bins is not None else theta.unit return ( fwhm_to_std( diff --git a/src/essreflectometry/conversions.py b/src/essreflectometry/conversions.py index 2f36afb..ceb7f94 100644 --- a/src/essreflectometry/conversions.py +++ b/src/essreflectometry/conversions.py @@ -9,11 +9,16 @@ from .types import ( ChopperCorrectedTofEvents, + DetectorPosition, FootprintCorrectedData, + Gravity, HistogrammedQData, + IncidentBeam, QBins, QData, Run, + SamplePosition, + SampleRotation, SpecularReflectionCoordTransformGraph, ThetaData, WavelengthData, @@ -99,7 +104,13 @@ def reflectometry_q(wavelength: sc.Variable, theta: sc.Variable) -> sc.Variable: return c * sc.sin(theta.astype(dtype, copy=False)) / wavelength -def specular_reflection() -> SpecularReflectionCoordTransformGraph: +def specular_reflection( + incident_beam: IncidentBeam[Run], + sample_position: SamplePosition[Run], + position: DetectorPosition[Run], + sample_rotation: SampleRotation[Run], + gravity: Gravity, +) -> SpecularReflectionCoordTransformGraph[Run]: """ Generate a coordinate transformation graph for specular reflection reflectometry. @@ -113,13 +124,18 @@ def specular_reflection() -> SpecularReflectionCoordTransformGraph: **tof.elastic_wavelength("tof"), "theta": theta, "Q": reflectometry_q, + "incident_beam": lambda: incident_beam, + "sample_position": lambda: sample_position, + "position": lambda: position, + "sample_rotation": lambda: sample_rotation, + "gravity": lambda: gravity, } return SpecularReflectionCoordTransformGraph(graph) def tof_to_wavelength( data_array: ChopperCorrectedTofEvents[Run], - graph: SpecularReflectionCoordTransformGraph, + graph: SpecularReflectionCoordTransformGraph[Run], wavelength_edges: Optional[WavelengthEdges], ) -> WavelengthData[Run]: """ @@ -148,7 +164,7 @@ def tof_to_wavelength( def wavelength_to_theta( data_array: WavelengthData[Run], - graph: SpecularReflectionCoordTransformGraph, + graph: SpecularReflectionCoordTransformGraph[Run], ) -> ThetaData[Run]: """ Use :code:`transform_coords` to find the theta values for the events and @@ -185,7 +201,7 @@ def wavelength_to_theta( def theta_to_q( data_array: FootprintCorrectedData[Run], q_bins: QBins, - graph: SpecularReflectionCoordTransformGraph, + graph: SpecularReflectionCoordTransformGraph[Run], ) -> QData[Run]: """ Convert from theta to Q and if necessary bin in Q. @@ -231,4 +247,5 @@ def histogram(data_array: QData[Run]) -> HistogrammedQData[Run]: wavelength_to_theta, theta_to_q, histogram, + specular_reflection, ) diff --git a/src/essreflectometry/corrections.py b/src/essreflectometry/corrections.py index f1331cb..dbde827 100644 --- a/src/essreflectometry/corrections.py +++ b/src/essreflectometry/corrections.py @@ -6,17 +6,21 @@ from .supermirror import SupermirrorCalibrationFactor from .tools import fwhm_to_std from .types import ( + BeamSize, FootprintCorrectedData, HistogrammedQData, IofQ, Reference, Run, Sample, + SampleSize, ThetaData, ) -def footprint_correction(data_array: ThetaData[Run]) -> FootprintCorrectedData[Run]: +def footprint_correction( + data_array: ThetaData[Run], beam_size: BeamSize[Run], sample_size: SampleSize[Run] +) -> FootprintCorrectedData[Run]: """ Perform the footprint correction on the data array that has a :code:`beam_size` and binned :code:`theta` values. @@ -31,12 +35,8 @@ def footprint_correction(data_array: ThetaData[Run]) -> FootprintCorrectedData[R : Footprint corrected data array. """ - size_of_beam_on_sample = beam_on_sample( - data_array.coords['beam_size'], data_array.bins.coords['theta'] - ) - footprint_scale = sc.erf( - fwhm_to_std(data_array.coords['sample_size'] / size_of_beam_on_sample) - ) + size_of_beam_on_sample = beam_on_sample(beam_size, data_array.bins.coords['theta']) + footprint_scale = sc.erf(fwhm_to_std(sample_size / size_of_beam_on_sample)) data_array_fp_correction = data_array / footprint_scale.squeeze() return FootprintCorrectedData[Run](data_array_fp_correction) diff --git a/src/essreflectometry/load.py b/src/essreflectometry/load.py new file mode 100644 index 0000000..ebe8bc7 --- /dev/null +++ b/src/essreflectometry/load.py @@ -0,0 +1,7 @@ +import scippnexus as snx + + +def load(filepath, *paths: str): + with snx.File(filepath, 'r') as f: + for path in paths: + yield f[path][()] diff --git a/src/essreflectometry/orso.py b/src/essreflectometry/orso.py index 8111360..4ecff85 100644 --- a/src/essreflectometry/orso.py +++ b/src/essreflectometry/orso.py @@ -16,13 +16,13 @@ from orsopy.fileio import base as orso_base from orsopy.fileio import data_source, orso, reduction +from .load import load from .supermirror import SupermirrorCalibrationFactor from .types import ( ChopperCorrectedTofEvents, FilePath, FootprintCorrectedData, IofQ, - RawData, Reference, Sample, ) @@ -61,33 +61,42 @@ """ORSO sample.""" -def parse_orso_experiment(raw_data: RawData[Sample]) -> OrsoExperiment: +def parse_orso_experiment(filepath: FilePath[Sample]) -> OrsoExperiment: """Parse ORSO experiment metadata from raw NeXus data.""" + title, instrument_name, facility, start_time = load( + filepath, + 'entry/title', + 'entry/instrument/name', + 'entry/facility', + 'entry/start_time', + ) return OrsoExperiment( data_source.Experiment( - title=raw_data['title'], - instrument=raw_data['instrument']['name'], - facility=raw_data.get('facility'), - start_date=parse_datetime(raw_data['start_time']), + title=title, + instrument=instrument_name, + facility=facility, + start_date=parse_datetime(start_time), probe='neutron', ) ) -def parse_orso_owner(raw_data: RawData[Sample]) -> OrsoOwner: +def parse_orso_owner(filepath: FilePath[Sample]) -> OrsoOwner: """Parse ORSO owner metadata from raw NeXus data.""" + (user,) = load(filepath, 'entry/user') return OrsoOwner( orso_base.Person( - name=raw_data['user']['name'], - contact=raw_data['user']['email'], - affiliation=raw_data['user'].get('affiliation'), + name=user['name'], + contact=user['email'], + affiliation=user.get('affiliation'), ) ) -def parse_orso_sample(raw_data: RawData[Sample]) -> OrsoSample: +def parse_orso_sample(filepath: FilePath[Sample]) -> OrsoSample: """Parse ORSO sample metadata from raw NeXus data.""" - if not raw_data.get('sample'): + (sample,) = load(filepath, 'entry/sample') + if not sample: return OrsoSample(data_source.Sample.empty()) raise NotImplementedError('NeXus sample parsing is not implemented') diff --git a/src/essreflectometry/types.py b/src/essreflectometry/types.py index fd87b42..fe91c86 100644 --- a/src/essreflectometry/types.py +++ b/src/essreflectometry/types.py @@ -8,8 +8,24 @@ Run = TypeVar('Run', Reference, Sample) -class RawData(sciline.Scope[Run, sc.DataGroup], sc.DataGroup): - """Data as loaded from a NeXus file.""" +class NeXusDetectorName(sciline.Scope[Run, str], str): + """Name of the detector in the nexus file containing the events of the run""" + + +class DetectorPosition(sciline.Scope[Run, sc.Variable], sc.Variable): + """Positions of the detector pixels, relative to the source(?), as a 3d-vector""" + + +class SamplePosition(sciline.Scope[Run, sc.Variable], sc.Variable): + """The position of the sample relative to the source(?).""" + + +class IncidentBeam(sciline.Scope[Run, sc.Variable], sc.Variable): + """Incident beam vector.""" + + +class SpecularReflectionCoordTransformGraph(sciline.Scope[Run, dict], dict): + """Coordinate transformation graph for specular reflection""" class RawEvents(sciline.Scope[Run, sc.DataArray], sc.DataArray): @@ -17,6 +33,10 @@ class RawEvents(sciline.Scope[Run, sc.DataArray], sc.DataArray): binned by `detector_number` (pixel of the detector frame).""" +class RawDetector(sciline.Scope[Run, sc.DataGroup], sc.DataGroup): + """NXdetector loaded from file""" + + class ChopperCorrectedTofEvents(sciline.Scope[Run, sc.DataArray], sc.DataArray): """Event time data after correcting tof for choppers.""" @@ -44,11 +64,6 @@ class FootprintCorrectedData(sciline.Scope[Run, sc.DataArray], sc.DataArray): on the sample for the incidence angle of the event.""" -SpecularReflectionCoordTransformGraph = NewType( - 'SpecularReflectionCoordTransformGraph', dict -) - - class HistogrammedQData(sciline.Scope[Run, sc.DataArray], sc.DataArray): """Histogram of event weights by momentum transfer and detector_number."""