diff --git a/pyproject.toml b/pyproject.toml index 7154e144..d919a178 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ requires-python = ">=3.10" dependencies = [ "sciline>=24.06.2", "scipp>=25.01.0", - "scippneutron>=24.11.0", + "scippneutron>=25.02.0", "scippnexus>=24.11.0", ] diff --git a/requirements/base.in b/requirements/base.in index aee29a13..8a284475 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -4,5 +4,5 @@ # The following was generated by 'tox -e deps', DO NOT EDIT MANUALLY! sciline>=24.06.2 scipp>=25.01.0 -scippneutron>=24.11.0 +scippneutron>=25.02.0 scippnexus>=24.11.0 diff --git a/requirements/base.txt b/requirements/base.txt index beeeab62..ee8f8510 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,24 +1,34 @@ -# SHA1:d18487328be0c30ec0a6929501d1c4a58c71bd48 +# SHA1:0aac37186f254abb39dbd8f3c3c6ad52bafc0553 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # +annotated-types==0.7.0 + # via pydantic contourpy==1.3.1 # via matplotlib cyclebane==24.10.0 # via sciline cycler==0.12.1 # via matplotlib +dnspython==2.7.0 + # via email-validator +email-validator==2.2.0 + # via scippneutron fonttools==4.56.0 # via matplotlib h5py==3.12.1 # via # scippneutron # scippnexus +idna==3.10 + # via email-validator kiwisolver==1.4.8 # via matplotlib +lazy-loader==0.4 + # via scippneutron matplotlib==3.10.0 # via # mpltoolbox @@ -27,7 +37,7 @@ mpltoolbox==24.5.1 # via scippneutron networkx==3.4.2 # via cyclebane -numpy==2.2.2 +numpy==2.2.3 # via # contourpy # h5py @@ -37,16 +47,23 @@ numpy==2.2.2 # scippneutron # scipy packaging==24.2 - # via matplotlib + # via + # lazy-loader + # matplotlib pillow==11.1.0 # via matplotlib plopp==24.10.0 # via scippneutron +pydantic==2.10.6 + # via scippneutron +pydantic-core==2.27.2 + # via pydantic pyparsing==3.2.1 # via matplotlib python-dateutil==2.9.0.post0 # via # matplotlib + # scippneutron # scippnexus sciline==24.10.0 # via -r base.in @@ -55,15 +72,19 @@ scipp==25.2.0 # -r base.in # scippneutron # scippnexus -scippneutron==25.1.0 +scippneutron==25.2.0 # via -r base.in scippnexus==24.11.1 # via # -r base.in # scippneutron -scipy==1.15.1 +scipy==1.15.2 # via # scippneutron # scippnexus six==1.17.0 # via python-dateutil +typing-extensions==4.12.2 + # via + # pydantic + # pydantic-core diff --git a/requirements/basetest.txt b/requirements/basetest.txt index 471ae175..48542e0a 100644 --- a/requirements/basetest.txt +++ b/requirements/basetest.txt @@ -19,10 +19,6 @@ cycler==0.12.1 # via matplotlib decorator==5.1.1 # via ipython -exceptiongroup==1.2.2 - # via - # ipython - # pytest executing==2.2.0 # via stack-data fonttools==4.56.0 @@ -47,7 +43,7 @@ matplotlib==3.10.0 # via plopp matplotlib-inline==0.1.7 # via ipython -numpy==2.2.2 +numpy==2.2.3 # via # contourpy # matplotlib @@ -90,7 +86,7 @@ requests==2.32.3 # via pooch scipp==25.2.0 # via tof -scipy==1.15.1 +scipy==1.15.2 # via # -r basetest.in # tof @@ -100,16 +96,12 @@ stack-data==0.6.3 # via ipython tof==25.2.0 # via -r basetest.in -tomli==2.2.1 - # via pytest traitlets==5.14.3 # via # comm # ipython # ipywidgets # matplotlib-inline -typing-extensions==4.12.2 - # via ipython urllib3==2.3.0 # via requests wcwidth==0.2.13 diff --git a/requirements/ci.txt b/requirements/ci.txt index 078cca56..3ff16713 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -44,14 +44,8 @@ requests==2.32.3 # via -r ci.in smmap==5.0.2 # via gitdb -tomli==2.2.1 - # via - # pyproject-api - # tox tox==4.24.1 # via -r ci.in -typing-extensions==4.12.2 - # via tox urllib3==2.3.0 # via requests virtualenv==20.29.2 diff --git a/requirements/dev.txt b/requirements/dev.txt index f5ef3159..c456d48f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -12,8 +12,6 @@ -r static.txt -r test.txt -r wheels.txt -annotated-types==0.7.0 - # via pydantic anyio==4.8.0 # via # httpx @@ -32,7 +30,7 @@ click==8.1.8 # via # pip-compile-multi # pip-tools -copier==9.4.1 +copier==9.5.0 # via -r dev.in dunamai==1.23.0 # via copier @@ -91,10 +89,6 @@ prometheus-client==0.21.1 # via jupyter-server pycparser==2.22 # via cffi -pydantic==2.10.6 - # via copier -pydantic-core==2.27.2 - # via pydantic python-json-logger==3.2.1 # via jupyter-events questionary==2.1.0 diff --git a/requirements/docs.txt b/requirements/docs.txt index f93446ad..423c0082 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -10,8 +10,6 @@ accessible-pygments==0.0.5 # via pydata-sphinx-theme alabaster==1.0.0 # via sphinx -appnope==0.1.4 - # via ipykernel asttokens==3.0.0 # via stack-data attrs==25.1.0 @@ -48,16 +46,12 @@ docutils==0.21.2 # nbsphinx # pydata-sphinx-theme # sphinx -exceptiongroup==1.2.2 - # via ipython executing==2.2.0 # via stack-data fastjsonschema==2.21.1 # via nbformat graphviz==0.20.3 # via -r docs.in -idna==3.10 - # via requests imagesize==1.4.1 # via sphinx importlib-resources==6.5.2 @@ -141,7 +135,7 @@ platformdirs==4.3.6 # via jupyter-core prompt-toolkit==3.0.50 # via ipython -psutil==6.1.1 +psutil==7.0.0 # via ipykernel ptyprocess==0.7.0 # via pexpect @@ -209,8 +203,6 @@ tinycss2==1.4.0 # via bleach tof==25.2.0 # via -r docs.in -tomli==2.2.1 - # via sphinx tornado==6.4.2 # via # ipykernel @@ -228,13 +220,6 @@ traitlets==5.14.3 # nbconvert # nbformat # nbsphinx -typing-extensions==4.12.2 - # via - # beautifulsoup4 - # ipython - # mistune - # pydata-sphinx-theme - # referencing urllib3==2.3.0 # via requests wcwidth==0.2.13 diff --git a/requirements/nightly.txt b/requirements/nightly.txt index f8a762d7..49ac1f87 100644 --- a/requirements/nightly.txt +++ b/requirements/nightly.txt @@ -32,10 +32,6 @@ dnspython==2.7.0 # via email-validator email-validator==2.2.0 # via scippneutron -exceptiongroup==1.2.2 - # via - # ipython - # pytest executing==2.2.0 # via stack-data fonttools==4.56.0 @@ -74,7 +70,7 @@ mpltoolbox==24.5.1 # via scippneutron networkx==3.4.2 # via cyclebane -numpy==2.2.2 +numpy==2.2.3 # via # contourpy # h5py @@ -142,7 +138,7 @@ scippnexus @ git+https://github.com/scipp/scippnexus@main # via # -r nightly.in # scippneutron -scipy==1.15.1 +scipy==1.15.2 # via # -r nightly.in # scippneutron @@ -154,8 +150,6 @@ stack-data==0.6.3 # via ipython tof @ git+https://github.com/scipp/tof@main # via -r nightly.in -tomli==2.2.1 - # via pytest traitlets==5.14.3 # via # comm @@ -164,7 +158,6 @@ traitlets==5.14.3 # matplotlib-inline typing-extensions==4.12.2 # via - # ipython # pydantic # pydantic-core urllib3==2.3.0 diff --git a/requirements/wheels.txt b/requirements/wheels.txt index bfae20bf..198dd23e 100644 --- a/requirements/wheels.txt +++ b/requirements/wheels.txt @@ -11,5 +11,3 @@ packaging==24.2 # via build pyproject-hooks==1.2.0 # via build -tomli==2.2.1 - # via build diff --git a/src/ess/reduce/nexus/_nexus_loader.py b/src/ess/reduce/nexus/_nexus_loader.py index fba29106..91026e48 100644 --- a/src/ess/reduce/nexus/_nexus_loader.py +++ b/src/ess/reduce/nexus/_nexus_loader.py @@ -8,14 +8,16 @@ from contextlib import AbstractContextManager, contextmanager, nullcontext from dataclasses import dataclass from math import prod -from typing import cast +from typing import TypeVar, cast import scipp as sc import scippnexus as snx from ..logging import get_logger from .types import ( + Beamline, FilePath, + Measurement, NeXusAllLocationSpec, NeXusEntryName, NeXusFile, @@ -27,6 +29,8 @@ class NoNewDefinitionsType: ... +_Model = TypeVar('_Model', Beamline, Measurement) + NoNewDefinitions = NoNewDefinitionsType() @@ -74,6 +78,18 @@ def load_all_components( return components +def load_metadata( + file_path: FilePath | NeXusFile | NeXusGroup, + model: type[_Model], + *, + entry_name: NeXusEntryName | None = None, + definitions: Mapping | NoNewDefinitionsType = NoNewDefinitions, +) -> _Model: + with _open_nexus_file(file_path, definitions=definitions) as f: + entry = _unique_child_group(f, snx.NXentry, entry_name) + return model.from_nexus_entry(entry) + + def compute_component_position(dg: sc.DataGroup) -> sc.DataGroup: # In some downstream packages we use some of the Nexus components which attempt # to compute positions without having actual Nexus data defining depends_on chains. diff --git a/src/ess/reduce/nexus/types.py b/src/ess/reduce/nexus/types.py index e6ae6a36..b7a22bf3 100644 --- a/src/ess/reduce/nexus/types.py +++ b/src/ess/reduce/nexus/types.py @@ -7,6 +7,7 @@ import sciline import scipp as sc import scippnexus as snx +from scippneutron import metadata as scn_meta FilePath = NewType('FilePath', Path) """Full path to a NeXus file on disk.""" @@ -177,6 +178,13 @@ class TransmissionRun(Generic[ScatteringRunType]): UniqueComponent = TypeVar('UniqueComponent', snx.NXsample, snx.NXsource) """Components that can be identified by their type as there will only be one.""" +Beamline = scn_meta.Beamline +"""Beamline metadata.""" +Measurement = scn_meta.Measurement +"""measurement metadata.""" +Source = scn_meta.Source +"""Neutron source metadata.""" + class NeXusName(sciline.Scope[Component, str], str): """Name of a component in a NeXus file.""" diff --git a/src/ess/reduce/nexus/workflow.py b/src/ess/reduce/nexus/workflow.py index a06e73ab..fd5a9eb9 100644 --- a/src/ess/reduce/nexus/workflow.py +++ b/src/ess/reduce/nexus/workflow.py @@ -19,6 +19,7 @@ from .types import ( AllNeXusComponents, Analyzers, + Beamline, CalibratedBeamline, CalibratedDetector, CalibratedMonitor, @@ -29,6 +30,7 @@ DetectorPositionOffset, Filename, GravityVector, + Measurement, MonitorData, MonitorPositionOffset, MonitorType, @@ -45,6 +47,7 @@ Position, PreopenNeXusFile, RunType, + SampleRun, TimeInterval, UniqueComponent, ) @@ -586,6 +589,18 @@ def _add_variances(da: sc.DataArray) -> sc.DataArray: return out +def load_beamline_metadata_from_nexus(file_spec: NeXusFileSpec[SampleRun]) -> Beamline: + """Load beamline metadata from a sample NeXus file.""" + return nexus.load_metadata(file_spec.value, Beamline) + + +def load_measurement_metadata_from_nexus( + file_spec: NeXusFileSpec[SampleRun], +) -> Measurement: + """Load measurement metadata from a sample NeXus file.""" + return nexus.load_metadata(file_spec.value, Measurement) + + definitions = snx.base_definitions() definitions["NXdetector"] = _StrippedDetector definitions["NXmonitor"] = _StrippedMonitor @@ -631,6 +646,11 @@ def _add_variances(da: sc.DataArray) -> sc.DataArray: _analyzer_providers = (parse_analyzers,) +_metadata_providers = ( + load_beamline_metadata_from_nexus, + load_measurement_metadata_from_nexus, +) + def LoadMonitorWorkflow() -> sciline.Pipeline: """Generic workflow for loading monitor data from a NeXus file.""" @@ -689,6 +709,7 @@ def GenericNeXusWorkflow( *_detector_providers, *_chopper_providers, *_analyzer_providers, + *_metadata_providers, ) ) wf[DetectorBankSizes] = DetectorBankSizes({}) diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index f5e4f8c1..949702e2 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2024 Scipp contributors (https://github.com/scipp) +from datetime import datetime, timezone + import pytest import scipp as sc import scippnexus as snx @@ -10,10 +12,12 @@ from ess.reduce.nexus.types import ( Analyzers, BackgroundRun, + Beamline, Choppers, DetectorData, EmptyBeamRun, Filename, + Measurement, Monitor1, Monitor2, Monitor3, @@ -578,6 +582,32 @@ def test_generic_nexus_workflow_load_analyzers() -> None: assert analyzer['usage'] == 'Bragg' +def test_generic_nexus_workflow_load_beamline_metadata() -> None: + wf = GenericNeXusWorkflow() + wf[Filename[SampleRun]] = data.bifrost_simulated_elastic() + beamline = wf.compute(Beamline) + + assert beamline.name == 'BIFROST' + assert beamline.facility == 'ESS' + assert beamline.site == 'ESS' + + +def test_generic_nexus_workflow_load_measurement_metadata() -> None: + wf = GenericNeXusWorkflow() + wf[Filename[SampleRun]] = data.loki_tutorial_sample_run_60250() + wf[Filename[BackgroundRun]] = data.loki_tutorial_background_run_60248() + measurement = wf.compute(Measurement) + + assert measurement.title == 'My experiment' + assert measurement.experiment_id == 'p1234' + assert measurement.start_time == datetime( + 2022, 2, 28, 21, 15, 0, tzinfo=timezone.utc + ) + assert measurement.end_time == datetime(2032, 2, 29, 9, 15, 0, tzinfo=timezone.utc) + assert measurement.run_number is None + assert measurement.experiment_doi is None + + def test_generic_nexus_workflow_includes_only_given_run_and_monitor_types() -> None: wf = GenericNeXusWorkflow(run_types=[SampleRun], monitor_types=[Monitor1, Monitor3]) graph = wf.underlying_graph