Skip to content

Commit e85642c

Browse files
authored
Merge pull request #244 from scipp/histogram-monitors
Histogram monitors
2 parents c0e8cf6 + 9d8d848 commit e85642c

File tree

4 files changed

+177
-10
lines changed

4 files changed

+177
-10
lines changed

src/ess/reduce/data.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ def get_path(self, name: str, unzip: bool = False) -> str:
4040
)
4141

4242

43+
_dream_registry = Registry(
44+
instrument='dream',
45+
files={
46+
"TEST_977695_00068064.hdf": "md5:9e6ee9ec70d7c5e8c0c93b9e07e8949f",
47+
},
48+
version='2',
49+
)
50+
51+
4352
_loki_registry = Registry(
4453
instrument='loki',
4554
files={
@@ -94,3 +103,11 @@ def loki_tutorial_background_run_60393() -> str:
94103
def loki_tutorial_sample_transmission_run() -> str:
95104
"""Sample transmission run (sample + sample holder/can + transmission monitor)."""
96105
return _loki_registry.get_path('60394-2022-02-28_2215.nxs')
106+
107+
108+
def dream_coda_test_file() -> str:
109+
"""CODA file for DREAM where most pulses have been removed.
110+
111+
See ``tools/shrink_nexus.py``.
112+
"""
113+
return _dream_registry.get_path('TEST_977695_00068064.hdf')

src/ess/reduce/nexus/workflow.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -560,10 +560,10 @@ def __init__(
560560
class _DummyField:
561561
"""Dummy field that can replace snx.Field in NXmonitor."""
562562

563-
def __init__(self):
563+
def __init__(self, dim: str):
564564
self.attrs = {}
565-
self.sizes = {'event_time_zero': 0}
566-
self.dims = ('event_time_zero',)
565+
self.sizes = {dim: 0}
566+
self.dims = (dim,)
567567
self.shape = (0,)
568568

569569
def __getitem__(self, key: Any) -> sc.Variable:
@@ -573,26 +573,39 @@ def __getitem__(self, key: Any) -> sc.Variable:
573573
class _StrippedMonitor(snx.NXmonitor):
574574
"""Monitor definition without event data for ScippNexus.
575575
576-
Drops NXevent_data group, data is replaced by a dummy field.
576+
Drops NXevent_data and NXdata groups, data is replaced by a dummy field.
577577
"""
578578

579579
def __init__(
580580
self, attrs: dict[str, Any], children: dict[str, snx.Field | snx.Group]
581581
):
582-
children = _drop(children, (snx.NXevent_data,))
583-
children['data'] = _DummyField()
582+
is_dense = snx.NXdata in (
583+
getattr(child, 'nx_class', None) for child in children
584+
)
585+
children = _drop(children, (snx.NXevent_data, snx.NXdata))
586+
children['data'] = _DummyField(dim='time' if is_dense else 'event_time_zero')
584587
super().__init__(attrs=attrs, children=children)
585588

586589

587590
def _add_variances(da: sc.DataArray) -> sc.DataArray:
588591
out = da.copy(deep=False)
589592
if out.bins is not None:
590593
content = out.bins.constituents['data']
591-
if content.variances is None:
592-
content.variances = content.values
594+
content.data = _assign_values_as_variances(content.data)
595+
elif out.variances is None:
596+
out.data = _assign_values_as_variances(out.data)
593597
return out
594598

595599

600+
def _assign_values_as_variances(var: sc.Variable) -> sc.Variable:
601+
try:
602+
var.variances = var.values
603+
except sc.VariancesError:
604+
var = var.to(dtype=sc.DType.float64)
605+
var.variances = var.values
606+
return var
607+
608+
596609
def load_beamline_metadata_from_nexus(file_spec: NeXusFileSpec[SampleRun]) -> Beamline:
597610
"""Load beamline metadata from a sample NeXus file."""
598611
return nexus.load_metadata(file_spec.value, Beamline)

tests/nexus/workflow_test.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,21 @@ def monitor_event_data() -> workflow.NeXusData[FrameMonitor1, SampleRun]:
471471
)
472472

473473

474+
@pytest.fixture
475+
def monitor_histogram_data() -> workflow.NeXusData[FrameMonitor1, SampleRun]:
476+
time = sc.epoch(unit='ns') + sc.arange('time', 1, 6, unit='s').to(unit='ns')
477+
frame_time = sc.arange('frame_time', 12, unit='ms').to(unit='ns')
478+
return workflow.NeXusData[FrameMonitor1, SampleRun](
479+
sc.DataArray(
480+
10.0
481+
* sc.arange('x', 5 * 12, unit='counts').fold(
482+
'x', sizes={'time': 5, 'frame_time': 12}
483+
),
484+
coords={'time': time, 'frame_time': frame_time},
485+
)
486+
)
487+
488+
474489
def test_assemble_monitor_data_adds_events_as_values_and_coords(
475490
calibrated_monitor, monitor_event_data
476491
) -> None:
@@ -482,7 +497,7 @@ def test_assemble_monitor_data_adds_events_as_values_and_coords(
482497
)
483498

484499

485-
def test_assemble_monitor_data_adds_variances_to_weights(
500+
def test_assemble_monitor_data_adds_variances_to_events(
486501
calibrated_monitor, monitor_event_data
487502
) -> None:
488503
monitor_data = workflow.assemble_monitor_data(
@@ -494,6 +509,30 @@ def test_assemble_monitor_data_adds_variances_to_weights(
494509
)
495510

496511

512+
def test_assemble_monitor_data_adds_histogram_as_values_and_coords(
513+
calibrated_monitor, monitor_histogram_data
514+
) -> None:
515+
monitor_data = workflow.assemble_monitor_data(
516+
calibrated_monitor, monitor_histogram_data
517+
)
518+
assert_identical(
519+
monitor_data.drop_coords(tuple(calibrated_monitor.coords)),
520+
monitor_histogram_data,
521+
)
522+
523+
524+
def test_assemble_monitor_data_adds_variances_to_weights(
525+
calibrated_monitor, monitor_histogram_data
526+
) -> None:
527+
monitor_data = workflow.assemble_monitor_data(
528+
calibrated_monitor, monitor_histogram_data
529+
)
530+
assert_identical(
531+
sc.variances(monitor_data.drop_coords(tuple(calibrated_monitor.coords))),
532+
monitor_histogram_data,
533+
)
534+
535+
497536
def test_assemble_monitor_preserves_coords(calibrated_monitor, monitor_event_data):
498537
calibrated_monitor.coords['abc'] = sc.scalar(1.2)
499538
monitor_data = workflow.assemble_monitor_data(
@@ -510,7 +549,7 @@ def test_assemble_monitor_preserves_masks(calibrated_monitor, monitor_event_data
510549
assert 'mymask' in monitor_data.masks
511550

512551

513-
def test_load_monitor_workflow() -> None:
552+
def test_load_event_monitor_workflow() -> None:
514553
wf = LoadMonitorWorkflow(run_types=[SampleRun], monitor_types=[FrameMonitor1])
515554
wf[Filename[SampleRun]] = data.loki_tutorial_sample_run_60250()
516555
wf[NeXusName[FrameMonitor1]] = 'monitor_1'
@@ -519,6 +558,21 @@ def test_load_monitor_workflow() -> None:
519558
assert 'source_position' in da.coords
520559
assert da.bins is not None
521560
assert da.dims == ('event_time_zero',)
561+
assert da.bins.constituents['data'].variances is not None
562+
563+
564+
def test_load_histogram_monitor_workflow() -> None:
565+
wf = LoadMonitorWorkflow(run_types=[SampleRun], monitor_types=[FrameMonitor1])
566+
wf[Filename[SampleRun]] = data.dream_coda_test_file()
567+
wf[NeXusName[FrameMonitor1]] = 'monitor_bunker'
568+
da = wf.compute(MonitorData[SampleRun, FrameMonitor1])
569+
assert 'position' in da.coords
570+
assert 'source_position' in da.coords
571+
assert da.bins is None
572+
assert set(da.dims) == {'time', 'frame_time'}
573+
assert 'time' in da.coords.keys()
574+
assert 'frame_time' in da.coords.keys()
575+
assert da.variances is not None
522576

523577

524578
def test_load_detector_workflow() -> None:

tools/shrink_nexus.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
3+
"""Shrink a nexus file by removing all but the first N pulses."""
4+
5+
import shutil
6+
import subprocess
7+
from pathlib import Path
8+
9+
import h5py as h5
10+
import numpy as np
11+
import scippnexus as snx
12+
13+
N_PULSE = 2
14+
15+
DIR = Path("data/coda")
16+
IN_FNAME = "977695_00068064.hdf"
17+
TMP_FNAME = f"TMP_{IN_FNAME}"
18+
OUT_FNAME = f"TEST_{IN_FNAME}"
19+
20+
21+
def main() -> None:
22+
shutil.copy(DIR / IN_FNAME, DIR / TMP_FNAME)
23+
with snx.File(DIR / TMP_FNAME, 'r') as f:
24+
entry = f['entry']
25+
user_names = list(entry[snx.NXuser])
26+
det_names = list(entry['instrument'][snx.NXdetector])
27+
mon_names = list(entry['instrument'][snx.NXmonitor])
28+
29+
last_times = []
30+
31+
with h5.File(DIR / TMP_FNAME, 'a') as f:
32+
entry = f['entry']
33+
for name in user_names:
34+
del entry[name]
35+
del entry['scripts']
36+
37+
instr = entry['instrument']
38+
for name in det_names:
39+
det = instr[name]
40+
del det['pixel_shape']
41+
42+
event_data_name = next(name for name in det if name.endswith('event_data'))
43+
events = det[event_data_name]
44+
45+
last_times.append(
46+
np.array(
47+
int(events['event_time_zero'][N_PULSE]),
48+
dtype=f'datetime64[{events["event_time_zero"].attrs["units"]}]',
49+
)
50+
)
51+
52+
n_events = events['event_index'][N_PULSE]
53+
slice_dataset(events, 'event_index', N_PULSE)
54+
slice_dataset(events, 'event_time_zero', N_PULSE)
55+
slice_dataset(events, 'event_id', n_events)
56+
slice_dataset(events, 'event_time_offset', n_events)
57+
58+
last_time = np.min(last_times)
59+
for name in mon_names:
60+
mon = instr[name]
61+
data = mon[f'{name}_events']
62+
time = data['time'][()].astype(f'datetime64[{data["time"].attrs["units"]}]')
63+
if time.shape == (0,):
64+
continue
65+
n_times = np.argmax(time > last_time)
66+
slice_dataset(data, 'time', n_times)
67+
68+
assert ( # noqa: S101
69+
data.attrs['axes'][0] == 'time'
70+
) # required to slice as below:
71+
slice_dataset(data, 'signal', n_times)
72+
73+
subprocess.check_call(['h5repack', DIR / TMP_FNAME, DIR / OUT_FNAME]) # noqa: S603, S607
74+
75+
76+
def slice_dataset(group, name, n):
77+
attrs = group[name].attrs
78+
group[name] = group.pop(name)[:n]
79+
group[name].attrs.update(attrs)
80+
81+
82+
if __name__ == "__main__":
83+
main()

0 commit comments

Comments
 (0)