From 681179c7efe1e7fdb8c6a3f3148d24967fdd2e2d Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 11 Nov 2025 11:07:44 +0100 Subject: [PATCH 01/12] add component_types to contraints to allow loading custom entries in the nexus file --- src/ess/reduce/nexus/workflow.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/ess/reduce/nexus/workflow.py b/src/ess/reduce/nexus/workflow.py index 03a4ae75..2a9dd927 100644 --- a/src/ess/reduce/nexus/workflow.py +++ b/src/ess/reduce/nexus/workflow.py @@ -683,7 +683,7 @@ def LoadMonitorWorkflow( wf = sciline.Pipeline( (*_common_providers, *_monitor_providers), constraints=_gather_constraints( - run_types=run_types, monitor_types=monitor_types + run_types=run_types, monitor_types=monitor_types, component_types=[] ), ) wf[PreopenNeXusFile] = PreopenNeXusFile(False) @@ -691,15 +691,13 @@ 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 + run_types=run_types, monitor_types=[], component_types=[] ), ) wf[DetectorBankSizes] = DetectorBankSizes({}) @@ -711,6 +709,7 @@ def GenericNeXusWorkflow( *, run_types: Iterable[sciline.typing.Key], monitor_types: Iterable[sciline.typing.Key], + component_types: Iterable[sciline.typing.Key] | None = None, ) -> sciline.Pipeline: """ Generic workflow for loading detector and monitor data from a NeXus file. @@ -735,6 +734,9 @@ def GenericNeXusWorkflow( List of monitor types to include in the workflow. Constrains the possible values of :class:`ess.reduce.nexus.types.MonitorType` and :class:`ess.reduce.nexus.types.Component`. + component_types: + Additional component types to include in the workflow. + Constrains the possible values of :class:`ess.reduce.nexus.types.Component`. Returns ------- @@ -750,7 +752,9 @@ def GenericNeXusWorkflow( *_metadata_providers, ), constraints=_gather_constraints( - run_types=run_types, monitor_types=monitor_types + run_types=run_types, + monitor_types=monitor_types, + component_types=[] if component_types is None else component_types, ), ) wf[DetectorBankSizes] = DetectorBankSizes({}) @@ -763,11 +767,12 @@ def _gather_constraints( *, run_types: Iterable[sciline.typing.Key], monitor_types: Iterable[sciline.typing.Key], + component_types: Iterable[sciline.typing.Key], ) -> dict[TypeVar, Iterable[type]]: mon = tuple(iter(monitor_types)) constraints = { RunType: run_types, MonitorType: mon, - Component: (*COMPONENT_CONSTRAINTS, *mon), + Component: (*COMPONENT_CONSTRAINTS, *mon, *component_types), } return constraints From 50ad6a03d5dfb3963891d16dc4761ed7f0f74f66 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 11 Nov 2025 11:33:08 +0100 Subject: [PATCH 02/12] add tests --- tests/nexus/workflow_test.py | 59 +++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index 3b75dbe7..fb1ed8a0 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_component, workflow from ess.reduce.nexus.types import ( BackgroundRun, Beamline, @@ -773,6 +774,56 @@ def test_generic_nexus_workflow_includes_only_given_monitor_types() -> None: 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}" + 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_component_user_affiliation( + loki_tutorial_sample_run_60250: Path, +) -> None: + # Load a component in one of the top-level entries + + class UserAffiliation(sl.Scope[RunType, str], str): + """User affiliation.""" + + def load_user_affiliation( + location: NeXusComponentLocationSpec[UserAffiliation, RunType], + ) -> UserAffiliation[RunType]: + return UserAffiliation[RunType]( + load_component(location, nx_class=snx.NXuser)['affiliation'] + ) + + wf = GenericNeXusWorkflow( + run_types=[SampleRun], monitor_types=[], component_types=[UserAffiliation] + ) + wf.insert(load_user_affiliation) + wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250 + wf[NeXusName[UserAffiliation]] = '/entry/user_0' + affiliation = wf.compute(UserAffiliation[SampleRun]) + assert affiliation == 'ESS' + + +def test_generic_nexus_workflow_load_custom_component_source_name( + loki_tutorial_sample_run_60250: Path, +) -> None: + # Load a component inside the instrument entry + + class SourceName(sl.Scope[RunType, str], str): + """Source name.""" + + def load_source_name( + location: NeXusComponentLocationSpec[SourceName, RunType], + ) -> SourceName[RunType]: + return SourceName[RunType]( + load_component(location, nx_class=snx.NXsource)['name'] + ) + + wf = GenericNeXusWorkflow( + run_types=[SampleRun], monitor_types=[], component_types=[SourceName] + ) + wf.insert(load_source_name) + wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250 + wf[NeXusName[SourceName]] = '/entry/instrument/source' + source_name = wf.compute(SourceName[SampleRun]) + assert source_name == 'moderator' From 43df02f6cfb175935bfe4e64e62c01a1ff7b0fa7 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 11 Nov 2025 11:33:36 +0100 Subject: [PATCH 03/12] formatting --- tests/nexus/workflow_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index fb1ed8a0..0d703f61 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -774,9 +774,9 @@ def test_generic_nexus_workflow_includes_only_given_monitor_types() -> None: 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}" - ) + 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_component_user_affiliation( From ca661a1aee64d0f29594fb1ce2f3427d58827a21 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Tue, 11 Nov 2025 11:40:38 +0100 Subject: [PATCH 04/12] fix LoadDetectorWorkflow tests --- tests/nexus/workflow_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index 0d703f61..627c44b9 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -578,7 +578,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]) @@ -588,7 +588,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]) @@ -601,7 +601,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]) From 8fbb2336ecaec9d6b6ef510904b5ed2e8ec550f5 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Fri, 14 Nov 2025 13:01:46 +0100 Subject: [PATCH 05/12] trying without component_types and adding a load_field --- src/ess/reduce/nexus/__init__.py | 2 ++ src/ess/reduce/nexus/_nexus_loader.py | 47 +++++++++++++++++++++++++++ tests/nexus/workflow_test.py | 26 +++++++++------ 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/ess/reduce/nexus/__init__.py b/src/ess/reduce/nexus/__init__.py index 7785bec0..1d8c634a 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_field, open_component_group, open_nexus_file, ) @@ -33,6 +34,7 @@ 'load_all_components', 'load_component', 'load_data', + 'load_field', '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..39fbb8f6 100644 --- a/src/ess/reduce/nexus/_nexus_loader.py +++ b/src/ess/reduce/nexus/_nexus_loader.py @@ -21,6 +21,7 @@ NeXusAllLocationSpec, NeXusEntryName, NeXusFile, + NeXusFileSpec, NeXusGroup, NeXusLocationSpec, ) @@ -42,6 +43,52 @@ def __repr__(self) -> str: NoLockingIfNeeded = NoLockingIfNeededType() +def load_field( + filename: NeXusFileSpec, + field_path: str, +) -> sc.Variable: + """Load a single field from a NeXus file. + + Parameters + ---------- + filename: + Path of the file to load from. + field_path: + Path of the field within the NeXus file. + + Returns + ------- + : + The loaded field as a variable. + """ + with open_nexus_file(filename.value) as f: + field = f[field_path] + return cast(sc.Variable, field) + + +def load_group( + filename: NeXusFileSpec, + group_path: str, +) -> sc.DataGroup: + """Load a single group from a NeXus file. + + Parameters + ---------- + filename: + Path of the file to load from. + group_path: + Path of the group within the NeXus file. + + Returns + ------- + : + The loaded group as a data group. + """ + with open_nexus_file(filename.value) as f: + group = f[group_path] + return cast(sc.DataGroup, group) + + def load_component( location: NeXusLocationSpec, *, diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index 627c44b9..b63a2db5 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -9,7 +9,12 @@ import scippnexus as snx from scipp.testing import assert_identical -from ess.reduce.nexus import compute_component_position, load_component, workflow +from ess.reduce.nexus import ( + compute_component_position, + load_component, + load_field, + workflow, +) from ess.reduce.nexus.types import ( BackgroundRun, Beamline, @@ -22,6 +27,7 @@ Measurement, MonitorType, NeXusComponentLocationSpec, + NeXusFileSpec, NeXusName, NeXusTransformation, PreopenNeXusFile, @@ -774,9 +780,9 @@ def test_generic_nexus_workflow_includes_only_given_monitor_types() -> None: 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}" + 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_component_user_affiliation( @@ -788,18 +794,18 @@ class UserAffiliation(sl.Scope[RunType, str], str): """User affiliation.""" def load_user_affiliation( - location: NeXusComponentLocationSpec[UserAffiliation, RunType], + file: NeXusFileSpec[RunType], + path: NeXusName[UserAffiliation[RunType]], ) -> UserAffiliation[RunType]: - return UserAffiliation[RunType]( - load_component(location, nx_class=snx.NXuser)['affiliation'] - ) + return UserAffiliation[RunType](load_field(file, path)) wf = GenericNeXusWorkflow( - run_types=[SampleRun], monitor_types=[], component_types=[UserAffiliation] + run_types=[SampleRun], + monitor_types=[], # component_types=[UserAffiliation] ) wf.insert(load_user_affiliation) wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250 - wf[NeXusName[UserAffiliation]] = '/entry/user_0' + wf[NeXusName[UserAffiliation[SampleRun]]] = '/entry/user_0/affiliation' affiliation = wf.compute(UserAffiliation[SampleRun]) assert affiliation == 'ESS' From 49b4b4c08b0485784201bddf605432c4a5c6e326 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:02:34 +0000 Subject: [PATCH 06/12] Apply automatic formatting --- tests/nexus/workflow_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index b63a2db5..a4c066f3 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -780,9 +780,9 @@ def test_generic_nexus_workflow_includes_only_given_monitor_types() -> None: 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}" - ) + 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_component_user_affiliation( From e18cd6bc2b3430fc2dafd269aee4b5e8393be3be Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Fri, 14 Nov 2025 15:42:24 +0100 Subject: [PATCH 07/12] remove component_types in favor of providers that use load_field and load_group --- src/ess/reduce/nexus/__init__.py | 2 ++ src/ess/reduce/nexus/_nexus_loader.py | 14 +++++--- src/ess/reduce/nexus/workflow.py | 17 +++------- tests/nexus/workflow_test.py | 49 +++++++++++---------------- 4 files changed, 36 insertions(+), 46 deletions(-) diff --git a/src/ess/reduce/nexus/__init__.py b/src/ess/reduce/nexus/__init__.py index 1d8c634a..22eefad2 100644 --- a/src/ess/reduce/nexus/__init__.py +++ b/src/ess/reduce/nexus/__init__.py @@ -21,6 +21,7 @@ load_component, load_data, load_field, + load_group, open_component_group, open_nexus_file, ) @@ -35,6 +36,7 @@ 'load_component', 'load_data', 'load_field', + 'load_group', '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 39fbb8f6..596d9ee8 100644 --- a/src/ess/reduce/nexus/_nexus_loader.py +++ b/src/ess/reduce/nexus/_nexus_loader.py @@ -46,7 +46,8 @@ def __repr__(self) -> str: def load_field( filename: NeXusFileSpec, field_path: str, -) -> sc.Variable: + selection: snx.typing.ScippIndex | slice = (), +) -> sc.Variable | sc.DataArray: """Load a single field from a NeXus file. Parameters @@ -55,20 +56,23 @@ def load_field( Path of the file to load from. field_path: Path of the field within the NeXus file. + selection: + Selection to apply to the field. Returns ------- : - The loaded field as a variable. + The loaded field as a variable or data array. """ with open_nexus_file(filename.value) as f: field = f[field_path] - return cast(sc.Variable, field) + return cast(sc.Variable | sc.DataArray, field[selection]) def load_group( filename: NeXusFileSpec, group_path: str, + selection: snx.typing.ScippIndex | slice = (), ) -> sc.DataGroup: """Load a single group from a NeXus file. @@ -78,6 +82,8 @@ def load_group( Path of the file to load from. group_path: Path of the group within the NeXus file. + selection: + Selection to apply to the group. Returns ------- @@ -86,7 +92,7 @@ def load_group( """ with open_nexus_file(filename.value) as f: group = f[group_path] - return cast(sc.DataGroup, group) + return cast(sc.DataGroup, group[selection]) def load_component( diff --git a/src/ess/reduce/nexus/workflow.py b/src/ess/reduce/nexus/workflow.py index 2a9dd927..81142865 100644 --- a/src/ess/reduce/nexus/workflow.py +++ b/src/ess/reduce/nexus/workflow.py @@ -683,7 +683,7 @@ def LoadMonitorWorkflow( wf = sciline.Pipeline( (*_common_providers, *_monitor_providers), constraints=_gather_constraints( - run_types=run_types, monitor_types=monitor_types, component_types=[] + run_types=run_types, monitor_types=monitor_types ), ) wf[PreopenNeXusFile] = PreopenNeXusFile(False) @@ -696,9 +696,7 @@ def LoadDetectorWorkflow( """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=[], component_types=[] - ), + constraints=_gather_constraints(run_types=run_types, monitor_types=[]), ) wf[DetectorBankSizes] = DetectorBankSizes({}) wf[PreopenNeXusFile] = PreopenNeXusFile(False) @@ -709,7 +707,6 @@ def GenericNeXusWorkflow( *, run_types: Iterable[sciline.typing.Key], monitor_types: Iterable[sciline.typing.Key], - component_types: Iterable[sciline.typing.Key] | None = None, ) -> sciline.Pipeline: """ Generic workflow for loading detector and monitor data from a NeXus file. @@ -734,9 +731,6 @@ def GenericNeXusWorkflow( List of monitor types to include in the workflow. Constrains the possible values of :class:`ess.reduce.nexus.types.MonitorType` and :class:`ess.reduce.nexus.types.Component`. - component_types: - Additional component types to include in the workflow. - Constrains the possible values of :class:`ess.reduce.nexus.types.Component`. Returns ------- @@ -752,9 +746,7 @@ def GenericNeXusWorkflow( *_metadata_providers, ), constraints=_gather_constraints( - run_types=run_types, - monitor_types=monitor_types, - component_types=[] if component_types is None else component_types, + run_types=run_types, monitor_types=monitor_types ), ) wf[DetectorBankSizes] = DetectorBankSizes({}) @@ -767,12 +759,11 @@ def _gather_constraints( *, run_types: Iterable[sciline.typing.Key], monitor_types: Iterable[sciline.typing.Key], - component_types: Iterable[sciline.typing.Key], ) -> dict[TypeVar, Iterable[type]]: mon = tuple(iter(monitor_types)) constraints = { RunType: run_types, MonitorType: mon, - Component: (*COMPONENT_CONSTRAINTS, *mon, *component_types), + Component: (*COMPONENT_CONSTRAINTS, *mon), } return constraints diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index a4c066f3..c3e3d7a8 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -11,8 +11,8 @@ from ess.reduce.nexus import ( compute_component_position, - load_component, load_field, + load_group, workflow, ) from ess.reduce.nexus.types import ( @@ -780,16 +780,14 @@ def test_generic_nexus_workflow_includes_only_given_monitor_types() -> None: 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}" + 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_component_user_affiliation( +def test_generic_nexus_workflow_load_custom_field_user_affiliation( loki_tutorial_sample_run_60250: Path, ) -> None: - # Load a component in one of the top-level entries - class UserAffiliation(sl.Scope[RunType, str], str): """User affiliation.""" @@ -799,10 +797,7 @@ def load_user_affiliation( ) -> UserAffiliation[RunType]: return UserAffiliation[RunType](load_field(file, path)) - wf = GenericNeXusWorkflow( - run_types=[SampleRun], - monitor_types=[], # component_types=[UserAffiliation] - ) + wf = GenericNeXusWorkflow(run_types=[SampleRun], monitor_types=[]) wf.insert(load_user_affiliation) wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250 wf[NeXusName[UserAffiliation[SampleRun]]] = '/entry/user_0/affiliation' @@ -810,26 +805,22 @@ def load_user_affiliation( assert affiliation == 'ESS' -def test_generic_nexus_workflow_load_custom_component_source_name( +def test_generic_nexus_workflow_load_custom_group_user( loki_tutorial_sample_run_60250: Path, ) -> None: - # Load a component inside the instrument entry - - class SourceName(sl.Scope[RunType, str], str): - """Source name.""" + class UserInfo(sl.Scope[RunType, str], str): + """User info.""" - def load_source_name( - location: NeXusComponentLocationSpec[SourceName, RunType], - ) -> SourceName[RunType]: - return SourceName[RunType]( - load_component(location, nx_class=snx.NXsource)['name'] - ) + def load_user_info( + file: NeXusFileSpec[RunType], + path: NeXusName[UserInfo[RunType]], + ) -> UserInfo[RunType]: + return UserInfo[RunType](load_group(file, path)) - wf = GenericNeXusWorkflow( - run_types=[SampleRun], monitor_types=[], component_types=[SourceName] - ) - wf.insert(load_source_name) + wf = GenericNeXusWorkflow(run_types=[SampleRun], monitor_types=[]) + wf.insert(load_user_info) wf[Filename[SampleRun]] = loki_tutorial_sample_run_60250 - wf[NeXusName[SourceName]] = '/entry/instrument/source' - source_name = wf.compute(SourceName[SampleRun]) - assert source_name == 'moderator' + wf[NeXusName[UserInfo]] = '/entry/user_0' + user_info = wf.compute(UserInfo[SampleRun]) + assert user_info['affiliation'] == 'ESS' + assert user_info['name'] == 'John Doe' From c43c1b04a5e3d26d4f8bceb469d3479797766867 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:43:24 +0000 Subject: [PATCH 08/12] Apply automatic formatting --- tests/nexus/workflow_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index c3e3d7a8..7b52568d 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -780,9 +780,9 @@ def test_generic_nexus_workflow_includes_only_given_monitor_types() -> None: 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}" - ) + 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( From 0f3fd11acde4ff0b752c492676ae0f21b7b993a3 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 17 Nov 2025 11:58:04 +0100 Subject: [PATCH 09/12] remove cast for load_field --- src/ess/reduce/nexus/_nexus_loader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ess/reduce/nexus/_nexus_loader.py b/src/ess/reduce/nexus/_nexus_loader.py index 596d9ee8..125acd34 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 @@ -47,7 +47,7 @@ def load_field( filename: NeXusFileSpec, field_path: str, selection: snx.typing.ScippIndex | slice = (), -) -> sc.Variable | sc.DataArray: +) -> Any: """Load a single field from a NeXus file. Parameters @@ -66,7 +66,7 @@ def load_field( """ with open_nexus_file(filename.value) as f: field = f[field_path] - return cast(sc.Variable | sc.DataArray, field[selection]) + return field[selection] def load_group( From c6e1bf8d4ece7476514f101e91c259a62bc8026a Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Mon, 17 Nov 2025 21:47:01 +0100 Subject: [PATCH 10/12] use NeXusLocationSpec --- src/ess/reduce/nexus/_nexus_loader.py | 43 +++++++++++---------------- tests/nexus/workflow_test.py | 9 ++++-- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/ess/reduce/nexus/_nexus_loader.py b/src/ess/reduce/nexus/_nexus_loader.py index 125acd34..98d953a4 100644 --- a/src/ess/reduce/nexus/_nexus_loader.py +++ b/src/ess/reduce/nexus/_nexus_loader.py @@ -21,7 +21,6 @@ NeXusAllLocationSpec, NeXusEntryName, NeXusFile, - NeXusFileSpec, NeXusGroup, NeXusLocationSpec, ) @@ -44,55 +43,47 @@ def __repr__(self) -> str: def load_field( - filename: NeXusFileSpec, - field_path: str, - selection: snx.typing.ScippIndex | slice = (), + location: NeXusLocationSpec, + definitions: Mapping | NoNewDefinitionsType = NoNewDefinitions, ) -> Any: """Load a single field from a NeXus file. Parameters ---------- - filename: - Path of the file to load from. - field_path: - Path of the field within the NeXus file. - selection: - Selection to apply to the field. + 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 or data array. + The loaded field (as a variable, data array, or raw python object). """ - with open_nexus_file(filename.value) as f: - field = f[field_path] - return field[selection] + with open_nexus_file(location.filename, definitions=definitions) as f: + field = f[location.entry_name] + return field[location.selection] def load_group( - filename: NeXusFileSpec, - group_path: str, - selection: snx.typing.ScippIndex | slice = (), + location: NeXusLocationSpec, + definitions: Mapping | NoNewDefinitionsType = NoNewDefinitions, ) -> sc.DataGroup: """Load a single group from a NeXus file. Parameters ---------- - filename: - Path of the file to load from. - group_path: - Path of the group within the NeXus file. - selection: - Selection to apply to the group. + location: + Location of the group within the NeXus file (filename, entry name, selection). Returns ------- : The loaded group as a data group. """ - with open_nexus_file(filename.value) as f: - group = f[group_path] - return cast(sc.DataGroup, group[selection]) + with open_nexus_file(location.filename, definitions=definitions) as f: + group = f[location.entry_name] + return cast(sc.DataGroup, group[location.selection]) def load_component( diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index 7b52568d..3f40df9a 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -28,6 +28,7 @@ MonitorType, NeXusComponentLocationSpec, NeXusFileSpec, + NeXusLocationSpec, NeXusName, NeXusTransformation, PreopenNeXusFile, @@ -795,7 +796,9 @@ def load_user_affiliation( file: NeXusFileSpec[RunType], path: NeXusName[UserAffiliation[RunType]], ) -> UserAffiliation[RunType]: - return UserAffiliation[RunType](load_field(file, path)) + return UserAffiliation[RunType]( + load_field(NeXusLocationSpec(filename=file.value, entry_name=path)) + ) wf = GenericNeXusWorkflow(run_types=[SampleRun], monitor_types=[]) wf.insert(load_user_affiliation) @@ -815,7 +818,9 @@ def load_user_info( file: NeXusFileSpec[RunType], path: NeXusName[UserInfo[RunType]], ) -> UserInfo[RunType]: - return UserInfo[RunType](load_group(file, path)) + return UserInfo[RunType]( + load_group(NeXusLocationSpec(filename=file.value, entry_name=path)) + ) wf = GenericNeXusWorkflow(run_types=[SampleRun], monitor_types=[]) wf.insert(load_user_info) From f19dd47563530fc058c04095ee565b566a0bb084 Mon Sep 17 00:00:00 2001 From: Neil Vaytet Date: Wed, 19 Nov 2025 11:19:43 +0100 Subject: [PATCH 11/12] use _unique_child_group and merge load_field and load_group into a single load_from_path function --- src/ess/reduce/nexus/__init__.py | 6 ++--- src/ess/reduce/nexus/_nexus_loader.py | 33 ++++++--------------------- tests/nexus/workflow_test.py | 29 ++++++++++------------- 3 files changed, 21 insertions(+), 47 deletions(-) diff --git a/src/ess/reduce/nexus/__init__.py b/src/ess/reduce/nexus/__init__.py index 22eefad2..c310ca4a 100644 --- a/src/ess/reduce/nexus/__init__.py +++ b/src/ess/reduce/nexus/__init__.py @@ -20,8 +20,7 @@ load_all_components, load_component, load_data, - load_field, - load_group, + load_from_path, open_component_group, open_nexus_file, ) @@ -35,8 +34,7 @@ 'load_all_components', 'load_component', 'load_data', - 'load_field', - 'load_group', + '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 98d953a4..1887d69c 100644 --- a/src/ess/reduce/nexus/_nexus_loader.py +++ b/src/ess/reduce/nexus/_nexus_loader.py @@ -42,11 +42,11 @@ def __repr__(self) -> str: NoLockingIfNeeded = NoLockingIfNeededType() -def load_field( +def load_from_path( location: NeXusLocationSpec, definitions: Mapping | NoNewDefinitionsType = NoNewDefinitions, ) -> Any: - """Load a single field from a NeXus file. + """Load a field or group from a NeXus file given its location. Parameters ---------- @@ -58,32 +58,13 @@ def load_field( Returns ------- : - The loaded field (as a variable, data array, or raw python object). + 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: - field = f[location.entry_name] - return field[location.selection] - - -def load_group( - location: NeXusLocationSpec, - definitions: Mapping | NoNewDefinitionsType = NoNewDefinitions, -) -> sc.DataGroup: - """Load a single group from a NeXus file. - - Parameters - ---------- - location: - Location of the group within the NeXus file (filename, entry name, selection). - - Returns - ------- - : - The loaded group as a data group. - """ - with open_nexus_file(location.filename, definitions=definitions) as f: - group = f[location.entry_name] - return cast(sc.DataGroup, group[location.selection]) + entry = _unique_child_group(f, snx.NXentry, location.entry_name) + item = entry[location.component_name] + return item[location.selection] def load_component( diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index 3f40df9a..42436f34 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -9,12 +9,7 @@ import scippnexus as snx from scipp.testing import assert_identical -from ess.reduce.nexus import ( - compute_component_position, - load_field, - load_group, - workflow, -) +from ess.reduce.nexus import compute_component_position, load_from_path, workflow from ess.reduce.nexus.types import ( BackgroundRun, Beamline, @@ -781,9 +776,9 @@ def test_generic_nexus_workflow_includes_only_given_monitor_types() -> None: 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}" + 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( @@ -793,17 +788,17 @@ class UserAffiliation(sl.Scope[RunType, str], str): """User affiliation.""" def load_user_affiliation( - file: NeXusFileSpec[RunType], - path: NeXusName[UserAffiliation[RunType]], + file: NeXusFileSpec[RunType], path: NeXusName[UserAffiliation[RunType]] ) -> UserAffiliation[RunType]: return UserAffiliation[RunType]( - load_field(NeXusLocationSpec(filename=file.value, entry_name=path)) + 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 - wf[NeXusName[UserAffiliation[SampleRun]]] = '/entry/user_0/affiliation' + # Path is relative to the top-level '/entry' + wf[NeXusName[UserAffiliation[SampleRun]]] = 'user_0/affiliation' affiliation = wf.compute(UserAffiliation[SampleRun]) assert affiliation == 'ESS' @@ -815,17 +810,17 @@ class UserInfo(sl.Scope[RunType, str], str): """User info.""" def load_user_info( - file: NeXusFileSpec[RunType], - path: NeXusName[UserInfo[RunType]], + file: NeXusFileSpec[RunType], path: NeXusName[UserInfo[RunType]] ) -> UserInfo[RunType]: return UserInfo[RunType]( - load_group(NeXusLocationSpec(filename=file.value, entry_name=path)) + 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 - wf[NeXusName[UserInfo]] = '/entry/user_0' + # 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' From f4b1611714a339d353c80ae8c8fb1e1d868d8a7c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:20:15 +0000 Subject: [PATCH 12/12] Apply automatic formatting --- tests/nexus/workflow_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index 42436f34..4e64396a 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -776,9 +776,9 @@ def test_generic_nexus_workflow_includes_only_given_monitor_types() -> None: 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}" - ) + 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(