diff --git a/src/ess/reduce/nexus/_nexus_loader.py b/src/ess/reduce/nexus/_nexus_loader.py index c0ac0a2c..046e6c0f 100644 --- a/src/ess/reduce/nexus/_nexus_loader.py +++ b/src/ess/reduce/nexus/_nexus_loader.py @@ -13,16 +13,16 @@ from ..logging import get_logger from .types import ( - Filename, - NeXusDetector, + AnyNeXusMonitorName, + AnyRunAnyNeXusMonitor, + AnyRunFilename, + AnyRunNeXusDetector, + AnyRunNeXusSample, + AnyRunNeXusSource, NeXusDetectorName, NeXusEntryName, NeXusGroup, NeXusLocationSpec, - NeXusMonitor, - NeXusMonitorName, - NeXusSample, - NeXusSource, NeXusSourceName, RawDetectorData, RawMonitorData, @@ -36,13 +36,13 @@ class NoNewDefinitionsType: ... def load_detector( - file_path: Filename, + file_path: AnyRunFilename, selection=(), *, detector_name: NeXusDetectorName, entry_name: NeXusEntryName | None = None, definitions: Mapping | None | NoNewDefinitionsType = NoNewDefinitions, -) -> NeXusDetector: +) -> AnyRunNeXusDetector: """Load a single detector (bank) from a NeXus file. The detector positions are computed automatically from NeXus transformations, @@ -74,7 +74,7 @@ def load_detector( A data group containing the detector events or histogram and any auxiliary data stored in the same NeXus group. """ - return NeXusDetector( + return AnyRunNeXusDetector( load_component( NeXusLocationSpec( filename=file_path, @@ -89,13 +89,13 @@ def load_detector( def load_monitor( - file_path: Filename, + file_path: AnyRunFilename, selection=(), *, - monitor_name: NeXusMonitorName, + monitor_name: AnyNeXusMonitorName, entry_name: NeXusEntryName | None = None, definitions: Mapping | None | NoNewDefinitionsType = NoNewDefinitions, -) -> NeXusMonitor: +) -> AnyRunAnyNeXusMonitor: """Load a single monitor from a NeXus file. The monitor position is computed automatically from NeXus transformations, @@ -127,7 +127,7 @@ def load_monitor( A data group containing the monitor events or histogram and any auxiliary data stored in the same NeXus group. """ - return NeXusMonitor( + return AnyRunAnyNeXusMonitor( load_component( NeXusLocationSpec( filename=file_path, @@ -142,12 +142,12 @@ def load_monitor( def load_source( - file_path: Filename, + file_path: AnyRunFilename, *, source_name: NeXusSourceName | None = None, entry_name: NeXusEntryName | None = None, definitions: Mapping | None | NoNewDefinitionsType = NoNewDefinitions, -) -> NeXusSource: +) -> AnyRunNeXusSource: """Load a source from a NeXus file. The source position is computed automatically from NeXus transformations, @@ -181,7 +181,7 @@ def load_source( A data group containing all data stored in the source NeXus group. """ - return NeXusSource( + return AnyRunNeXusSource( load_component( NeXusLocationSpec( filename=file_path, component_name=source_name, entry_name=entry_name @@ -193,10 +193,10 @@ def load_source( def load_sample( - file_path: Filename, + file_path: AnyRunFilename, entry_name: NeXusEntryName | None = None, definitions: Mapping | None | NoNewDefinitionsType = NoNewDefinitions, -) -> NeXusSample: +) -> AnyRunNeXusSample: """Load a sample from a NeXus file. The sample is located based on its NeXus class. @@ -226,7 +226,7 @@ def load_sample( A data group containing all data stored in the sample NeXus group. """ - return NeXusSample( + return AnyRunNeXusSample( load_component( NeXusLocationSpec(filename=file_path, entry_name=entry_name), nx_class=snx.NXsample, @@ -274,7 +274,7 @@ def load_component( def _open_nexus_file( - file_path: Filename, + file_path: AnyRunFilename, definitions: Mapping | None | NoNewDefinitionsType = NoNewDefinitions, ) -> ContextManager: if isinstance(file_path, getattr(NeXusGroup, '__supertype__', type(None))): @@ -310,7 +310,7 @@ def _unique_child_group( return next(iter(children.values())) # type: ignore[return-value] -def extract_detector_data(detector: NeXusDetector) -> RawDetectorData: +def extract_detector_data(detector: AnyRunNeXusDetector) -> RawDetectorData: """Get and return the events or histogram from a detector loaded from NeXus. This function looks for a data array in the detector group and returns that. @@ -339,7 +339,7 @@ def extract_detector_data(detector: NeXusDetector) -> RawDetectorData: return RawDetectorData(_extract_events_or_histogram(detector)) -def extract_monitor_data(monitor: NeXusMonitor) -> RawMonitorData: +def extract_monitor_data(monitor: AnyRunAnyNeXusMonitor) -> RawMonitorData: """Get and return the events or histogram from a monitor loaded from NeXus. This function looks for a data array in the monitor group and returns that. @@ -413,7 +413,7 @@ def _select_unique_array( def load_event_data( - file_path: Filename, + file_path: AnyRunFilename, selection=(), *, entry_name: NeXusEntryName | None = None, @@ -629,7 +629,7 @@ def _parse_monitor(group: snx.Group) -> NeXusMonitorInfo: ) -def read_nexus_file_info(file_path: Filename) -> NeXusFileInfo: +def read_nexus_file_info(file_path: AnyRunFilename) -> NeXusFileInfo: """Opens and inspects a NeXus file, returning a summary of its contents.""" with _open_nexus_file(file_path) as f: entry = _unique_child_group(f, snx.NXentry, None) diff --git a/src/ess/reduce/nexus/generic_types.py b/src/ess/reduce/nexus/generic_types.py index 633493cc..e35275f5 100644 --- a/src/ess/reduce/nexus/generic_types.py +++ b/src/ess/reduce/nexus/generic_types.py @@ -1,13 +1,14 @@ """Domain types for use with Sciline, parametrized by run- and monitor-type.""" from dataclasses import dataclass +from pathlib import Path from typing import Generic, NewType, TypeVar import sciline import scipp as sc import scippnexus as snx -from .types import FilePath, NeXusFile, NeXusGroup, NeXusLocationSpec +from .types import Component, FilePath, NeXusFile, NeXusGroup, NeXusLocationSpec # 1 TypeVars used to parametrize the generic parts of the workflow @@ -148,11 +149,11 @@ class MonitorData( """Calibrated monitor merged with neutron event data.""" -Component = TypeVar('Component', bound=snx.NXobject) +class Filename(sciline.Scope[RunType, Path], Path): ... @dataclass -class Filename(Generic[RunType]): +class NeXusFileSpec(Generic[RunType]): value: FilePath | NeXusFile | NeXusGroup diff --git a/src/ess/reduce/nexus/generic_workflow.py b/src/ess/reduce/nexus/generic_workflow.py index 0bff90f7..6cea9517 100644 --- a/src/ess/reduce/nexus/generic_workflow.py +++ b/src/ess/reduce/nexus/generic_workflow.py @@ -20,6 +20,10 @@ from .types import DetectorBankSizes, GravityVector, NeXusDetectorName, PulseSelection +def file_path_to_file_spec(filename: gt.Filename[RunType]) -> gt.NeXusFileSpec[RunType]: + return gt.NeXusFileSpec[RunType](filename) + + def no_monitor_position_offset() -> gt.MonitorPositionOffset[RunType, MonitorType]: return gt.MonitorPositionOffset[RunType, MonitorType](workflow.no_offset) @@ -29,34 +33,38 @@ def no_detector_position_offset() -> gt.DetectorPositionOffset[RunType]: def unique_sample_spec( - filename: gt.Filename[RunType], + filename: gt.NeXusFileSpec[RunType], ) -> gt.NeXusComponentLocationSpec[snx.NXsample, RunType]: - return gt.NeXusComponentLocationSpec[snx.NXsample, RunType](filename=filename) + return gt.NeXusComponentLocationSpec[snx.NXsample, RunType](filename=filename.value) def unique_source_spec( - filename: gt.Filename[RunType], + filename: gt.NeXusFileSpec[RunType], ) -> gt.NeXusComponentLocationSpec[snx.NXsource, RunType]: - return gt.NeXusComponentLocationSpec[snx.NXsource, RunType](filename=filename) + return gt.NeXusComponentLocationSpec[snx.NXsource, RunType](filename=filename.value) def monitor_by_name( - filename: gt.Filename[RunType], + filename: gt.NeXusFileSpec[RunType], name: gt.NeXusMonitorName[MonitorType], selection: PulseSelection, ) -> gt.NeXusMonitorLocationSpec[RunType, MonitorType]: return gt.NeXusMonitorLocationSpec[RunType, MonitorType]( - filename=filename, component_name=name, selection={'event_time_zero': selection} + filename=filename.value, + component_name=name, + selection={'event_time_zero': selection}, ) def detector_by_name( - filename: gt.Filename[RunType], + filename: gt.NeXusFileSpec[RunType], name: NeXusDetectorName, selection: PulseSelection, ) -> gt.NeXusComponentLocationSpec[snx.NXdetector, RunType]: return gt.NeXusComponentLocationSpec[snx.NXdetector, RunType]( - filename=filename, component_name=name, selection={'event_time_zero': selection} + filename=filename.value, + component_name=name, + selection={'event_time_zero': selection}, ) @@ -174,7 +182,7 @@ def assemble_monitor_data( assemble_monitor_data.__doc__ = workflow.assemble_monitor_data.__doc__ -_common_providers = (workflow.gravity_vector_neg_y,) +_common_providers = (workflow.gravity_vector_neg_y, file_path_to_file_spec) _monitor_providers = ( no_monitor_position_offset, diff --git a/src/ess/reduce/nexus/types.py b/src/ess/reduce/nexus/types.py index b4e6ae57..0f3f8580 100644 --- a/src/ess/reduce/nexus/types.py +++ b/src/ess/reduce/nexus/types.py @@ -25,7 +25,7 @@ """Name of a detector (bank) in a NeXus file.""" NeXusEntryName = NewType('NeXusEntryName', str) """Name of an entry in a NeXus file.""" -NeXusMonitorName = NewType('NeXusMonitorName', str) +AnyNeXusMonitorName = NewType('AnyNeXusMonitorName', str) """Name of a monitor in a NeXus file.""" NeXusSourceName = NewType('NeXusSourceName', str) """Name of a source in a NeXus file.""" @@ -35,39 +35,39 @@ RawMonitorData = NewType('RawMonitorData', sc.DataArray) """Data extracted from a RawMonitor.""" -NeXusDetector = NewType('NeXusDetector', sc.DataGroup) +AnyRunNeXusDetector = NewType('AnyRunNeXusDetector', sc.DataGroup) """Full raw data from a NeXus detector.""" -NeXusMonitor = NewType('NeXusMonitor', sc.DataGroup) +AnyRunAnyNeXusMonitor = NewType('AnyRunAnyNeXusMonitor', sc.DataGroup) """Full raw data from a NeXus monitor.""" -NeXusSample = NewType('NeXusSample', sc.DataGroup) +AnyRunNeXusSample = NewType('AnyRunNeXusSample', sc.DataGroup) """Raw data from a NeXus sample.""" -NeXusSource = NewType('NeXusSource', sc.DataGroup) +AnyRunNeXusSource = NewType('AnyRunNeXusSource', sc.DataGroup) """Raw data from a NeXus source.""" -NeXusDetectorEventData = NewType('NeXusDetectorEventData', sc.DataArray) +AnyRunNeXusDetectorEventData = NewType('AnyRunNeXusDetectorEventData', sc.DataArray) """Data array loaded from a NeXus NXevent_data group within an NXdetector.""" -NeXusMonitorEventData = NewType('NeXusMonitorEventData', sc.DataArray) +AnyRunAnyNeXusMonitorEventData = NewType('AnyRunAnyNeXusMonitorEventData', sc.DataArray) """Data array loaded from a NeXus NXevent_data group within an NXmonitor.""" -SourcePosition = NewType('SourcePosition', sc.Variable) +AnyRunSourcePosition = NewType('AnyRunSourcePosition', sc.Variable) """Position of the neutron source.""" -SamplePosition = NewType('SamplePosition', sc.Variable) +AnyRunSamplePosition = NewType('AnyRunSamplePosition', sc.Variable) """Position of the sample.""" -DetectorPositionOffset = NewType('DetectorPositionOffset', sc.Variable) +AnyRunDetectorPositionOffset = NewType('AnyRunDetectorPositionOffset', sc.Variable) """Offset for the detector position, added to base position.""" -MonitorPositionOffset = NewType('MonitorPositionOffset', sc.Variable) +AnyRunAnyMonitorPositionOffset = NewType('AnyRunAnyMonitorPositionOffset', sc.Variable) """Offset for the monitor position, added to base position.""" DetectorBankSizes = NewType("DetectorBankSizes", dict[str, dict[str, int | Any]]) -CalibratedDetector = NewType('CalibratedDetector', sc.DataArray) -CalibratedMonitor = NewType('CalibratedMonitor', sc.DataArray) +AnyRunCalibratedDetector = NewType('AnyRunCalibratedDetector', sc.DataArray) +AnyRunAnyCalibratedMonitor = NewType('AnyRunAnyCalibratedMonitor', sc.DataArray) -DetectorData = NewType('DetectorData', sc.DataArray) -MonitorData = NewType('MonitorData', sc.DataArray) +AnyRunDetectorData = NewType('AnyRunDetectorData', sc.DataArray) +AnyRunAnyMonitorData = NewType('AnyRunAnyMonitorData', sc.DataArray) PulseSelection = NewType('PulseSelection', slice) @@ -75,7 +75,7 @@ Component = TypeVar('Component', bound=snx.NXobject) -Filename = FilePath | NeXusFile | NeXusGroup +AnyRunFilename = FilePath | NeXusFile | NeXusGroup @dataclass @@ -84,7 +84,7 @@ class NeXusLocationSpec(Generic[Component]): NeXus filename and optional parameters to identify (parts of) a component to load. """ - filename: Filename + filename: AnyRunFilename entry_name: NeXusEntryName | None = None component_name: str | None = None selection: snx.typing.ScippIndex = () diff --git a/src/ess/reduce/nexus/workflow.py b/src/ess/reduce/nexus/workflow.py index 851e869d..8cca2660 100644 --- a/src/ess/reduce/nexus/workflow.py +++ b/src/ess/reduce/nexus/workflow.py @@ -13,27 +13,27 @@ from . import _nexus_loader as nexus from .types import ( - CalibratedDetector, - CalibratedMonitor, + AnyNeXusMonitorName, + AnyRunAnyCalibratedMonitor, + AnyRunAnyMonitorData, + AnyRunAnyMonitorPositionOffset, + AnyRunAnyNeXusMonitor, + AnyRunAnyNeXusMonitorEventData, + AnyRunCalibratedDetector, + AnyRunDetectorData, + AnyRunDetectorPositionOffset, + AnyRunFilename, + AnyRunNeXusDetector, + AnyRunNeXusDetectorEventData, + AnyRunNeXusSample, + AnyRunNeXusSource, + AnyRunSamplePosition, + AnyRunSourcePosition, DetectorBankSizes, - DetectorData, - DetectorPositionOffset, - Filename, GravityVector, - MonitorData, - MonitorPositionOffset, - NeXusDetector, - NeXusDetectorEventData, NeXusDetectorName, NeXusLocationSpec, - NeXusMonitor, - NeXusMonitorEventData, - NeXusMonitorName, - NeXusSample, - NeXusSource, PulseSelection, - SamplePosition, - SourcePosition, ) origin = sc.vector([0, 0, 0], unit="m") @@ -49,7 +49,7 @@ def gravity_vector_neg_y() -> GravityVector: return GravityVector(sc.vector(value=[0, -1, 0]) * g) -def unique_sample_spec(filename: Filename) -> NeXusLocationSpec[snx.NXsample]: +def unique_sample_spec(filename: AnyRunFilename) -> NeXusLocationSpec[snx.NXsample]: """ Create a location spec for a unique sample group in a NeXus file. @@ -61,7 +61,7 @@ def unique_sample_spec(filename: Filename) -> NeXusLocationSpec[snx.NXsample]: return NeXusLocationSpec[snx.NXsample](filename=filename) -def unique_source_spec(filename: Filename) -> NeXusLocationSpec[snx.NXsource]: +def unique_source_spec(filename: AnyRunFilename) -> NeXusLocationSpec[snx.NXsource]: """ Create a location spec for a unique source group in a NeXus file. @@ -74,7 +74,7 @@ def unique_source_spec(filename: Filename) -> NeXusLocationSpec[snx.NXsource]: def monitor_by_name( - filename: Filename, name: NeXusMonitorName, selection: PulseSelection + filename: AnyRunFilename, name: AnyNeXusMonitorName, selection: PulseSelection ) -> NeXusLocationSpec[snx.NXmonitor]: """ Create a location spec for a monitor group in a NeXus file. @@ -94,7 +94,7 @@ def monitor_by_name( def detector_by_name( - filename: Filename, name: NeXusDetectorName, selection: PulseSelection + filename: AnyRunFilename, name: NeXusDetectorName, selection: PulseSelection ) -> NeXusLocationSpec[snx.NXdetector]: """ Create a location spec for a detector group in a NeXus file. @@ -113,7 +113,7 @@ def detector_by_name( ) -def load_nexus_sample(location: NeXusLocationSpec[snx.NXsample]) -> NeXusSample: +def load_nexus_sample(location: NeXusLocationSpec[snx.NXsample]) -> AnyRunNeXusSample: """ Load a NeXus sample group from a file. @@ -131,10 +131,10 @@ def load_nexus_sample(location: NeXusLocationSpec[snx.NXsample]) -> NeXusSample: dg = nexus.load_component(location, nx_class=snx.NXsample) except ValueError: dg = sc.DataGroup() - return NeXusSample(dg) + return AnyRunNeXusSample(dg) -def load_nexus_source(location: NeXusLocationSpec[snx.NXsource]) -> NeXusSource: +def load_nexus_source(location: NeXusLocationSpec[snx.NXsource]) -> AnyRunNeXusSource: """ Load a NeXus source group from a file. @@ -143,10 +143,12 @@ def load_nexus_source(location: NeXusLocationSpec[snx.NXsource]) -> NeXusSource: location: Location spec for the source group. """ - return NeXusSource(nexus.load_component(location, nx_class=snx.NXsource)) + return AnyRunNeXusSource(nexus.load_component(location, nx_class=snx.NXsource)) -def load_nexus_detector(location: NeXusLocationSpec[snx.NXdetector]) -> NeXusDetector: +def load_nexus_detector( + location: NeXusLocationSpec[snx.NXdetector], +) -> AnyRunNeXusDetector: """ Load detector from NeXus, but with event data replaced by placeholders. @@ -180,12 +182,14 @@ def load_nexus_detector(location: NeXusLocationSpec[snx.NXdetector]) -> NeXusDet # The selection is only used for selecting a range of event data. location = replace(location, selection=()) - return NeXusDetector( + return AnyRunNeXusDetector( nexus.load_component(location, nx_class=snx.NXdetector, definitions=definitions) ) -def load_nexus_monitor(location: NeXusLocationSpec[snx.NXmonitor]) -> NeXusMonitor: +def load_nexus_monitor( + location: NeXusLocationSpec[snx.NXmonitor], +) -> AnyRunAnyNeXusMonitor: """ Load monitor from NeXus, but with event data replaced by placeholders. @@ -216,14 +220,14 @@ def load_nexus_monitor(location: NeXusLocationSpec[snx.NXmonitor]) -> NeXusMonit """ definitions = snx.base_definitions() definitions["NXmonitor"] = _StrippedMonitor - return NeXusMonitor( + return AnyRunAnyNeXusMonitor( nexus.load_component(location, nx_class=snx.NXmonitor, definitions=definitions) ) def load_nexus_detector_event_data( location: NeXusLocationSpec[snx.NXdetector], -) -> NeXusDetectorEventData: +) -> AnyRunNeXusDetectorEventData: """ Load event data from a NeXus detector group. @@ -232,7 +236,7 @@ def load_nexus_detector_event_data( location: Location spec for the detector group. """ - return NeXusDetectorEventData( + return AnyRunNeXusDetectorEventData( nexus.load_event_data( file_path=location.filename, entry_name=location.entry_name, @@ -244,7 +248,7 @@ def load_nexus_detector_event_data( def load_nexus_monitor_event_data( location: NeXusLocationSpec[snx.NXmonitor], -) -> NeXusMonitorEventData: +) -> AnyRunAnyNeXusMonitorEventData: """ Load event data from a NeXus monitor group. @@ -253,7 +257,7 @@ def load_nexus_monitor_event_data( location: Location spec for the monitor group. """ - return NeXusMonitorEventData( + return AnyRunAnyNeXusMonitorEventData( nexus.load_event_data( file_path=location.filename, entry_name=location.entry_name, @@ -263,7 +267,7 @@ def load_nexus_monitor_event_data( ) -def get_source_position(source: NeXusSource) -> SourcePosition: +def get_source_position(source: AnyRunNeXusSource) -> AnyRunSourcePosition: """ Extract the source position from a NeXus source group. @@ -272,10 +276,10 @@ def get_source_position(source: NeXusSource) -> SourcePosition: source: NeXus source group. """ - return SourcePosition(source["position"]) + return AnyRunSourcePosition(source["position"]) -def get_sample_position(sample: NeXusSample) -> SamplePosition: +def get_sample_position(sample: AnyRunNeXusSample) -> AnyRunSamplePosition: """ Extract the sample position from a NeXus sample group. @@ -286,18 +290,18 @@ def get_sample_position(sample: NeXusSample) -> SamplePosition: sample: NeXus sample group. """ - return SamplePosition(sample.get("position", origin)) + return AnyRunSamplePosition(sample.get("position", origin)) def get_calibrated_detector( - detector: NeXusDetector, + detector: AnyRunNeXusDetector, *, - offset: DetectorPositionOffset, - source_position: SourcePosition, - sample_position: SamplePosition, + offset: AnyRunDetectorPositionOffset, + source_position: AnyRunSourcePosition, + sample_position: AnyRunSamplePosition, gravity: GravityVector, bank_sizes: DetectorBankSizes, -) -> CalibratedDetector: +) -> AnyRunCalibratedDetector: """ Extract the data array corresponding to a detector's signal field. @@ -322,14 +326,15 @@ def get_calibrated_detector( Dictionary of detector bank sizes. """ da = nexus.extract_detector_data(detector) - if (sizes := (bank_sizes or {}).get(detector['nexus_component_name'])) is not None: + if ( + sizes := (bank_sizes or {}).get(detector.get('nexus_component_name')) + ) is not None: da = da.fold(dim="detector_number", sizes=sizes) # Note: We apply offset as early as possible, i.e., right in this function # the detector array from the raw loader NeXus group, to prevent a source of bugs. - position = detector['position'] - return CalibratedDetector( + return AnyRunCalibratedDetector( da.assign_coords( - position=position + offset, + position=da.coords['position'] + offset, source_position=source_position, sample_position=sample_position, gravity=gravity, @@ -338,8 +343,8 @@ def get_calibrated_detector( def assemble_detector_data( - detector: CalibratedDetector, event_data: NeXusDetectorEventData -) -> DetectorData: + detector: AnyRunCalibratedDetector, event_data: AnyRunNeXusDetectorEventData +) -> AnyRunDetectorData: """ Assemble a detector data array with event data and source- and sample-position. @@ -355,7 +360,7 @@ def assemble_detector_data( grouped = nexus.group_event_data( event_data=event_data, detector_number=detector.coords['detector_number'] ) - return DetectorData( + return AnyRunDetectorData( _add_variances(grouped) .assign_coords(detector.coords) .assign_masks(detector.masks) @@ -363,10 +368,10 @@ def assemble_detector_data( def get_calibrated_monitor( - monitor: NeXusMonitor, - offset: MonitorPositionOffset, - source_position: SourcePosition, -) -> CalibratedMonitor: + monitor: AnyRunAnyNeXusMonitor, + offset: AnyRunAnyMonitorPositionOffset, + source_position: AnyRunSourcePosition, +) -> AnyRunAnyCalibratedMonitor: """ Extract the data array corresponding to a monitor's signal field. @@ -382,7 +387,7 @@ def get_calibrated_monitor( source_position: Position of the neutron source. """ - return CalibratedMonitor( + return AnyRunAnyCalibratedMonitor( nexus.extract_monitor_data(monitor).assign_coords( position=monitor['position'] + offset, source_position=source_position, @@ -391,8 +396,9 @@ def get_calibrated_monitor( def assemble_monitor_data( - monitor: CalibratedMonitor, event_data: NeXusMonitorEventData -) -> MonitorData: + monitor: AnyRunAnyCalibratedMonitor, + event_data: AnyRunAnyNeXusMonitorEventData, +) -> AnyRunAnyMonitorData: """ Assemble a monitor data array with event data. @@ -406,7 +412,7 @@ def assemble_monitor_data( Event data array. """ da = event_data.assign_coords(monitor.coords).assign_masks(monitor.masks) - return MonitorData(_add_variances(da)) + return AnyRunAnyMonitorData(_add_variances(da)) def _drop( @@ -484,7 +490,7 @@ def LoadMonitorWorkflow() -> sciline.Pipeline: ) ) wf[PulseSelection] = PulseSelection(()) - wf[MonitorPositionOffset] = MonitorPositionOffset(no_offset) + wf[AnyRunAnyMonitorPositionOffset] = AnyRunAnyMonitorPositionOffset(no_offset) return wf @@ -507,11 +513,11 @@ def LoadDetectorWorkflow() -> sciline.Pipeline: ) wf[PulseSelection] = PulseSelection(()) wf[DetectorBankSizes] = DetectorBankSizes({}) - wf[DetectorPositionOffset] = DetectorPositionOffset(no_offset) + wf[AnyRunDetectorPositionOffset] = AnyRunDetectorPositionOffset(no_offset) return wf -def LoadNeXusWorkflow(filename: Filename) -> sciline.Pipeline: +def LoadNeXusWorkflow(filename: AnyRunFilename) -> sciline.Pipeline: """ Workflow for loading detector and monitor data from a NeXus file. @@ -528,9 +534,9 @@ def LoadNeXusWorkflow(filename: Filename) -> sciline.Pipeline: import pandas as pd wf = sciline.Pipeline() - wf[DetectorData] = LoadDetectorWorkflow() - wf[MonitorData] = LoadMonitorWorkflow() - wf[Filename] = filename + wf[AnyRunDetectorData] = LoadDetectorWorkflow() + wf[AnyRunAnyMonitorData] = LoadMonitorWorkflow() + wf[AnyRunFilename] = filename wf.insert(nexus.read_nexus_file_info) wf[nexus.NeXusFileInfo] = info = wf.compute(nexus.NeXusFileInfo) # Note: There is a good reason against auto-mapping here: @@ -541,7 +547,9 @@ def LoadNeXusWorkflow(filename: Filename) -> sciline.Pipeline: dets = [name for name, det in info.detectors.items() if det.n_pixel is not None] det_df = pd.DataFrame({NeXusDetectorName: dets}, index=dets).rename_axis('detector') mons = list(info.monitors) - mon_df = pd.DataFrame({NeXusMonitorName: mons}, index=mons).rename_axis('monitor') + mon_df = pd.DataFrame({AnyNeXusMonitorName: mons}, index=mons).rename_axis( + 'monitor' + ) return wf.map(det_df).map(mon_df) diff --git a/tests/nexus/nexus_loader_test.py b/tests/nexus/nexus_loader_test.py index 37212c65..54723b74 100644 --- a/tests/nexus/nexus_loader_test.py +++ b/tests/nexus/nexus_loader_test.py @@ -364,7 +364,7 @@ def test_load_monitor(nexus_file, expected_monitor, entry_name, selection): monitor = nexus.load_monitor( nexus_file, **({'selection': selection} if selection is not None else {}), - monitor_name=nexus.types.NeXusMonitorName('monitor'), + monitor_name=nexus.types.AnyNeXusMonitorName('monitor'), entry_name=entry_name, ) expected = expected_monitor[selection] if selection else expected_monitor @@ -382,7 +382,7 @@ def test_load_source(nexus_file, expected_source, entry_name, source_name): # NeXus details that we don't need to test as long as the positions are ok: del source['depends_on'] del source['transformations'] - sc.testing.assert_identical(source, nexus.types.NeXusSource(expected_source)) + sc.testing.assert_identical(source, nexus.types.AnyRunNeXusSource(expected_source)) @pytest.mark.parametrize( @@ -414,7 +414,7 @@ def new(*args, **kwargs): @pytest.mark.parametrize('entry_name', [None, nexus.types.NeXusEntryName('entry-001')]) def test_load_sample(nexus_file, expected_sample, entry_name): sample = nexus.load_sample(nexus_file, entry_name=entry_name) - sc.testing.assert_identical(sample, nexus.types.NeXusSample(expected_sample)) + sc.testing.assert_identical(sample, nexus.types.AnyRunNeXusSample(expected_sample)) def test_extract_detector_data(): @@ -425,7 +425,7 @@ def test_extract_detector_data(): ' _': sc.linspace('xx', 2, 3, 10), } ) - data = nexus.extract_detector_data(nexus.types.NeXusDetector(detector)) + data = nexus.extract_detector_data(nexus.types.AnyRunNeXusDetector(detector)) sc.testing.assert_identical(data, nexus.types.RawDetectorData(detector['jdl2ab'])) @@ -437,7 +437,7 @@ def test_extract_monitor_data(): ' _': sc.linspace('xx', 2, 3, 10), } ) - data = nexus.extract_monitor_data(nexus.types.NeXusMonitor(monitor)) + data = nexus.extract_monitor_data(nexus.types.AnyRunAnyNeXusMonitor(monitor)) sc.testing.assert_identical(data, nexus.types.RawMonitorData(monitor['(eed)'])) @@ -453,7 +453,7 @@ def test_extract_detector_data_requires_unique_dense_data(): with pytest.raises( ValueError, match="Cannot uniquely identify the data to extract" ): - nexus.extract_detector_data(nexus.types.NeXusDetector(detector)) + nexus.extract_detector_data(nexus.types.AnyRunNeXusDetector(detector)) def test_extract_detector_data_requires_unique_event_data(): @@ -468,7 +468,7 @@ def test_extract_detector_data_requires_unique_event_data(): with pytest.raises( ValueError, match="Cannot uniquely identify the data to extract" ): - nexus.extract_detector_data(nexus.types.NeXusDetector(detector)) + nexus.extract_detector_data(nexus.types.AnyRunNeXusDetector(detector)) def test_extract_detector_data_favors_event_data_over_histogram_data(): @@ -480,5 +480,5 @@ def test_extract_detector_data_favors_event_data_over_histogram_data(): ' _': sc.linspace('xx', 2, 3, 10), } ) - data = nexus.extract_detector_data(nexus.types.NeXusDetector(detector)) + data = nexus.extract_detector_data(nexus.types.AnyRunNeXusDetector(detector)) sc.testing.assert_identical(data, nexus.types.RawDetectorData(detector['lob'])) diff --git a/tests/nexus/workflow_test.py b/tests/nexus/workflow_test.py index a516b9ab..15dbd909 100644 --- a/tests/nexus/workflow_test.py +++ b/tests/nexus/workflow_test.py @@ -7,13 +7,13 @@ @pytest.fixture(params=[{}, {'aux': 1}]) -def group_with_no_position(request) -> workflow.NeXusSample: - return workflow.NeXusSample(sc.DataGroup(request.param)) +def group_with_no_position(request) -> workflow.AnyRunNeXusSample: + return workflow.AnyRunNeXusSample(sc.DataGroup(request.param)) def test_sample_position_returns_position_of_group() -> None: position = sc.vector([1.0, 2.0, 3.0], unit='m') - sample_group = workflow.NeXusSample(sc.DataGroup(position=position)) + sample_group = workflow.AnyRunNeXusSample(sc.DataGroup(position=position)) assert_identical(workflow.get_sample_position(sample_group), position) @@ -27,7 +27,7 @@ def test_get_sample_position_returns_origin_if_position_not_found( def test_get_source_position_returns_position_of_group() -> None: position = sc.vector([1.0, 2.0, 3.0], unit='m') - source_group = workflow.NeXusSource(sc.DataGroup(position=position)) + source_group = workflow.AnyRunNeXusSource(sc.DataGroup(position=position)) assert_identical(workflow.get_source_position(source_group), position) @@ -39,17 +39,23 @@ def test_get_source_position_raises_exception_if_position_not_found( @pytest.fixture() -def nexus_detector() -> workflow.NeXusDetector: +def nexus_detector() -> workflow.AnyRunNeXusDetector: detector_number = sc.arange('detector_number', 6, unit=None) + x = sc.linspace('detector_number', 0, 1, num=6, unit='m') + position = sc.spatial.as_vectors(x, sc.zeros_like(x), sc.zeros_like(x)) data = sc.DataArray( sc.empty_like(detector_number), coords={ 'detector_number': detector_number, + 'position': position, }, ) - return workflow.NeXusDetector( + return workflow.AnyRunNeXusDetector( sc.DataGroup( data=data, + # Note that this position (the overall detector position) will be ignored, + # only the pixel positions (typically defined relative to this in an + # actual NeXus file) will be used. position=sc.vector([1.0, 2.0, 3.0], unit='m'), nexus_component_name='detector1', ) @@ -61,7 +67,7 @@ def source_position() -> sc.Variable: return sc.vector([0.0, 0.0, -10.0], unit='m') -def test_get_calibrated_detector_extracts_data_field_from_nexus_monitor( +def test_get_calibrated_detector_extracts_data_field_from_nexus_detector( nexus_detector, source_position, ) -> None: @@ -74,9 +80,7 @@ def test_get_calibrated_detector_extracts_data_field_from_nexus_monitor( bank_sizes={}, ) assert_identical( - detector.drop_coords( - ('position', 'sample_position', 'source_position', 'gravity') - ), + detector.drop_coords(('sample_position', 'source_position', 'gravity')), nexus_detector['data'], ) @@ -98,6 +102,21 @@ def test_get_calibrated_detector_folds_detector_number_if_mapping_given( assert detector.sizes == sizes +def test_get_calibrated_detector_works_if_nexus_component_name_is_missing( + nexus_detector, source_position +): + del nexus_detector['nexus_component_name'] + detector = workflow.get_calibrated_detector( + nexus_detector, + offset=workflow.no_offset, + source_position=source_position, + sample_position=workflow.origin, + gravity=workflow.gravity_vector_neg_y(), + bank_sizes={}, + ) + assert detector.sizes == nexus_detector['data'].sizes + + def test_get_calibrated_detector_adds_offset_to_position( nexus_detector, source_position, @@ -111,7 +130,9 @@ def test_get_calibrated_detector_adds_offset_to_position( gravity=workflow.gravity_vector_neg_y(), bank_sizes={}, ) - assert_identical(detector.coords['position'], sc.vector([1.1, 2.2, 3.3], unit='m')) + position = nexus_detector['data'].coords['position'] + offset + assert detector.coords['position'].sizes == {'detector_number': 6} + assert_identical(detector.coords['position'], position) def test_get_calibrated_detector_forwards_coords( @@ -147,9 +168,9 @@ def test_get_calibrated_detector_forwards_masks( @pytest.fixture() -def calibrated_detector() -> workflow.CalibratedDetector: +def calibrated_detector() -> workflow.AnyRunCalibratedDetector: detector_number = sc.arange('detector_number', 6, unit=None) - return workflow.CalibratedDetector( + return workflow.AnyRunCalibratedDetector( sc.DataArray( sc.empty_like(detector_number), coords={ @@ -161,13 +182,13 @@ def calibrated_detector() -> workflow.CalibratedDetector: @pytest.fixture() -def detector_event_data() -> workflow.NeXusDetectorEventData: +def detector_event_data() -> workflow.AnyRunNeXusDetectorEventData: content = sc.DataArray( sc.ones(dims=['event'], shape=[17], unit='counts'), coords={'event_id': sc.arange('event', 17, unit=None) % sc.index(6)}, ) weights = sc.bins(data=content, dim='event') - return workflow.NeXusDetectorEventData( + return workflow.AnyRunNeXusDetectorEventData( sc.DataArray( weights, coords={ @@ -217,9 +238,9 @@ def test_assemble_detector_preserves_masks(calibrated_detector, detector_event_d @pytest.fixture() -def nexus_monitor() -> workflow.NeXusMonitor: +def nexus_monitor() -> workflow.AnyRunAnyNeXusMonitor: data = sc.DataArray(sc.scalar(1.2), coords={'something': sc.scalar(13)}) - return workflow.NeXusMonitor( + return workflow.AnyRunAnyNeXusMonitor( sc.DataGroup(data=data, position=sc.vector([1.0, 2.0, 3.0], unit='m')) ) @@ -250,8 +271,8 @@ def test_get_calibrated_monitor_subtracts_offset_from_position( @pytest.fixture() -def calibrated_monitor() -> workflow.CalibratedMonitor: - return workflow.CalibratedMonitor( +def calibrated_monitor() -> workflow.AnyRunAnyCalibratedMonitor: + return workflow.AnyRunAnyCalibratedMonitor( sc.DataArray( sc.scalar(0), coords={'position': sc.vector([1.0, 2.0, 3.0], unit='m')}, @@ -260,10 +281,10 @@ def calibrated_monitor() -> workflow.CalibratedMonitor: @pytest.fixture() -def monitor_event_data() -> workflow.NeXusMonitorEventData: +def monitor_event_data() -> workflow.AnyRunAnyNeXusMonitorEventData: content = sc.DataArray(sc.ones(dims=['event'], shape=[17], unit='counts')) weights = sc.bins(data=content, dim='event') - return workflow.NeXusMonitorEventData( + return workflow.AnyRunAnyNeXusMonitorEventData( sc.DataArray( weights, coords={