diff --git a/src/ess/reduce/nexus/__init__.py b/src/ess/reduce/nexus/__init__.py index 7785bec0..c310ca4a 100644 --- a/src/ess/reduce/nexus/__init__.py +++ b/src/ess/reduce/nexus/__init__.py @@ -20,6 +20,7 @@ load_all_components, load_component, load_data, + load_from_path, open_component_group, open_nexus_file, ) @@ -33,6 +34,7 @@ 'load_all_components', 'load_component', 'load_data', + 'load_from_path', 'open_component_group', 'open_nexus_file', 'types', diff --git a/src/ess/reduce/nexus/_nexus_loader.py b/src/ess/reduce/nexus/_nexus_loader.py index b4375f74..1887d69c 100644 --- a/src/ess/reduce/nexus/_nexus_loader.py +++ b/src/ess/reduce/nexus/_nexus_loader.py @@ -8,7 +8,7 @@ from contextlib import AbstractContextManager, contextmanager, nullcontext from dataclasses import dataclass from math import prod -from typing import TypeVar, cast +from typing import Any, TypeVar, cast import scipp as sc import scippnexus as snx @@ -42,6 +42,31 @@ def __repr__(self) -> str: NoLockingIfNeeded = NoLockingIfNeededType() +def load_from_path( + location: NeXusLocationSpec, + definitions: Mapping | NoNewDefinitionsType = NoNewDefinitions, +) -> Any: + """Load a field or group from a NeXus file given its location. + + Parameters + ---------- + location: + Location of the field within the NeXus file (filename, entry name, selection). + definitions: + Application definitions to use for the file. + + Returns + ------- + : + The loaded field (as a variable, data array, or raw python object) or group + (as a data group). + """ + with open_nexus_file(location.filename, definitions=definitions) as f: + entry = _unique_child_group(f, snx.NXentry, location.entry_name) + item = entry[location.component_name] + return item[location.selection] + + def load_component( location: NeXusLocationSpec, *, diff --git a/src/ess/reduce/nexus/workflow.py b/src/ess/reduce/nexus/workflow.py index 03a4ae75..81142865 100644 --- a/src/ess/reduce/nexus/workflow.py +++ b/src/ess/reduce/nexus/workflow.py @@ -691,16 +691,12 @@ def LoadMonitorWorkflow( def LoadDetectorWorkflow( - *, - run_types: Iterable[sciline.typing.Key], - monitor_types: Iterable[sciline.typing.Key], + *, run_types: Iterable[sciline.typing.Key] ) -> sciline.Pipeline: """Generic workflow for loading detector data from a NeXus file.""" wf = sciline.Pipeline( (*_common_providers, *_detector_providers), - constraints=_gather_constraints( - run_types=run_types, monitor_types=monitor_types - ), + constraints=_gather_constraints(run_types=run_types, monitor_types=[]), ) wf[DetectorBankSizes] = DetectorBankSizes({}) wf[PreopenNeXusFile] = PreopenNeXusFile(False) diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index 3b75dbe7..4e64396a 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -4,11 +4,12 @@ from pathlib import Path import pytest +import sciline as sl import scipp as sc import scippnexus as snx from scipp.testing import assert_identical -from ess.reduce.nexus import compute_component_position, workflow +from ess.reduce.nexus import compute_component_position, load_from_path, workflow from ess.reduce.nexus.types import ( BackgroundRun, Beamline, @@ -21,6 +22,8 @@ Measurement, MonitorType, NeXusComponentLocationSpec, + NeXusFileSpec, + NeXusLocationSpec, NeXusName, NeXusTransformation, PreopenNeXusFile, @@ -577,7 +580,7 @@ def test_load_histogram_monitor_workflow(dream_coda_test_file: Path) -> None: def test_load_detector_workflow(loki_tutorial_sample_run_60250: Path) -> None: - wf = LoadDetectorWorkflow(run_types=[SampleRun], monitor_types=[]) + wf = LoadDetectorWorkflow(run_types=[SampleRun]) wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250 wf[NeXusName[snx.NXdetector]] = 'larmor_detector' da = wf.compute(RawDetector[SampleRun]) @@ -587,7 +590,7 @@ def test_load_detector_workflow(loki_tutorial_sample_run_60250: Path) -> None: def test_load_histogram_detector_workflow(tbl_commissioning_orca_file: Path) -> None: - wf = LoadDetectorWorkflow(run_types=[SampleRun], monitor_types=[]) + wf = LoadDetectorWorkflow(run_types=[SampleRun]) wf[Filename[SampleRun]] = tbl_commissioning_orca_file wf[NeXusName[snx.NXdetector]] = 'orca_detector' da = wf.compute(RawDetector[SampleRun]) @@ -600,7 +603,7 @@ def test_load_histogram_detector_workflow(tbl_commissioning_orca_file: Path) -> def test_load_empty_histogram_detector_workflow( tbl_commissioning_orca_file: Path, ) -> None: - wf = LoadDetectorWorkflow(run_types=[SampleRun], monitor_types=[]) + wf = LoadDetectorWorkflow(run_types=[SampleRun]) wf[Filename[SampleRun]] = tbl_commissioning_orca_file wf[NeXusName[snx.NXdetector]] = 'orca_detector' da = wf.compute(EmptyDetector[SampleRun]) @@ -776,3 +779,48 @@ def assert_not_contains_type_arg(node: object, excluded: set[type]) -> None: assert not any( arg in excluded for arg in getattr(node, "__args__", ()) ), f"Node {node} contains one of {excluded!r}" + + +def test_generic_nexus_workflow_load_custom_field_user_affiliation( + loki_tutorial_sample_run_60250: Path, +) -> None: + class UserAffiliation(sl.Scope[RunType, str], str): + """User affiliation.""" + + def load_user_affiliation( + file: NeXusFileSpec[RunType], path: NeXusName[UserAffiliation[RunType]] + ) -> UserAffiliation[RunType]: + return UserAffiliation[RunType]( + load_from_path(NeXusLocationSpec(filename=file.value, component_name=path)) + ) + + wf = GenericNeXusWorkflow(run_types=[SampleRun], monitor_types=[]) + wf.insert(load_user_affiliation) + wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250 + # Path is relative to the top-level '/entry' + wf[NeXusName[UserAffiliation[SampleRun]]] = 'user_0/affiliation' + affiliation = wf.compute(UserAffiliation[SampleRun]) + assert affiliation == 'ESS' + + +def test_generic_nexus_workflow_load_custom_group_user( + loki_tutorial_sample_run_60250: Path, +) -> None: + class UserInfo(sl.Scope[RunType, str], str): + """User info.""" + + def load_user_info( + file: NeXusFileSpec[RunType], path: NeXusName[UserInfo[RunType]] + ) -> UserInfo[RunType]: + return UserInfo[RunType]( + load_from_path(NeXusLocationSpec(filename=file.value, component_name=path)) + ) + + wf = GenericNeXusWorkflow(run_types=[SampleRun], monitor_types=[]) + wf.insert(load_user_info) + wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250 + # Path is relative to the top-level '/entry' + wf[NeXusName[UserInfo]] = 'user_0' + user_info = wf.compute(UserInfo[SampleRun]) + assert user_info['affiliation'] == 'ESS' + assert user_info['name'] == 'John Doe'