Skip to content

Commit a17457f

Browse files
authored
Merge pull request #13 from scipp/grow-events-script
feat: add grow nexus script
2 parents c1b1598 + 4341528 commit a17457f

File tree

11 files changed

+218
-18
lines changed

11 files changed

+218
-18
lines changed

pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ dependencies = [
3636

3737
dynamic = ["version"]
3838

39+
[project.scripts]
40+
grow-nexus = "ess.reduce.scripts.grow_nexus:main"
41+
3942
[project.urls]
4043
"Bug Tracker" = "https://github.com/scipp/essreduce/issues"
4144
"Documentation" = "https://scipp.github.io/essreduce"

requirements/basetest.in

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
# Do not make an environment from this file, use test.txt instead!
33

44
pytest
5+
numpy

requirements/basetest.txt

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SHA1:0eaa389e1fdb3a1917c0f987514bd561be5718ee
1+
# SHA1:43a90d54540ba9fdbb2c21a785a9d5d7483af53b
22
#
33
# This file is autogenerated by pip-compile-multi
44
# To update, run:
@@ -9,11 +9,13 @@ exceptiongroup==1.2.0
99
# via pytest
1010
iniconfig==2.0.0
1111
# via pytest
12-
packaging==23.2
12+
numpy==1.26.4
13+
# via -r basetest.in
14+
packaging==24.0
1315
# via pytest
1416
pluggy==1.4.0
1517
# via pytest
16-
pytest==8.0.2
18+
pytest==8.1.1
1719
# via -r basetest.in
1820
tomli==2.0.1
1921
# via pytest

requirements/ci.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ gitpython==3.1.42
2727
# via -r ci.in
2828
idna==3.6
2929
# via requests
30-
packaging==23.2
30+
packaging==24.0
3131
# via
3232
# -r ci.in
3333
# pyproject-api
@@ -48,7 +48,7 @@ tomli==2.0.1
4848
# via
4949
# pyproject-api
5050
# tox
51-
tox==4.13.0
51+
tox==4.14.1
5252
# via -r ci.in
5353
urllib3==2.2.1
5454
# via requests

requirements/dev.txt

+5-5
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ isoduration==20.11.0
5050
# via jsonschema
5151
jinja2-ansible-filters==1.3.2
5252
# via copier
53-
json5==0.9.20
53+
json5==0.9.22
5454
# via jupyterlab-server
5555
jsonpointer==2.4
5656
# via jsonschema
@@ -61,7 +61,7 @@ jsonschema[format-nongpl]==4.21.1
6161
# nbformat
6262
jupyter-events==0.9.0
6363
# via jupyter-server
64-
jupyter-lsp==2.2.3
64+
jupyter-lsp==2.2.4
6565
# via jupyterlab
6666
jupyter-server==2.13.0
6767
# via
@@ -71,7 +71,7 @@ jupyter-server==2.13.0
7171
# notebook-shim
7272
jupyter-server-terminals==0.5.2
7373
# via jupyter-server
74-
jupyterlab==4.1.3
74+
jupyterlab==4.1.4
7575
# via -r dev.in
7676
jupyterlab-server==2.25.3
7777
# via jupyterlab
@@ -83,7 +83,7 @@ pathspec==0.12.1
8383
# via copier
8484
pip-compile-multi==2.6.3
8585
# via -r dev.in
86-
pip-tools==7.4.0
86+
pip-tools==7.4.1
8787
# via pip-compile-multi
8888
plumbum==1.8.2
8989
# via copier
@@ -121,7 +121,7 @@ terminado==0.18.0
121121
# jupyter-server-terminals
122122
toposort==1.10
123123
# via pip-compile-multi
124-
types-python-dateutil==2.8.19.20240106
124+
types-python-dateutil==2.8.19.20240311
125125
# via arrow
126126
uri-template==1.3.0
127127
# via jsonschema

requirements/docs.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ nbsphinx==0.9.3
118118
# via -r docs.in
119119
nest-asyncio==1.6.0
120120
# via ipykernel
121-
packaging==23.2
121+
packaging==24.0
122122
# via
123123
# ipykernel
124124
# nbconvert

requirements/mypy.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# pip-compile-multi
77
#
88
-r test.txt
9-
mypy==1.8.0
9+
mypy==1.9.0
1010
# via -r mypy.in
1111
mypy-extensions==1.0.0
1212
# via mypy

requirements/nightly.txt

-5
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@
88
-r basetest.txt
99
h5py==3.10.0
1010
# via scippnexus
11-
numpy==1.26.4
12-
# via
13-
# h5py
14-
# scipp
15-
# scipy
1611
python-dateutil==2.9.0.post0
1712
# via scippnexus
1813
scipp==24.2.0

requirements/wheels.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#
88
build==1.1.1
99
# via -r wheels.in
10-
packaging==23.2
10+
packaging==24.0
1111
# via build
1212
pyproject-hooks==1.0.0
1313
# via build

src/ess/reduce/scripts/grow_nexus.py

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import argparse
2+
import shutil
3+
from typing import Optional
4+
5+
import h5py
6+
7+
8+
def _scale_group(event_data: h5py.Group, scale: int):
9+
if not all(
10+
required_field in event_data
11+
for required_field in ('event_index', 'event_time_offset', 'event_id')
12+
):
13+
return
14+
event_index = (event_data['event_index'][:] * scale).astype('uint')
15+
event_data['event_index'][:] = event_index
16+
17+
size = event_data['event_id'].size
18+
event_data['event_id'].resize(event_index[-1], axis=0)
19+
event_data['event_time_offset'].resize(event_index[-1], axis=0)
20+
21+
for s in range(1, scale):
22+
event_data['event_id'][s * size : (s + 1) * size] = event_data['event_id'][
23+
:size
24+
]
25+
event_data['event_time_offset'][s * size : (s + 1) * size] = event_data[
26+
'event_time_offset'
27+
][:size]
28+
29+
30+
def _grow_nexus_file_impl(file: h5py.File, detector_scale: int, monitor_scale: int):
31+
for group in file.values():
32+
if group.attrs.get('NX_class', '') == 'NXentry':
33+
entry = group
34+
break
35+
for group in entry.values():
36+
if group.attrs.get('NX_class', '') == 'NXinstrument':
37+
instrument = group
38+
break
39+
for group in instrument.values():
40+
if (nx_class := group.attrs.get('NX_class', '')) in (
41+
'NXdetector',
42+
'NXmonitor',
43+
):
44+
for subgroup in group.values():
45+
if subgroup.attrs.get('NX_class', '') == 'NXevent_data':
46+
_scale_group(
47+
subgroup,
48+
scale=detector_scale
49+
if nx_class == 'NXdetector'
50+
else monitor_scale,
51+
)
52+
53+
54+
def grow_nexus_file(
55+
*, filename: str, detector_scale: int, monitor_scale: Optional[int]
56+
):
57+
with h5py.File(filename, 'a') as f:
58+
_grow_nexus_file_impl(
59+
f,
60+
detector_scale,
61+
monitor_scale if monitor_scale is not None else detector_scale,
62+
)
63+
64+
65+
def integer_greater_than_one(x):
66+
x = int(x)
67+
if x < 1:
68+
raise argparse.ArgumentTypeError('Must be larger than or equal to 1')
69+
return x
70+
71+
72+
def main():
73+
parser = argparse.ArgumentParser()
74+
parser.add_argument(
75+
"-f",
76+
"--file",
77+
type=str,
78+
help=(
79+
'Input file name. The events in the input file will be '
80+
'repeated `scale` times and stored in the output file.'
81+
),
82+
required=True,
83+
)
84+
parser.add_argument(
85+
"-o",
86+
"--output",
87+
type=str,
88+
help='Output file name where the resulting nexus file will be written.',
89+
required=True,
90+
)
91+
parser.add_argument(
92+
"-s",
93+
"--detector-scale",
94+
type=integer_greater_than_one,
95+
help=('Scale factor to multiply the number of detector events by.'),
96+
required=True,
97+
)
98+
parser.add_argument(
99+
"-m",
100+
"--monitor-scale",
101+
type=integer_greater_than_one,
102+
default=None,
103+
help=(
104+
'Scale factor to multiply the number of monitor events by. '
105+
'If not given, the detector scale will be used'
106+
),
107+
)
108+
args = parser.parse_args()
109+
if args.file != args.output:
110+
shutil.copy2(args.file, args.output)
111+
grow_nexus_file(
112+
filename=args.output,
113+
detector_scale=args.detector_scale,
114+
monitor_scale=args.monitor_scale,
115+
)

tests/scripts/test_grow_nexus.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import os
2+
import tempfile
3+
4+
import h5py
5+
import numpy as np
6+
import pytest
7+
8+
from ess.reduce.scripts.grow_nexus import grow_nexus_file
9+
10+
11+
@pytest.fixture
12+
def nexus_file():
13+
with tempfile.TemporaryDirectory() as tmp:
14+
path = os.path.join(tmp, 'test.nxs')
15+
with h5py.File(path, 'a') as hf:
16+
entry = hf.create_group('entry')
17+
entry.attrs['NX_class'] = 'NXentry'
18+
19+
instrument = entry.create_group('instrument')
20+
instrument.attrs['NX_class'] = 'NXinstrument'
21+
22+
for group, nxclass in (
23+
('detector', 'NXdetector'),
24+
('monitor', 'NXmonitor'),
25+
):
26+
detector = instrument.create_group(group)
27+
detector.attrs['NX_class'] = nxclass
28+
29+
event_data = detector.create_group('event_data')
30+
event_data.attrs['NX_class'] = 'NXevent_data'
31+
32+
event_data.create_dataset(
33+
'event_index',
34+
data=np.array([2, 4, 6]),
35+
maxshape=(None,),
36+
chunks=True,
37+
)
38+
event_data.create_dataset(
39+
'event_time_zero',
40+
data=np.array([0, 1, 2]),
41+
maxshape=(None,),
42+
chunks=True,
43+
)
44+
event_data.create_dataset(
45+
'event_id',
46+
data=np.array([0, 1, 2, 0, 1, 2]),
47+
maxshape=(None,),
48+
chunks=True,
49+
)
50+
event_data.create_dataset(
51+
'event_time_offset',
52+
data=np.array([1, 2, 1, 2, 1, 2]),
53+
maxshape=(None,),
54+
chunks=True,
55+
)
56+
57+
yield path
58+
59+
60+
@pytest.mark.parametrize('monitor_scale', (1, 2, None))
61+
@pytest.mark.parametrize('detector_scale', (1, 2))
62+
def test_grow_nexus(nexus_file, detector_scale, monitor_scale):
63+
grow_nexus_file(
64+
filename=nexus_file, detector_scale=detector_scale, monitor_scale=monitor_scale
65+
)
66+
67+
monitor_scale = monitor_scale if monitor_scale is not None else detector_scale
68+
69+
with h5py.File(nexus_file, 'r') as f:
70+
for detector, scale in zip(
71+
('detector', 'monitor'), (detector_scale, monitor_scale)
72+
):
73+
np.testing.assert_equal(
74+
[scale * i for i in [2, 4, 6]],
75+
f[f'entry/instrument/{detector}/event_data/event_index'][()],
76+
)
77+
np.testing.assert_equal(
78+
scale * [0, 1, 2, 0, 1, 2],
79+
f[f'entry/instrument/{detector}/event_data/event_id'][()],
80+
)
81+
np.testing.assert_equal(
82+
scale * [1, 2, 1, 2, 1, 2],
83+
f[f'entry/instrument/{detector}/event_data/event_time_offset'][()],
84+
)

0 commit comments

Comments
 (0)