Skip to content

Commit 70f9261

Browse files
Merge pull request #245 from scipp/toa-tof-rebin
Resample TOA-TOF-converted histogram data using `rebin`
2 parents e85642c + 840b3b2 commit 70f9261

File tree

6 files changed

+579
-320
lines changed

6 files changed

+579
-320
lines changed

src/ess/reduce/time_of_flight/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
resample_monitor_time_of_flight_data,
1414
)
1515
from .simulation import simulate_beamline
16-
from .to_events import to_events
1716
from .types import (
1817
DetectorLtotal,
1918
DetectorTofData,
@@ -58,5 +57,4 @@
5857
"resample_detector_time_of_flight_data",
5958
"resample_monitor_time_of_flight_data",
6059
"simulate_beamline",
61-
"to_events",
6260
]

src/ess/reduce/time_of_flight/eto_to_tof.py

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
MonitorType,
2828
RunType,
2929
)
30-
from .to_events import to_events
30+
from .resample import rebin_strictly_increasing
3131
from .types import (
3232
DetectorLtotal,
3333
DetectorTofData,
@@ -664,51 +664,13 @@ def monitor_time_of_flight_data(
664664
)
665665

666666

667-
def _resample_tof_data(da: sc.DataArray) -> sc.DataArray:
668-
"""
669-
Histogrammed data that has been converted to `tof` will typically have
670-
unsorted bin edges (due to either wrapping of `time_of_flight` or wavelength
671-
overlap between subframes).
672-
This function re-histograms the data to ensure that the bin edges are sorted.
673-
It makes use of the ``to_events`` helper which generates a number of events in each
674-
bin with a uniform distribution. The new events are then histogrammed using a set of
675-
sorted bin edges.
676-
677-
WARNING:
678-
This function is highly experimental, has limitations and should be used with
679-
caution. It is a workaround to the issue that rebinning data with unsorted bin
680-
edges is not supported in scipp.
681-
As such, this function is not part of the default set of providers, and needs to be
682-
inserted manually into the workflow.
683-
684-
Parameters
685-
----------
686-
da:
687-
Histogrammed data with the time-of-flight coordinate.
688-
"""
689-
dim = next(iter(set(da.dims) & {"time_of_flight", "tof"}))
690-
data = da.rename_dims({dim: "tof"}).drop_coords(
691-
[name for name in da.coords if name != "tof"]
692-
)
693-
events = to_events(data, "event")
694-
695-
# Define a new bin width, close to the original bin width.
696-
# TODO: this could be a workflow parameter
697-
coord = da.coords["tof"]
698-
bin_width = (coord[dim, 1:] - coord[dim, :-1]).nanmedian()
699-
rehist = events.hist(tof=bin_width)
700-
return rehist.assign_coords(
701-
{key: var for key, var in da.coords.items() if dim not in var.dims}
702-
)
703-
704-
705667
def resample_detector_time_of_flight_data(
706668
da: DetectorTofData[RunType],
707669
) -> ResampledDetectorTofData[RunType]:
708670
"""
709671
Resample the detector time-of-flight data to ensure that the bin edges are sorted.
710672
"""
711-
return ResampledDetectorTofData(_resample_tof_data(da))
673+
return ResampledDetectorTofData[RunType](rebin_strictly_increasing(da, dim='tof'))
712674

713675

714676
def resample_monitor_time_of_flight_data(
@@ -717,7 +679,9 @@ def resample_monitor_time_of_flight_data(
717679
"""
718680
Resample the monitor time-of-flight data to ensure that the bin edges are sorted.
719681
"""
720-
return ResampledMonitorTofData(_resample_tof_data(da))
682+
return ResampledMonitorTofData[RunType, MonitorType](
683+
rebin_strictly_increasing(da, dim='tof')
684+
)
721685

722686

723687
def default_parameters() -> dict:
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
3+
4+
5+
import numpy as np
6+
import scipp as sc
7+
8+
9+
def find_strictly_increasing_sections(var: sc.Variable) -> list[slice]:
10+
"""
11+
Find strictly increasing sections in a coordinate dimension (minimum length 2).
12+
13+
Parameters
14+
----------
15+
var:
16+
The variable to analyze, which should be one-dimensional.
17+
18+
Returns
19+
-------
20+
sections:
21+
Slice objects that can be used extract strictly increasing sections.
22+
"""
23+
values = var.values
24+
finite = np.isfinite(values)
25+
increasing = (np.sign(np.diff(values)) > 0) & finite[:-1] & finite[1:]
26+
# 1 marks the start of an increasing section, -1 marks the end
27+
transitions = np.diff(np.concatenate(([False], increasing, [False])).astype(int))
28+
section_starts = np.where(transitions == 1)[0]
29+
section_ends = np.where(transitions == -1)[0] + np.array(1)
30+
return [
31+
slice(start, end)
32+
for start, end in zip(section_starts, section_ends, strict=True)
33+
if end - start >= 2 # Ensure section has at least 2 points
34+
]
35+
36+
37+
def get_min_max(
38+
var: sc.Variable, *, dim: str, slices: list[slice]
39+
) -> tuple[sc.Variable, sc.Variable]:
40+
if not slices:
41+
raise ValueError("No strictly increasing sections found.")
42+
combined = sc.concat([var[dim, slice] for slice in slices], dim)
43+
return combined.min(), combined.max()
44+
45+
46+
def make_regular_grid(
47+
var: sc.Variable, *, dim: str, slices: list[slice]
48+
) -> sc.Variable:
49+
"""
50+
Create a regular grid variable based on the min and max of the slices.
51+
52+
The grid is constructed such that it includes the minimum and maximum values
53+
of the strictly increasing sections, with a step size equal to the difference
54+
between the first two values of the section with the minimum start value (which is
55+
not necessarily the first section).
56+
"""
57+
min_val, max_val = get_min_max(var, dim=dim, slices=slices)
58+
first: sc.Variable | None = None
59+
for s in slices:
60+
first = var[dim, s]
61+
if sc.identical(first[0], min_val):
62+
break
63+
if first is None:
64+
# This should not happen if slices are correctly identified and passed from
65+
# find_strictly_increasing_sections.
66+
raise ValueError("Section is not strictly increasing.")
67+
step = first[1] - first[0]
68+
return sc.arange(
69+
dim=dim,
70+
start=min_val.value,
71+
stop=max_val.value + step.value, # Ensure the last bin edge is included
72+
step=step.value,
73+
unit=step.unit,
74+
dtype=step.dtype,
75+
)
76+
77+
78+
def rebin_strictly_increasing(da: sc.DataArray, dim: str) -> sc.DataArray:
79+
"""
80+
Find strictly monotonic sections in a coordinate dimension and rebin the data array
81+
into a regular grid based on these sections.
82+
"""
83+
# Ensure the dimension is named like the coordinate.
84+
da = da.rename_dims({da.coords[dim].dim: dim})
85+
slices = find_strictly_increasing_sections(da.coords[dim])
86+
if len(slices) == 1:
87+
return da[dim, slices[0]]
88+
if not slices:
89+
raise ValueError("No strictly increasing sections found.")
90+
if da.coords[dim].dtype not in (sc.DType.float64, sc.DType.float32):
91+
# rebin does not like integer coords.
92+
da = da.assign_coords({dim: da.coords[dim].to(dtype='float64')})
93+
# Slices refer to the indices in the coord, which are bin edges. For slicing data
94+
# we need to stop at the last index minus one.
95+
sections = [da[dim, section.start : section.stop - 1] for section in slices]
96+
edges = make_regular_grid(da.coords[dim], dim=dim, slices=slices)
97+
return sc.reduce([sc.rebin(section, {dim: edges}) for section in sections]).sum()

src/ess/reduce/time_of_flight/to_events.py

Lines changed: 0 additions & 111 deletions
This file was deleted.

0 commit comments

Comments
 (0)