From 350f970a6cf110820cd1cccca4f8d8f33899eb87 Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Wed, 7 Aug 2024 13:52:03 +0200 Subject: [PATCH 1/7] Add common Nexus loaders --- src/ess/dream/io/geant4.py | 16 +-- src/ess/dream/io/nexus.py | 192 +++++++++++++++++++++++++++++----- src/ess/powder/types.py | 61 +++++++++-- tests/dream/io/geant4_test.py | 4 +- tests/dream/io/nexus_test.py | 32 +++--- 5 files changed, 249 insertions(+), 56 deletions(-) diff --git a/src/ess/dream/io/geant4.py b/src/ess/dream/io/geant4.py index d5060e4a..d9aec0ed 100644 --- a/src/ess/dream/io/geant4.py +++ b/src/ess/dream/io/geant4.py @@ -10,10 +10,10 @@ CalibrationData, CalibrationFilename, Filename, + NeXusDetector, NeXusDetectorDimensions, NeXusDetectorName, RawDetector, - RawDetectorData, RawSample, RawSource, ReducibleDetectorData, @@ -64,16 +64,16 @@ def load_geant4_csv(file_path: Filename[RunType]) -> AllRawDetectors[RunType]: def extract_geant4_detector( detectors: AllRawDetectors[RunType], detector_name: NeXusDetectorName -) -> RawDetector[RunType]: +) -> NeXusDetector[RunType]: """Extract a single detector from a loaded GEANT4 simulation.""" - return RawDetector[RunType](detectors["instrument"][detector_name]) + return NeXusDetector[RunType](detectors["instrument"][detector_name]) def extract_geant4_detector_data( - detector: RawDetector[RunType], -) -> RawDetectorData[RunType]: + detector: NeXusDetector[RunType], +) -> RawDetector[RunType]: """Extract the histogram or event data from a loaded GEANT4 detector.""" - return RawDetectorData[RunType](extract_detector_data(detector)) + return RawDetector[RunType](extract_detector_data(detector)) def _load_raw_events(file_path: str) -> sc.DataArray: @@ -176,7 +176,7 @@ def get_sample_position(raw_sample: RawSample[RunType]) -> SamplePosition[RunTyp def patch_detector_data( - detector_data: RawDetectorData[RunType], + detector_data: RawDetector[RunType], source_position: SourcePosition[RunType], sample_position: SamplePosition[RunType], ) -> ReducibleDetectorData[RunType]: @@ -188,7 +188,7 @@ def patch_detector_data( def geant4_detector_dimensions( - data: RawDetectorData[SampleRun], + data: RawDetector[SampleRun], ) -> NeXusDetectorDimensions: # For geant4 data, we group by detector identifier, so the data already has # logical dimensions, so we simply return the dimensions of the detector. diff --git a/src/ess/dream/io/nexus.py b/src/ess/dream/io/nexus.py index 98fb618f..96d7bc29 100644 --- a/src/ess/dream/io/nexus.py +++ b/src/ess/dream/io/nexus.py @@ -13,14 +13,25 @@ but it is not possible to reshape the data into all the logical dimensions. """ +import warnings +from typing import Any + import scipp as sc +import scippnexus as snx from ess.reduce import nexus from ess.powder.types import ( + DetectorEventData, Filename, - LoadedNeXusDetector, + MonitorEventData, + MonitorType, + NeXusDetector, NeXusDetectorName, - RawDetectorData, + NeXusMonitor, + NeXusMonitorName, + RawDetector, + RawMonitor, + RawMonitorData, RawSample, RawSource, ReducibleDetectorData, @@ -84,10 +95,62 @@ def load_nexus_source(file_path: Filename[RunType]) -> RawSource[RunType]: def load_nexus_detector( file_path: Filename[RunType], detector_name: NeXusDetectorName -) -> LoadedNeXusDetector[RunType]: - out = nexus.load_detector(file_path=file_path, detector_name=detector_name) - out.pop("pixel_shape", None) - return LoadedNeXusDetector[RunType](out) +) -> NeXusDetector[RunType]: + definitions = snx.base_definitions() + definitions["NXdetector"] = FilteredDetector + # Events will be loaded later. Should we set something else as data instead, or + # use different NeXus definitions to completely bypass the (empty) event load? + dg = nexus.load_detector( + file_path=file_path, + detector_name=detector_name, + selection={'event_time_zero': slice(0, 0)}, + definitions=definitions, + ) + # The name is required later, e.g., for determining logical detector shape + dg['detector_name'] = detector_name + return NeXusDetector[RunType](dg) + + +def load_nexus_monitor( + file_path: Filename[RunType], monitor_name: NeXusMonitorName[MonitorType] +) -> NeXusMonitor[RunType, MonitorType]: + # It would be simpler to use something like + # selection={'event_time_zero': slice(0, 0)}, + # to avoid loading events, but currently we have files with empty NXevent_data + # groups so that does not work. Instead, skip event loading and create empty dummy. + definitions = snx.base_definitions() + definitions["NXmonitor"] = NXmonitor_no_events + # TODO There is a another problem with the DREAM files: + # Transformaiton chains depend on transformations outside the current group, so + # loading a monitor in an isolated manner is not possible + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + category=UserWarning, + message="Failed to load", + ) + monitor = nexus.load_monitor( + file_path=file_path, + monitor_name=monitor_name, + definitions=definitions, + ) + empty_events = sc.DataArray( + sc.empty(dims=['event'], shape=[0], dtype='float32', unit='counts'), + coords={'event_time_offset': sc.array(dims=['event'], values=[], unit='ns')}, + ) + monitor[f'{monitor_name}_events'] = sc.DataArray( + sc.bins( + dim='event', + data=empty_events, + begin=sc.empty(dims=['event_time_zero'], shape=[0], unit=None), + ), + coords={ + 'event_time_zero': sc.datetimes( + dims=['event_time_zero'], values=[], unit='ns' + ) + }, + ) + return NeXusMonitor[RunType, MonitorType](monitor) def get_source_position( @@ -103,42 +166,121 @@ def get_sample_position( def get_detector_data( - detector: LoadedNeXusDetector[RunType], - detector_name: NeXusDetectorName, -) -> RawDetectorData[RunType]: + detector: NeXusDetector[RunType], +) -> RawDetector[RunType]: da = nexus.extract_detector_data(detector) - if detector_name in DETECTOR_BANK_SIZES: - da = da.fold(dim="detector_number", sizes=DETECTOR_BANK_SIZES[detector_name]) - return RawDetectorData[RunType](da) + if (sizes := DETECTOR_BANK_SIZES.get(detector['detector_name'])) is not None: + da = da.fold(dim="detector_number", sizes=sizes) + return RawDetector[RunType](da) + + +def get_monitor_data( + monitor: NeXusMonitor[RunType, MonitorType], + source_position: SourcePosition[RunType], +) -> RawMonitor[RunType, MonitorType]: + return RawMonitor[RunType, MonitorType]( + nexus.extract_monitor_data(monitor).assign_coords( + position=monitor['position'], source_position=source_position + ) + ) -def patch_detector_data( - detector_data: RawDetectorData[RunType], +def assemble_detector_data( + detector: RawDetector[RunType], + event_data: DetectorEventData[RunType], source_position: SourcePosition[RunType], sample_position: SamplePosition[RunType], ) -> ReducibleDetectorData[RunType]: """ - Patch a detector data object with source and sample positions. + Assemble a detector data object with source and sample positions and event data. Also adds variances to the event data if they are missing. """ - out = detector_data.copy(deep=False) + grouped = nexus.group_event_data( + event_data=event_data, detector_number=detector.coords['detector_number'] + ) + detector.data = grouped.data + return ReducibleDetectorData[RunType]( + _add_variances(da=detector).assign_coords( + source_position=source_position, sample_position=sample_position + ) + ) + + +def assemble_monitor_data( + monitor_data: RawMonitor[RunType, MonitorType], + event_data: MonitorEventData[RunType, MonitorType], +) -> RawMonitorData[RunType, MonitorType]: + meta = monitor_data.drop_coords('event_time_zero') + da = event_data.assign_coords(meta.coords).assign_masks(meta.masks) + return RawMonitorData[RunType, MonitorType](_add_variances(da=da)) + + +def _skip( + _: str, obj: snx.Field | snx.Group, classes: tuple[snx.NXobject, ...] +) -> bool: + return isinstance(obj, snx.Group) and (obj.nx_class in classes) + + +class FilteredDetector(snx.NXdetector): + def __init__( + self, attrs: dict[str, Any], children: dict[str, snx.Field | snx.Group] + ): + children = { + name: child + for name, child in children.items() + if not _skip(name, child, classes=(snx.NXoff_geometry,)) + } + super().__init__(attrs=attrs, children=children) + + +class NXmonitor_no_events(snx.NXmonitor): + def __init__( + self, attrs: dict[str, Any], children: dict[str, snx.Field | snx.Group] + ): + children = { + name: child + for name, child in children.items() + if not _skip(name, child, classes=(snx.NXevent_data,)) + } + super().__init__(attrs=attrs, children=children) + + +def load_detector_event_data( + file_path: Filename[RunType], detector_name: NeXusDetectorName +) -> DetectorEventData[RunType]: + da = nexus.load_event_data(file_path=file_path, component_name=detector_name) + return DetectorEventData[RunType](da) + + +def load_monitor_event_data( + file_path: Filename[RunType], monitor_name: NeXusMonitorName[MonitorType] +) -> MonitorEventData[RunType, MonitorType]: + da = nexus.load_event_data(file_path=file_path, component_name=monitor_name) + return MonitorEventData[RunType, MonitorType](da) + + +def _add_variances(da: sc.DataArray) -> sc.DataArray: + out = da.copy(deep=False) if out.bins is not None: - content = out.bins.constituents["data"] + content = out.bins.constituents['data'] if content.variances is None: content.variances = content.values - out.coords["sample_position"] = sample_position - out.coords["source_position"] = source_position - return ReducibleDetectorData[RunType](out) + return out providers = ( + assemble_detector_data, + assemble_monitor_data, + get_detector_data, + get_monitor_data, + get_sample_position, + get_source_position, + load_detector_event_data, + load_monitor_event_data, + load_nexus_detector, + load_nexus_monitor, load_nexus_sample, load_nexus_source, - load_nexus_detector, - get_source_position, - get_sample_position, - get_detector_data, - patch_detector_data, ) """ Providers for loading and processing DREAM NeXus data. diff --git a/src/ess/powder/types.py b/src/ess/powder/types.py index 658bcc1a..e9adb4de 100644 --- a/src/ess/powder/types.py +++ b/src/ess/powder/types.py @@ -28,6 +28,13 @@ RunType = TypeVar("RunType", EmptyInstrumentRun, SampleRun, VanadiumRun) """TypeVar used for specifying the run.""" +# 1.2 Monitor types +Monitor1 = NewType('Monitor1', int) +"""Placeholder for monitor 1.""" +Monitor2 = NewType('Monitor2', int) +"""Placeholder for monitor 2.""" +MonitorType = TypeVar('MonitorType', Monitor1, Monitor2) +"""TypeVar used for identifying a monitor""" # 2 Workflow parameters @@ -38,6 +45,11 @@ NeXusDetectorName = NewType("NeXusDetectorName", str) """Name of detector entry in NeXus file""" + +class NeXusMonitorName(sciline.Scope[MonitorType, str], str): + """Name of Incident|Transmission monitor in NeXus file""" + + DspacingBins = NewType("DSpacingBins", sc.Variable) """Bin edges for d-spacing.""" @@ -126,9 +138,46 @@ class FocussedDataDspacingTwoTheta(sciline.Scope[RunType, sc.DataArray], sc.Data """Data that has been normalized by a vanadium run, and grouped into 2theta bins.""" -class LoadedNeXusDetector(sciline.Scope[RunType, sc.DataGroup], sc.DataGroup): - """Detector data, loaded from a NeXus file, containing not only neutron events - but also pixel shape information, transformations, ...""" +class NeXusDetector(sciline.Scope[RunType, sc.DataGroup], sc.DataGroup): + """ + Detector loaded from a NeXus file, without event data. + + Contains detector numbers, pixel shape information, transformations, ... + """ + + +class NeXusMonitor( + sciline.ScopeTwoParams[RunType, MonitorType, sc.DataGroup], sc.DataGroup +): + """ + Monitor loaded from a NeXus file, without event data. + + Contains detector numbers, pixel shape information, transformations, ... + """ + + +class DetectorEventData(sciline.Scope[RunType, sc.DataArray], sc.DataArray): + """Event data loaded from a detector in a NeXus file""" + + +class MonitorEventData( + sciline.ScopeTwoParams[RunType, MonitorType, sc.DataArray], sc.DataArray +): + """Event data loaded from a monitor in a NeXus file""" + + +class RawMonitor( + sciline.ScopeTwoParams[RunType, MonitorType, sc.DataArray], sc.DataArray +): + """Raw monitor data""" + + +class RawMonitorData( + sciline.ScopeTwoParams[RunType, MonitorType, sc.DataArray], sc.DataArray +): + """Raw monitor data where variances and necessary coordinates + (e.g. source position) have been added, and where optionally some + user configuration was applied to some of the coordinates.""" class MaskedData(sciline.Scope[RunType, sc.DataArray], sc.DataArray): @@ -156,11 +205,7 @@ class RawDataAndMetadata(sciline.Scope[RunType, sc.DataGroup], sc.DataGroup): """Raw data and associated metadata.""" -class RawDetector(sciline.Scope[RunType, sc.DataGroup], sc.DataGroup): - """Full raw data for a detector.""" - - -class RawDetectorData(sciline.Scope[RunType, sc.DataArray], sc.DataArray): +class RawDetector(sciline.Scope[RunType, sc.DataArray], sc.DataArray): """Data (events / histogram) extracted from a RawDetector.""" diff --git a/tests/dream/io/geant4_test.py b/tests/dream/io/geant4_test.py index 74522663..70adecb0 100644 --- a/tests/dream/io/geant4_test.py +++ b/tests/dream/io/geant4_test.py @@ -11,7 +11,7 @@ import scipp.testing from ess.dream import data, load_geant4_csv -from ess.powder.types import Filename, NeXusDetectorName, RawDetectorData, SampleRun +from ess.powder.types import Filename, NeXusDetectorName, RawDetector, SampleRun @pytest.fixture(scope="module") @@ -180,6 +180,6 @@ def test_geant4_in_pipeline(file_path, file): NeXusDetectorName: NeXusDetectorName("mantle"), }, ) - detector = pipeline.compute(RawDetectorData[SampleRun]) + detector = pipeline.compute(RawDetector[SampleRun]) expected = load_geant4_csv(file)["instrument"]["mantle"]["events"] sc.testing.assert_identical(detector, expected) diff --git a/tests/dream/io/nexus_test.py b/tests/dream/io/nexus_test.py index 2e684279..8c7e8b9d 100644 --- a/tests/dream/io/nexus_test.py +++ b/tests/dream/io/nexus_test.py @@ -8,8 +8,11 @@ from ess.dream import nexus from ess.powder.types import ( Filename, + Monitor1, NeXusDetectorName, - RawDetectorData, + NeXusMonitorName, + RawDetector, + RawMonitor, ReducibleDetectorData, SampleRun, ) @@ -20,15 +23,7 @@ @pytest.fixture() def providers(): - return ( - nexus.dummy_load_sample, - nexus.load_nexus_source, - nexus.load_nexus_detector, - nexus.get_sample_position, - nexus.get_source_position, - nexus.get_detector_data, - nexus.patch_detector_data, - ) + return (*nexus.providers, nexus.dummy_load_sample) @pytest.fixture( @@ -50,7 +45,7 @@ def params(request): def test_can_load_nexus_detector_data(providers, params): pipeline = sciline.Pipeline(params=params, providers=providers) - result = pipeline.compute(RawDetectorData[SampleRun]) + result = pipeline.compute(RawDetector[SampleRun]) assert ( set(result.dims) == hr_sans_dims if params[NeXusDetectorName] @@ -60,6 +55,17 @@ def test_can_load_nexus_detector_data(providers, params): ) else bank_dims ) + assert result.bins.size().sum().value == 0 + + +def test_can_load_nexus_monitor_data(providers): + pipeline = sciline.Pipeline(providers=providers) + pipeline[Filename[SampleRun]] = dream.data.get_path( + 'DREAM_nexus_sorted-2023-12-07.nxs' + ) + pipeline[NeXusMonitorName[Monitor1]] = 'monitor_cave' + result = pipeline.compute(RawMonitor[SampleRun, Monitor1]) + assert result.bins.size().sum().value == 0 def test_load_fails_with_bad_detector_name(providers): @@ -69,10 +75,10 @@ def test_load_fails_with_bad_detector_name(providers): } pipeline = sciline.Pipeline(params=params, providers=providers) with pytest.raises(KeyError, match='bad_detector'): - pipeline.compute(RawDetectorData[SampleRun]) + pipeline.compute(RawDetector[SampleRun]) -def test_patch_nexus_detector_data(providers, params): +def test_assemble_nexus_detector_data(providers, params): pipeline = sciline.Pipeline(params=params, providers=providers) result = pipeline.compute(ReducibleDetectorData[SampleRun]) assert ( From 8a38782a25f94fcb23bc02e9c9d427de3cb3ad78 Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Wed, 7 Aug 2024 14:08:11 +0200 Subject: [PATCH 2/7] Cleanup --- src/ess/dream/io/nexus.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ess/dream/io/nexus.py b/src/ess/dream/io/nexus.py index 96d7bc29..6e248f5b 100644 --- a/src/ess/dream/io/nexus.py +++ b/src/ess/dream/io/nexus.py @@ -120,9 +120,6 @@ def load_nexus_monitor( # groups so that does not work. Instead, skip event loading and create empty dummy. definitions = snx.base_definitions() definitions["NXmonitor"] = NXmonitor_no_events - # TODO There is a another problem with the DREAM files: - # Transformaiton chains depend on transformations outside the current group, so - # loading a monitor in an isolated manner is not possible with warnings.catch_warnings(): warnings.filterwarnings( "ignore", From 84db083cd22ab36a231ee0a5be8034d2071e890d Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Wed, 7 Aug 2024 14:25:22 +0200 Subject: [PATCH 3/7] Move common NeXus stuff to ess.powder.nexus --- src/ess/dream/io/nexus.py | 233 +------------------------------- src/ess/powder/__init__.py | 2 + src/ess/powder/nexus.py | 251 +++++++++++++++++++++++++++++++++++ src/ess/powder/types.py | 2 + tests/dream/io/nexus_test.py | 4 +- 5 files changed, 262 insertions(+), 230 deletions(-) create mode 100644 src/ess/powder/nexus.py diff --git a/src/ess/dream/io/nexus.py b/src/ess/dream/io/nexus.py index 6e248f5b..942ce438 100644 --- a/src/ess/dream/io/nexus.py +++ b/src/ess/dream/io/nexus.py @@ -13,32 +13,7 @@ but it is not possible to reshape the data into all the logical dimensions. """ -import warnings -from typing import Any - -import scipp as sc -import scippnexus as snx -from ess.reduce import nexus - -from ess.powder.types import ( - DetectorEventData, - Filename, - MonitorEventData, - MonitorType, - NeXusDetector, - NeXusDetectorName, - NeXusMonitor, - NeXusMonitorName, - RawDetector, - RawMonitor, - RawMonitorData, - RawSample, - RawSource, - ReducibleDetectorData, - RunType, - SamplePosition, - SourcePosition, -) +from ess import powder DETECTOR_BANK_SIZES = { "endcap_backward_detector": { @@ -76,209 +51,11 @@ } -def load_nexus_sample(file_path: Filename[RunType]) -> RawSample[RunType]: - return RawSample[RunType](nexus.load_sample(file_path)) - - -def dummy_load_sample(file_path: Filename[RunType]) -> RawSample[RunType]: - """ - In test files there is not always a sample, so we need a dummy. - """ - return RawSample[RunType]( - sc.DataGroup({'position': sc.vector(value=[0, 0, 0], unit='m')}) - ) - - -def load_nexus_source(file_path: Filename[RunType]) -> RawSource[RunType]: - return RawSource[RunType](nexus.load_source(file_path)) - - -def load_nexus_detector( - file_path: Filename[RunType], detector_name: NeXusDetectorName -) -> NeXusDetector[RunType]: - definitions = snx.base_definitions() - definitions["NXdetector"] = FilteredDetector - # Events will be loaded later. Should we set something else as data instead, or - # use different NeXus definitions to completely bypass the (empty) event load? - dg = nexus.load_detector( - file_path=file_path, - detector_name=detector_name, - selection={'event_time_zero': slice(0, 0)}, - definitions=definitions, - ) - # The name is required later, e.g., for determining logical detector shape - dg['detector_name'] = detector_name - return NeXusDetector[RunType](dg) - - -def load_nexus_monitor( - file_path: Filename[RunType], monitor_name: NeXusMonitorName[MonitorType] -) -> NeXusMonitor[RunType, MonitorType]: - # It would be simpler to use something like - # selection={'event_time_zero': slice(0, 0)}, - # to avoid loading events, but currently we have files with empty NXevent_data - # groups so that does not work. Instead, skip event loading and create empty dummy. - definitions = snx.base_definitions() - definitions["NXmonitor"] = NXmonitor_no_events - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - category=UserWarning, - message="Failed to load", - ) - monitor = nexus.load_monitor( - file_path=file_path, - monitor_name=monitor_name, - definitions=definitions, - ) - empty_events = sc.DataArray( - sc.empty(dims=['event'], shape=[0], dtype='float32', unit='counts'), - coords={'event_time_offset': sc.array(dims=['event'], values=[], unit='ns')}, - ) - monitor[f'{monitor_name}_events'] = sc.DataArray( - sc.bins( - dim='event', - data=empty_events, - begin=sc.empty(dims=['event_time_zero'], shape=[0], unit=None), - ), - coords={ - 'event_time_zero': sc.datetimes( - dims=['event_time_zero'], values=[], unit='ns' - ) - }, - ) - return NeXusMonitor[RunType, MonitorType](monitor) - - -def get_source_position( - raw_source: RawSource[RunType], -) -> SourcePosition[RunType]: - return SourcePosition[RunType](raw_source["position"]) - - -def get_sample_position( - raw_sample: RawSample[RunType], -) -> SamplePosition[RunType]: - return SamplePosition[RunType](raw_sample["position"]) - - -def get_detector_data( - detector: NeXusDetector[RunType], -) -> RawDetector[RunType]: - da = nexus.extract_detector_data(detector) - if (sizes := DETECTOR_BANK_SIZES.get(detector['detector_name'])) is not None: - da = da.fold(dim="detector_number", sizes=sizes) - return RawDetector[RunType](da) - - -def get_monitor_data( - monitor: NeXusMonitor[RunType, MonitorType], - source_position: SourcePosition[RunType], -) -> RawMonitor[RunType, MonitorType]: - return RawMonitor[RunType, MonitorType]( - nexus.extract_monitor_data(monitor).assign_coords( - position=monitor['position'], source_position=source_position - ) - ) - - -def assemble_detector_data( - detector: RawDetector[RunType], - event_data: DetectorEventData[RunType], - source_position: SourcePosition[RunType], - sample_position: SamplePosition[RunType], -) -> ReducibleDetectorData[RunType]: - """ - Assemble a detector data object with source and sample positions and event data. - Also adds variances to the event data if they are missing. - """ - grouped = nexus.group_event_data( - event_data=event_data, detector_number=detector.coords['detector_number'] - ) - detector.data = grouped.data - return ReducibleDetectorData[RunType]( - _add_variances(da=detector).assign_coords( - source_position=source_position, sample_position=sample_position - ) - ) - - -def assemble_monitor_data( - monitor_data: RawMonitor[RunType, MonitorType], - event_data: MonitorEventData[RunType, MonitorType], -) -> RawMonitorData[RunType, MonitorType]: - meta = monitor_data.drop_coords('event_time_zero') - da = event_data.assign_coords(meta.coords).assign_masks(meta.masks) - return RawMonitorData[RunType, MonitorType](_add_variances(da=da)) - - -def _skip( - _: str, obj: snx.Field | snx.Group, classes: tuple[snx.NXobject, ...] -) -> bool: - return isinstance(obj, snx.Group) and (obj.nx_class in classes) - - -class FilteredDetector(snx.NXdetector): - def __init__( - self, attrs: dict[str, Any], children: dict[str, snx.Field | snx.Group] - ): - children = { - name: child - for name, child in children.items() - if not _skip(name, child, classes=(snx.NXoff_geometry,)) - } - super().__init__(attrs=attrs, children=children) - - -class NXmonitor_no_events(snx.NXmonitor): - def __init__( - self, attrs: dict[str, Any], children: dict[str, snx.Field | snx.Group] - ): - children = { - name: child - for name, child in children.items() - if not _skip(name, child, classes=(snx.NXevent_data,)) - } - super().__init__(attrs=attrs, children=children) - - -def load_detector_event_data( - file_path: Filename[RunType], detector_name: NeXusDetectorName -) -> DetectorEventData[RunType]: - da = nexus.load_event_data(file_path=file_path, component_name=detector_name) - return DetectorEventData[RunType](da) - - -def load_monitor_event_data( - file_path: Filename[RunType], monitor_name: NeXusMonitorName[MonitorType] -) -> MonitorEventData[RunType, MonitorType]: - da = nexus.load_event_data(file_path=file_path, component_name=monitor_name) - return MonitorEventData[RunType, MonitorType](da) - - -def _add_variances(da: sc.DataArray) -> sc.DataArray: - out = da.copy(deep=False) - if out.bins is not None: - content = out.bins.constituents['data'] - if content.variances is None: - content.variances = content.values - return out +def dream_detector_bank_sizes() -> powder.types.DetectorBankSizes | None: + return powder.types.DetectorBankSizes(DETECTOR_BANK_SIZES) -providers = ( - assemble_detector_data, - assemble_monitor_data, - get_detector_data, - get_monitor_data, - get_sample_position, - get_source_position, - load_detector_event_data, - load_monitor_event_data, - load_nexus_detector, - load_nexus_monitor, - load_nexus_sample, - load_nexus_source, -) +providers = (*powder.nexus.providers, dream_detector_bank_sizes) """ -Providers for loading and processing DREAM NeXus data. +Providers for loading and processing NeXus data. """ diff --git a/src/ess/powder/__init__.py b/src/ess/powder/__init__.py index 50ecb914..38ba4624 100644 --- a/src/ess/powder/__init__.py +++ b/src/ess/powder/__init__.py @@ -15,6 +15,7 @@ smoothing, ) from .masking import with_pixel_mask_filenames +from . import nexus try: __version__ = importlib.metadata.version(__package__ or __name__) @@ -39,6 +40,7 @@ "filtering", "grouping", "masking", + "nexus", "providers", "smoothing", "with_pixel_mask_filenames", diff --git a/src/ess/powder/nexus.py b/src/ess/powder/nexus.py new file mode 100644 index 00000000..6cba8944 --- /dev/null +++ b/src/ess/powder/nexus.py @@ -0,0 +1,251 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2024 Scipp contributors (https://github.com/scipp) + +"""NeXus input/output for DREAM. + +Notes on the detector dimensions (2024-05-22): + +See https://confluence.esss.lu.se/pages/viewpage.action?pageId=462000005 +and the ICD DREAM interface specification for details. + +- The high-resolution and SANS detectors have a very odd numbering scheme. + The scheme attempts to follows some sort of physical ordering in space (x,y,z), + but it is not possible to reshape the data into all the logical dimensions. +""" + +import warnings +from typing import Any + +import scipp as sc +import scippnexus as snx +from ess.reduce import nexus + +from ess.powder.types import ( + DetectorBankSizes, + DetectorEventData, + Filename, + MonitorEventData, + MonitorType, + NeXusDetector, + NeXusDetectorName, + NeXusMonitor, + NeXusMonitorName, + RawDetector, + RawMonitor, + RawMonitorData, + RawSample, + RawSource, + ReducibleDetectorData, + RunType, + SamplePosition, + SourcePosition, +) + + +def load_nexus_sample(file_path: Filename[RunType]) -> RawSample[RunType]: + return RawSample[RunType](nexus.load_sample(file_path)) + + +def dummy_load_sample(file_path: Filename[RunType]) -> RawSample[RunType]: + """ + In test files there is not always a sample, so we need a dummy. + """ + return RawSample[RunType]( + sc.DataGroup({'position': sc.vector(value=[0, 0, 0], unit='m')}) + ) + + +def load_nexus_source(file_path: Filename[RunType]) -> RawSource[RunType]: + return RawSource[RunType](nexus.load_source(file_path)) + + +def load_nexus_detector( + file_path: Filename[RunType], detector_name: NeXusDetectorName +) -> NeXusDetector[RunType]: + definitions = snx.base_definitions() + definitions["NXdetector"] = FilteredDetector + # Events will be loaded later. Should we set something else as data instead, or + # use different NeXus definitions to completely bypass the (empty) event load? + dg = nexus.load_detector( + file_path=file_path, + detector_name=detector_name, + selection={'event_time_zero': slice(0, 0)}, + definitions=definitions, + ) + # The name is required later, e.g., for determining logical detector shape + dg['detector_name'] = detector_name + return NeXusDetector[RunType](dg) + + +def load_nexus_monitor( + file_path: Filename[RunType], monitor_name: NeXusMonitorName[MonitorType] +) -> NeXusMonitor[RunType, MonitorType]: + # It would be simpler to use something like + # selection={'event_time_zero': slice(0, 0)}, + # to avoid loading events, but currently we have files with empty NXevent_data + # groups so that does not work. Instead, skip event loading and create empty dummy. + definitions = snx.base_definitions() + definitions["NXmonitor"] = NXmonitor_no_events + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + category=UserWarning, + message="Failed to load", + ) + monitor = nexus.load_monitor( + file_path=file_path, + monitor_name=monitor_name, + definitions=definitions, + ) + empty_events = sc.DataArray( + sc.empty(dims=['event'], shape=[0], dtype='float32', unit='counts'), + coords={'event_time_offset': sc.array(dims=['event'], values=[], unit='ns')}, + ) + monitor[f'{monitor_name}_events'] = sc.DataArray( + sc.bins( + dim='event', + data=empty_events, + begin=sc.empty(dims=['event_time_zero'], shape=[0], unit=None), + ), + coords={ + 'event_time_zero': sc.datetimes( + dims=['event_time_zero'], values=[], unit='ns' + ) + }, + ) + return NeXusMonitor[RunType, MonitorType](monitor) + + +def get_source_position( + raw_source: RawSource[RunType], +) -> SourcePosition[RunType]: + return SourcePosition[RunType](raw_source["position"]) + + +def get_sample_position( + raw_sample: RawSample[RunType], +) -> SamplePosition[RunType]: + return SamplePosition[RunType](raw_sample["position"]) + + +def get_detector_data( + detector: NeXusDetector[RunType], + bank_sizes: DetectorBankSizes | None = None, +) -> RawDetector[RunType]: + da = nexus.extract_detector_data(detector) + if (sizes := (bank_sizes or {}).get(detector['detector_name'])) is not None: + da = da.fold(dim="detector_number", sizes=sizes) + return RawDetector[RunType](da) + + +def get_monitor_data( + monitor: NeXusMonitor[RunType, MonitorType], + source_position: SourcePosition[RunType], +) -> RawMonitor[RunType, MonitorType]: + return RawMonitor[RunType, MonitorType]( + nexus.extract_monitor_data(monitor).assign_coords( + position=monitor['position'], source_position=source_position + ) + ) + + +def assemble_detector_data( + detector: RawDetector[RunType], + event_data: DetectorEventData[RunType], + source_position: SourcePosition[RunType], + sample_position: SamplePosition[RunType], +) -> ReducibleDetectorData[RunType]: + """ + Assemble a detector data object with source and sample positions and event data. + Also adds variances to the event data if they are missing. + """ + grouped = nexus.group_event_data( + event_data=event_data, detector_number=detector.coords['detector_number'] + ) + detector.data = grouped.data + return ReducibleDetectorData[RunType]( + _add_variances(da=detector).assign_coords( + source_position=source_position, sample_position=sample_position + ) + ) + + +def assemble_monitor_data( + monitor_data: RawMonitor[RunType, MonitorType], + event_data: MonitorEventData[RunType, MonitorType], +) -> RawMonitorData[RunType, MonitorType]: + meta = monitor_data.drop_coords('event_time_zero') + da = event_data.assign_coords(meta.coords).assign_masks(meta.masks) + return RawMonitorData[RunType, MonitorType](_add_variances(da=da)) + + +def _skip( + _: str, obj: snx.Field | snx.Group, classes: tuple[snx.NXobject, ...] +) -> bool: + return isinstance(obj, snx.Group) and (obj.nx_class in classes) + + +class FilteredDetector(snx.NXdetector): + def __init__( + self, attrs: dict[str, Any], children: dict[str, snx.Field | snx.Group] + ): + children = { + name: child + for name, child in children.items() + if not _skip(name, child, classes=(snx.NXoff_geometry,)) + } + super().__init__(attrs=attrs, children=children) + + +class NXmonitor_no_events(snx.NXmonitor): + def __init__( + self, attrs: dict[str, Any], children: dict[str, snx.Field | snx.Group] + ): + children = { + name: child + for name, child in children.items() + if not _skip(name, child, classes=(snx.NXevent_data,)) + } + super().__init__(attrs=attrs, children=children) + + +def load_detector_event_data( + file_path: Filename[RunType], detector_name: NeXusDetectorName +) -> DetectorEventData[RunType]: + da = nexus.load_event_data(file_path=file_path, component_name=detector_name) + return DetectorEventData[RunType](da) + + +def load_monitor_event_data( + file_path: Filename[RunType], monitor_name: NeXusMonitorName[MonitorType] +) -> MonitorEventData[RunType, MonitorType]: + da = nexus.load_event_data(file_path=file_path, component_name=monitor_name) + return MonitorEventData[RunType, MonitorType](da) + + +def _add_variances(da: sc.DataArray) -> sc.DataArray: + out = da.copy(deep=False) + if out.bins is not None: + content = out.bins.constituents['data'] + if content.variances is None: + content.variances = content.values + return out + + +providers = ( + assemble_detector_data, + assemble_monitor_data, + get_detector_data, + get_monitor_data, + get_sample_position, + get_source_position, + load_detector_event_data, + load_monitor_event_data, + load_nexus_detector, + load_nexus_monitor, + load_nexus_sample, + load_nexus_source, +) +""" +Providers for loading and processing NeXus data. +""" diff --git a/src/ess/powder/types.py b/src/ess/powder/types.py index e9adb4de..5697475d 100644 --- a/src/ess/powder/types.py +++ b/src/ess/powder/types.py @@ -38,6 +38,8 @@ # 2 Workflow parameters +DetectorBankSizes = NewType("DetectorBankSizes", dict[str, dict[str, int | Any]]) + CalibrationFilename = NewType("CalibrationFilename", str | None) """Filename of the instrument calibration file.""" diff --git a/tests/dream/io/nexus_test.py b/tests/dream/io/nexus_test.py index 8c7e8b9d..db8c9f25 100644 --- a/tests/dream/io/nexus_test.py +++ b/tests/dream/io/nexus_test.py @@ -2,7 +2,7 @@ # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) import pytest import sciline -from ess import dream +from ess import dream, powder import ess.dream.data # noqa: F401 from ess.dream import nexus @@ -23,7 +23,7 @@ @pytest.fixture() def providers(): - return (*nexus.providers, nexus.dummy_load_sample) + return (*nexus.providers, powder.nexus.dummy_load_sample) @pytest.fixture( From e5a50c551f63318473c56000c159cc1891404a95 Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Wed, 7 Aug 2024 14:28:41 +0200 Subject: [PATCH 4/7] Format --- src/ess/dream/io/nexus.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/ess/dream/io/nexus.py b/src/ess/dream/io/nexus.py index 942ce438..3e9755a3 100644 --- a/src/ess/dream/io/nexus.py +++ b/src/ess/dream/io/nexus.py @@ -37,16 +37,10 @@ "strip": 256, "counter": 2, }, - "high_resolution_detector": { - "strip": 32, - "other": -1, - }, + "high_resolution_detector": {"strip": 32, "other": -1}, "sans_detector": lambda x: x.fold( dim="detector_number", - sizes={ - "strip": 32, - "other": -1, - }, + sizes={"strip": 32, "other": -1}, ), } From 4fe18da28f951adfc24beb12145e86588e3dcbb5 Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Thu, 8 Aug 2024 11:51:14 +0200 Subject: [PATCH 5/7] Don't make empty bins, add docstring --- src/ess/powder/nexus.py | 67 +++++++++++++++++++----------------- tests/dream/io/nexus_test.py | 6 ++-- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/ess/powder/nexus.py b/src/ess/powder/nexus.py index 6cba8944..479c09d0 100644 --- a/src/ess/powder/nexus.py +++ b/src/ess/powder/nexus.py @@ -13,7 +13,6 @@ but it is not possible to reshape the data into all the logical dimensions. """ -import warnings from typing import Any import scipp as sc @@ -62,14 +61,31 @@ def load_nexus_source(file_path: Filename[RunType]) -> RawSource[RunType]: def load_nexus_detector( file_path: Filename[RunType], detector_name: NeXusDetectorName ) -> NeXusDetector[RunType]: + """ + Load detector from NeXus, but with event data replaced by placeholders. + + Currently the placeholder is the detector number, but this may change in the future. + + The returned object is a scipp.DataGroup, as it may contain additional information + about the detector that cannot be represented as a single scipp.DataArray. Most + downstream code will only be interested in the contained scipp.DataArray so this + needs to be extracted. However, other processing steps may require the additional + information, so it is kept in the DataGroup. + + Loading thus proceeds in three steps: + + 1. This function loads the detector, but replaces the event data with placeholders. + 2. :py:func:`get_detector_data` drops the additional information, returning only + the contained scipp.DataArray, reshaped to the logical detector shape. + This will generally contain coordinates as well as pixel masks. + 3. :py:func:`assemble_detector_data` replaces placeholder data values with the + event data, and adds source and sample positions. + """ definitions = snx.base_definitions() definitions["NXdetector"] = FilteredDetector - # Events will be loaded later. Should we set something else as data instead, or - # use different NeXus definitions to completely bypass the (empty) event load? dg = nexus.load_detector( file_path=file_path, detector_name=detector_name, - selection={'event_time_zero': slice(0, 0)}, definitions=definitions, ) # The name is required later, e.g., for determining logical detector shape @@ -86,32 +102,8 @@ def load_nexus_monitor( # groups so that does not work. Instead, skip event loading and create empty dummy. definitions = snx.base_definitions() definitions["NXmonitor"] = NXmonitor_no_events - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - category=UserWarning, - message="Failed to load", - ) - monitor = nexus.load_monitor( - file_path=file_path, - monitor_name=monitor_name, - definitions=definitions, - ) - empty_events = sc.DataArray( - sc.empty(dims=['event'], shape=[0], dtype='float32', unit='counts'), - coords={'event_time_offset': sc.array(dims=['event'], values=[], unit='ns')}, - ) - monitor[f'{monitor_name}_events'] = sc.DataArray( - sc.bins( - dim='event', - data=empty_events, - begin=sc.empty(dims=['event_time_zero'], shape=[0], unit=None), - ), - coords={ - 'event_time_zero': sc.datetimes( - dims=['event_time_zero'], values=[], unit='ns' - ) - }, + monitor = nexus.load_monitor( + file_path=file_path, monitor_name=monitor_name, definitions=definitions ) return NeXusMonitor[RunType, MonitorType](monitor) @@ -192,8 +184,9 @@ def __init__( children = { name: child for name, child in children.items() - if not _skip(name, child, classes=(snx.NXoff_geometry,)) + if not _skip(name, child, classes=(snx.NXoff_geometry, snx.NXevent_data)) } + children['data'] = children['detector_number'] super().__init__(attrs=attrs, children=children) @@ -206,6 +199,18 @@ def __init__( for name, child in children.items() if not _skip(name, child, classes=(snx.NXevent_data,)) } + + class DummyField: + def __init__(self): + self.attrs = {} + self.sizes = {'event_time_zero': 0} + self.dims = ('event_time_zero',) + self.shape = (0,) + + def __getitem__(self, key: Any) -> sc.Variable: + return sc.empty(dims=self.dims, shape=self.shape, unit=None) + + children['data'] = DummyField() super().__init__(attrs=attrs, children=children) diff --git a/tests/dream/io/nexus_test.py b/tests/dream/io/nexus_test.py index db8c9f25..7b28e4f1 100644 --- a/tests/dream/io/nexus_test.py +++ b/tests/dream/io/nexus_test.py @@ -2,6 +2,7 @@ # Copyright (c) 2023 Scipp contributors (https://github.com/scipp) import pytest import sciline +import scipp as sc from ess import dream, powder import ess.dream.data # noqa: F401 @@ -55,7 +56,8 @@ def test_can_load_nexus_detector_data(providers, params): ) else bank_dims ) - assert result.bins.size().sum().value == 0 + + assert sc.identical(result.data, result.coords['detector_number']) def test_can_load_nexus_monitor_data(providers): @@ -65,7 +67,7 @@ def test_can_load_nexus_monitor_data(providers): ) pipeline[NeXusMonitorName[Monitor1]] = 'monitor_cave' result = pipeline.compute(RawMonitor[SampleRun, Monitor1]) - assert result.bins.size().sum().value == 0 + assert result.sizes == {'event_time_zero': 0} def test_load_fails_with_bad_detector_name(providers): From baf4ddd96195115da96c9216947e38cdf764d8ce Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Thu, 8 Aug 2024 13:03:20 +0200 Subject: [PATCH 6/7] Cleanup and docs --- src/ess/powder/nexus.py | 149 ++++++++++++++++++++++++---------------- 1 file changed, 91 insertions(+), 58 deletions(-) diff --git a/src/ess/powder/nexus.py b/src/ess/powder/nexus.py index 479c09d0..46728900 100644 --- a/src/ess/powder/nexus.py +++ b/src/ess/powder/nexus.py @@ -1,17 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2024 Scipp contributors (https://github.com/scipp) -"""NeXus input/output for DREAM. - -Notes on the detector dimensions (2024-05-22): - -See https://confluence.esss.lu.se/pages/viewpage.action?pageId=462000005 -and the ICD DREAM interface specification for details. - -- The high-resolution and SANS detectors have a very odd numbering scheme. - The scheme attempts to follows some sort of physical ordering in space (x,y,z), - but it is not possible to reshape the data into all the logical dimensions. -""" +"""NeXus input/output for ESS powder reduction.""" from typing import Any @@ -75,14 +65,14 @@ def load_nexus_detector( Loading thus proceeds in three steps: 1. This function loads the detector, but replaces the event data with placeholders. - 2. :py:func:`get_detector_data` drops the additional information, returning only + 2. :py:func:`get_detector_array` drops the additional information, returning only the contained scipp.DataArray, reshaped to the logical detector shape. This will generally contain coordinates as well as pixel masks. 3. :py:func:`assemble_detector_data` replaces placeholder data values with the event data, and adds source and sample positions. """ definitions = snx.base_definitions() - definitions["NXdetector"] = FilteredDetector + definitions["NXdetector"] = _StrippedDetector dg = nexus.load_detector( file_path=file_path, detector_name=detector_name, @@ -96,12 +86,28 @@ def load_nexus_detector( def load_nexus_monitor( file_path: Filename[RunType], monitor_name: NeXusMonitorName[MonitorType] ) -> NeXusMonitor[RunType, MonitorType]: - # It would be simpler to use something like - # selection={'event_time_zero': slice(0, 0)}, - # to avoid loading events, but currently we have files with empty NXevent_data - # groups so that does not work. Instead, skip event loading and create empty dummy. + """ + Load monitor from NeXus, but with event data replaced by placeholders. + + Currently the placeholder is a size-0 array, but this may change in the future. + + The returned object is a scipp.DataGroup, as it may contain additional information + about the monitor that cannot be represented as a single scipp.DataArray. Most + downstream code will only be interested in the contained scipp.DataArray so this + needs to be extracted. However, other processing steps may require the additional + information, so it is kept in the DataGroup. + + Loading thus proceeds in three steps: + + 1. This function loads the monitor, but replaces the event data with placeholders. + 2. :py:func:`get_monitor_array` drops the additional information, returning only + the contained scipp.DataArray. + This will generally contain coordinates as well as pixel masks. + 3. :py:func:`assemble_monitor_data` replaces placeholder data values with the + event data, and adds source and sample positions. + """ definitions = snx.base_definitions() - definitions["NXmonitor"] = NXmonitor_no_events + definitions["NXmonitor"] = _StrippedMonitor monitor = nexus.load_monitor( file_path=file_path, monitor_name=monitor_name, definitions=definitions ) @@ -120,20 +126,34 @@ def get_sample_position( return SamplePosition[RunType](raw_sample["position"]) -def get_detector_data( +def get_detector_signal_array( detector: NeXusDetector[RunType], bank_sizes: DetectorBankSizes | None = None, ) -> RawDetector[RunType]: + """ + Extract the data array corresponding to a detector's signal field. + + The returned data array includes coords and masks pertaining directly to the + signal values array, but not additional information about the detector. The + data array is reshaped to the logical detector shape, which by folding the data + array along the detector_number dimension. + """ da = nexus.extract_detector_data(detector) if (sizes := (bank_sizes or {}).get(detector['detector_name'])) is not None: da = da.fold(dim="detector_number", sizes=sizes) return RawDetector[RunType](da) -def get_monitor_data( +def get_monitor_signal_array( monitor: NeXusMonitor[RunType, MonitorType], source_position: SourcePosition[RunType], ) -> RawMonitor[RunType, MonitorType]: + """ + Extract the data array corresponding to a monitor's signal field. + + The returned data array includes coords pertaining directly to the + signal values array, but not additional information about the monitor. + """ return RawMonitor[RunType, MonitorType]( nexus.extract_monitor_data(monitor).assign_coords( position=monitor['position'], source_position=source_position @@ -148,69 +168,82 @@ def assemble_detector_data( sample_position: SamplePosition[RunType], ) -> ReducibleDetectorData[RunType]: """ - Assemble a detector data object with source and sample positions and event data. + Assemble a detector data array with event data and source- and sample-position. + Also adds variances to the event data if they are missing. """ grouped = nexus.group_event_data( event_data=event_data, detector_number=detector.coords['detector_number'] ) - detector.data = grouped.data return ReducibleDetectorData[RunType]( - _add_variances(da=detector).assign_coords( - source_position=source_position, sample_position=sample_position - ) + _add_variances(grouped) + .assign_coords(source_position=source_position, sample_position=sample_position) + .assign_coords(detector.coords) + .assign_masks(detector.masks) ) def assemble_monitor_data( - monitor_data: RawMonitor[RunType, MonitorType], + monitor: RawMonitor[RunType, MonitorType], event_data: MonitorEventData[RunType, MonitorType], ) -> RawMonitorData[RunType, MonitorType]: - meta = monitor_data.drop_coords('event_time_zero') - da = event_data.assign_coords(meta.coords).assign_masks(meta.masks) + """ + Assemble a monitor data array with event data. + + Also adds variances to the event data if they are missing. + """ + da = event_data.assign_coords(monitor.coords).assign_masks(monitor.masks) return RawMonitorData[RunType, MonitorType](_add_variances(da=da)) -def _skip( - _: str, obj: snx.Field | snx.Group, classes: tuple[snx.NXobject, ...] -) -> bool: - return isinstance(obj, snx.Group) and (obj.nx_class in classes) +def _drop( + children: dict[str, snx.Field | snx.Group], classes: tuple[snx.NXobject, ...] +) -> dict[str, snx.Field | snx.Group]: + return { + name: child + for name, child in children.items() + if not (isinstance(child, snx.Group) and (child.nx_class in classes)) + } + +class _StrippedDetector(snx.NXdetector): + """Detector definition without large geometry or event data for ScippNexus. + + Drops NXoff_geometry and NXevent_data groups, data is replaced by detector_number. + """ -class FilteredDetector(snx.NXdetector): def __init__( self, attrs: dict[str, Any], children: dict[str, snx.Field | snx.Group] ): - children = { - name: child - for name, child in children.items() - if not _skip(name, child, classes=(snx.NXoff_geometry, snx.NXevent_data)) - } + children = _drop(children, (snx.NXoff_geometry, snx.NXevent_data)) children['data'] = children['detector_number'] super().__init__(attrs=attrs, children=children) -class NXmonitor_no_events(snx.NXmonitor): +class _DummyField: + """Dummy field that can replace snx.Field in NXmonitor.""" + + def __init__(self): + self.attrs = {} + self.sizes = {'event_time_zero': 0} + self.dims = ('event_time_zero',) + self.shape = (0,) + + def __getitem__(self, key: Any) -> sc.Variable: + return sc.empty(dims=self.dims, shape=self.shape, unit=None) + + +class _StrippedMonitor(snx.NXmonitor): + """Monitor definition without event data for ScippNexus. + + Drops NXevent_data group, data is replaced by a dummy field. + """ + def __init__( self, attrs: dict[str, Any], children: dict[str, snx.Field | snx.Group] ): - children = { - name: child - for name, child in children.items() - if not _skip(name, child, classes=(snx.NXevent_data,)) - } - - class DummyField: - def __init__(self): - self.attrs = {} - self.sizes = {'event_time_zero': 0} - self.dims = ('event_time_zero',) - self.shape = (0,) - - def __getitem__(self, key: Any) -> sc.Variable: - return sc.empty(dims=self.dims, shape=self.shape, unit=None) - - children['data'] = DummyField() + children = _drop(children, (snx.NXevent_data,)) + children['data'] = _DummyField() super().__init__(attrs=attrs, children=children) @@ -240,8 +273,8 @@ def _add_variances(da: sc.DataArray) -> sc.DataArray: providers = ( assemble_detector_data, assemble_monitor_data, - get_detector_data, - get_monitor_data, + get_detector_signal_array, + get_monitor_signal_array, get_sample_position, get_source_position, load_detector_event_data, From 78b985a69834fc7129023e3e478423f56680d38d Mon Sep 17 00:00:00 2001 From: Simon Heybrock Date: Thu, 8 Aug 2024 14:41:16 +0200 Subject: [PATCH 7/7] Do not use make_binned --- tests/powder/filtering_test.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/powder/filtering_test.py b/tests/powder/filtering_test.py index 95661cae..a32968a0 100644 --- a/tests/powder/filtering_test.py +++ b/tests/powder/filtering_test.py @@ -39,12 +39,8 @@ def make_data_with_pulse_time(rng, n_event) -> sc.DataArray: ), }, ) - return sc.binning.make_binned( - events, - edges=[ - sc.array(dims=['tof'], values=[10, 500, 1000], unit='us', dtype='int64') - ], - groups=[sc.arange('spectrum', 0, 10, unit=None, dtype='int64')], + return events.group(sc.arange('spectrum', 0, 10, unit=None, dtype='int64')).bin( + tof=sc.array(dims=['tof'], values=[10, 500, 1000], unit='us', dtype='int64') )