Skip to content
68 changes: 57 additions & 11 deletions src/ess/reduce/nexus/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,11 @@ def get_calibrated_detector(
# If the NXdetector in the file is not 1-D, we want to match the order of dims.
# zip_pixel_offsets otherwise yields a vector with dimensions in the order given
# by the x/y/z offsets.
offsets = snx.zip_pixel_offsets(da.coords).transpose(da.dims).copy()
offsets = snx.zip_pixel_offsets(da.coords)
# Get the dims in the order of the detector data array, but filter out dims that
# don't exist in the offsets (e.g. the detector data may have a 'time' dimension).
dims = [dim for dim in da.dims if dim in offsets.dims]
offsets = offsets.transpose(dims).copy()
# We use the unit of the offsets as this is likely what the user expects.
if transform.value.unit is not None and transform.value.unit != '':
transform_value = transform.value.to(unit=offsets.unit)
Expand All @@ -399,7 +403,7 @@ def get_calibrated_detector(

def assemble_detector_data(
detector: EmptyDetector[RunType],
event_data: NeXusData[snx.NXdetector, RunType],
neutron_data: NeXusData[snx.NXdetector, RunType],
) -> RawDetector[RunType]:
"""
Assemble a detector data array with event data.
Expand All @@ -410,14 +414,15 @@ def assemble_detector_data(
----------
detector:
Calibrated detector data array.
event_data:
Event data array.
neutron_data:
Neutron data array (events or histogram).
"""
grouped = nexus.group_event_data(
event_data=event_data, detector_number=detector.coords['detector_number']
)
if neutron_data.bins is not None:
neutron_data = nexus.group_event_data(
event_data=neutron_data, detector_number=detector.coords['detector_number']
)
return RawDetector[RunType](
_add_variances(grouped)
_add_variances(neutron_data)
.assign_coords(detector.coords)
.assign_masks(detector.masks)
)
Expand Down Expand Up @@ -504,6 +509,19 @@ def _drop(
}


class _EmptyField:
"""Empty field that can replace a missing detector_number in NXdetector."""

def __init__(self, sizes: dict[str, int]):
self.attrs = {}
self.sizes = sizes.copy()
self.dims = tuple(sizes.keys())
self.shape = tuple(sizes.values())

def __getitem__(self, key: Any) -> sc.Variable:
return sc.zeros(dims=self.dims, shape=self.shape, unit=None, dtype='int32')


class _StrippedDetector(snx.NXdetector):
"""Detector definition without large geometry or event data for ScippNexus.

Expand All @@ -513,8 +531,36 @@ class _StrippedDetector(snx.NXdetector):
def __init__(
self, attrs: dict[str, Any], children: dict[str, snx.Field | snx.Group]
):
children = _drop(children, (snx.NXoff_geometry, snx.NXevent_data))
children['data'] = children['detector_number']
if 'detector_number' in children:
data = children['detector_number']
else:
# We get the 'data' sizes before the NXdata is dropped
if 'data' not in children:
raise KeyError(
"StrippedDetector: Cannot determine shape of the detector. "
"No 'detector_number' was found, and the 'data' entry is missing."
)
if 'value' not in children['data']:
raise KeyError(
"StrippedDetector: Cannot determine shape of the detector. "
"The 'data' entry has no 'value'."
)
# We drop any time-related dimension from the data sizes, as they are not
# relevant for the detector geometry/shape.
data = _EmptyField(
sizes={
dim: size
for dim, size in children['data']['value'].sizes.items()
if dim not in ('time', 'frame_time')
}
)

children = _drop(
children, (snx.NXoff_geometry, snx.NXevent_data, snx.NXdata, snx.NXlog)
)

children['data'] = data

super().__init__(attrs=attrs, children=children)


Expand All @@ -528,7 +574,7 @@ def __init__(self, dim: str):
self.shape = (0,)

def __getitem__(self, key: Any) -> sc.Variable:
return sc.empty(dims=self.dims, shape=self.shape, unit=None)
return sc.zeros(dims=self.dims, shape=self.shape, unit=None, dtype='int32')


class _StrippedMonitor(snx.NXmonitor):
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ def tbl_registry() -> Registry:
return make_registry(
'ess/tbl',
files={
"857127_00000112_small.hdf": "md5:6f3059e0e5e111a2a8f1b368f24c6f93",
"857127_00000112_small.hdf": "md5:0db89493b859dbb2f7354c3711ed7fbd",
},
version='1',
version='2',
)


Expand Down
26 changes: 26 additions & 0 deletions tests/nexus/workflow_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
BackgroundRun,
Beamline,
EmptyBeamRun,
EmptyDetector,
Filename,
FrameMonitor0,
FrameMonitor1,
Expand Down Expand Up @@ -585,6 +586,31 @@ def test_load_detector_workflow(loki_tutorial_sample_run_60250: Path) -> None:
assert da.dims == ('detector_number',)


def test_load_histogram_detector_workflow(tbl_commissioning_orca_file: Path) -> None:
wf = LoadDetectorWorkflow(run_types=[SampleRun], monitor_types=[])
wf[Filename[SampleRun]] = tbl_commissioning_orca_file
wf[NeXusName[snx.NXdetector]] = 'orca_detector'
da = wf.compute(RawDetector[SampleRun])
assert 'position' in da.coords
assert da.bins is None
assert 'time' in da.dims
assert da.ndim == 3


def test_load_empty_histogram_detector_workflow(
tbl_commissioning_orca_file: Path,
) -> None:
wf = LoadDetectorWorkflow(run_types=[SampleRun], monitor_types=[])
wf[Filename[SampleRun]] = tbl_commissioning_orca_file
wf[NeXusName[snx.NXdetector]] = 'orca_detector'
da = wf.compute(EmptyDetector[SampleRun])
assert 'position' in da.coords
assert da.bins is None
# The empty detector has no time dimension, only the dimensions of the geometry
assert 'time' not in da.dims
assert da.ndim == 2


@pytest.mark.parametrize('preopen', [True, False])
def test_generic_nexus_workflow(
preopen: bool, loki_tutorial_sample_run_60250: Path
Expand Down
Loading