Skip to content

Commit 6ffeb88

Browse files
authored
Merge pull request #255 from scipp/fix-broken-chunking
Fix broken chunking when computing Tof lookup table
2 parents d52fe6b + e3aeb92 commit 6ffeb88

File tree

2 files changed

+118
-4
lines changed

2 files changed

+118
-4
lines changed

src/ess/reduce/time_of_flight/lut.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Utilities for computing time-of-flight lookup tables from neutron simulations.
55
"""
66

7+
import math
78
from dataclasses import dataclass
89
from typing import NewType
910

@@ -330,12 +331,11 @@ def make_tof_lookup_table(
330331
ndist = len(distance_bins) - 1
331332
max_size = 2e7
332333
total_size = ndist * len(simulation.time_of_arrival)
333-
nchunks = total_size / max_size
334-
chunk_size = int(ndist / nchunks) + 1
334+
nchunks = math.ceil(total_size / max_size)
335+
chunk_size = math.ceil(ndist / nchunks)
335336
pieces = []
336-
for i in range(int(nchunks) + 1):
337+
for i in range(nchunks):
337338
dist_edges = distance_bins[i * chunk_size : (i + 1) * chunk_size + 1]
338-
339339
pieces.append(
340340
_compute_mean_tof_in_distance_range(
341341
simulation=simulation,

tests/time_of_flight/lut_test.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
3+
import pytest
4+
import scipp as sc
5+
6+
from ess.reduce import time_of_flight
7+
from ess.reduce.time_of_flight import TofLookupTableWorkflow
8+
9+
sl = pytest.importorskip("sciline")
10+
11+
12+
def test_lut_workflow_computes_table():
13+
wf = TofLookupTableWorkflow()
14+
wf[time_of_flight.DiskChoppers] = {}
15+
wf[time_of_flight.SourcePosition] = sc.vector([0, 0, 0], unit='m')
16+
wf[time_of_flight.NumberOfSimulatedNeutrons] = 100_000
17+
wf[time_of_flight.SimulationSeed] = 60
18+
wf[time_of_flight.PulseStride] = 1
19+
20+
lmin, lmax = sc.scalar(25.0, unit='m'), sc.scalar(35.0, unit='m')
21+
dres = sc.scalar(0.1, unit='m')
22+
tres = sc.scalar(333.0, unit='us')
23+
24+
wf[time_of_flight.LtotalRange] = lmin, lmax
25+
wf[time_of_flight.DistanceResolution] = dres
26+
wf[time_of_flight.TimeResolution] = tres
27+
28+
table = wf.compute(time_of_flight.TimeOfFlightLookupTable)
29+
30+
assert table.coords['distance'].min() < lmin
31+
assert table.coords['distance'].max() > lmax
32+
assert table.coords['event_time_offset'].max() == sc.scalar(1 / 14, unit='s').to(
33+
unit=table.coords['event_time_offset'].unit
34+
)
35+
assert sc.isclose(table.coords['distance_resolution'], dres)
36+
# Note that the time resolution is not exactly preserved since we want the table to
37+
# span exactly the frame period.
38+
assert sc.isclose(table.coords['time_resolution'], tres, rtol=sc.scalar(0.01))
39+
40+
41+
def test_lut_workflow_computes_table_in_chunks():
42+
wf = TofLookupTableWorkflow()
43+
wf[time_of_flight.DiskChoppers] = {}
44+
wf[time_of_flight.SourcePosition] = sc.vector([0, 0, 0], unit='m')
45+
# Lots of neutrons activates chunking
46+
wf[time_of_flight.NumberOfSimulatedNeutrons] = 1_000_000
47+
wf[time_of_flight.SimulationSeed] = 61
48+
wf[time_of_flight.PulseStride] = 1
49+
50+
lmin, lmax = sc.scalar(25.0, unit='m'), sc.scalar(35.0, unit='m')
51+
dres = sc.scalar(0.1, unit='m')
52+
tres = sc.scalar(250.0, unit='us')
53+
54+
wf[time_of_flight.LtotalRange] = lmin, lmax
55+
wf[time_of_flight.DistanceResolution] = dres
56+
wf[time_of_flight.TimeResolution] = tres
57+
58+
table = wf.compute(time_of_flight.TimeOfFlightLookupTable)
59+
60+
assert table.coords['distance'].min() < lmin
61+
assert table.coords['distance'].max() > lmax
62+
assert table.coords['event_time_offset'].max() == sc.scalar(1 / 14, unit='s').to(
63+
unit=table.coords['event_time_offset'].unit
64+
)
65+
assert sc.isclose(table.coords['distance_resolution'], dres)
66+
# Note that the time resolution is not exactly preserved since we want the table to
67+
# span exactly the frame period.
68+
assert sc.isclose(table.coords['time_resolution'], tres, rtol=sc.scalar(0.01))
69+
70+
71+
def test_lut_workflow_pulse_skipping():
72+
wf = TofLookupTableWorkflow()
73+
wf[time_of_flight.DiskChoppers] = {}
74+
wf[time_of_flight.SourcePosition] = sc.vector([0, 0, 0], unit='m')
75+
wf[time_of_flight.NumberOfSimulatedNeutrons] = 100_000
76+
wf[time_of_flight.SimulationSeed] = 62
77+
wf[time_of_flight.PulseStride] = 2
78+
79+
lmin, lmax = sc.scalar(55.0, unit='m'), sc.scalar(65.0, unit='m')
80+
dres = sc.scalar(0.1, unit='m')
81+
tres = sc.scalar(250.0, unit='us')
82+
83+
wf[time_of_flight.LtotalRange] = lmin, lmax
84+
wf[time_of_flight.DistanceResolution] = dres
85+
wf[time_of_flight.TimeResolution] = tres
86+
87+
table = wf.compute(time_of_flight.TimeOfFlightLookupTable)
88+
89+
assert table.coords['event_time_offset'].max() == 2 * sc.scalar(
90+
1 / 14, unit='s'
91+
).to(unit=table.coords['event_time_offset'].unit)
92+
93+
94+
def test_lut_workflow_non_exact_distance_range():
95+
wf = TofLookupTableWorkflow()
96+
wf[time_of_flight.DiskChoppers] = {}
97+
wf[time_of_flight.SourcePosition] = sc.vector([0, 0, 0], unit='m')
98+
wf[time_of_flight.NumberOfSimulatedNeutrons] = 100_000
99+
wf[time_of_flight.SimulationSeed] = 63
100+
wf[time_of_flight.PulseStride] = 1
101+
102+
lmin, lmax = sc.scalar(25.0, unit='m'), sc.scalar(35.0, unit='m')
103+
dres = sc.scalar(0.33, unit='m')
104+
tres = sc.scalar(250.0, unit='us')
105+
106+
wf[time_of_flight.LtotalRange] = lmin, lmax
107+
wf[time_of_flight.DistanceResolution] = dres
108+
wf[time_of_flight.TimeResolution] = tres
109+
110+
table = wf.compute(time_of_flight.TimeOfFlightLookupTable)
111+
112+
assert table.coords['distance'].min() < lmin
113+
assert table.coords['distance'].max() > lmax
114+
assert sc.isclose(table.coords['distance_resolution'], dres)

0 commit comments

Comments
 (0)