Skip to content

Commit 53b861f

Browse files
committed
Do not clip and no extra integral
1 parent 2397d65 commit 53b861f

File tree

2 files changed

+665
-663
lines changed

2 files changed

+665
-663
lines changed

src/ess/reduce/normalization.py

Lines changed: 42 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -15,51 +15,26 @@ def normalize_by_monitor_histogram(
1515
monitor: sc.DataArray,
1616
uncertainty_broadcast_mode: UncertaintyBroadcastMode,
1717
) -> sc.DataArray:
18-
"""Normalize detector data by a histogrammed monitor.
18+
"""Normalize detector data by a normalized histogrammed monitor.
1919
20-
First, the monitor is clipped to the range of the detector
20+
This normalization accounts for both the (wavelength) profile of the incident beam
21+
and the integrated neutron flux, meaning measurement duration and source strength.
2122
22-
.. math::
23-
24-
\\bar{m}_i = m_i I(x_i, x_{i+1}),
23+
- For *event* detectors, the monitor values are mapped to the detector
24+
using :func:`scipp.lookup`. That is, for detector event :math:`d_i`,
25+
:math:`m_i` is the monitor bin value at the same coordinate.
26+
- For *histogram* detectors, the monitor is rebinned using to the detector
27+
binning using :func:`scipp.rebin`. Thus, detector value :math:`d_i` and
28+
monitor value :math:`m_i` correspond to the same bin.
2529
26-
where :math:`m_i` is the monitor intensity in bin :math:`i`,
27-
:math:`x_i` is the lower bin edge of bin :math:`i`, and
28-
:math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector.
30+
In both cases, let :math:`x_i` be the lower bound of monitor bin :math:`i`
31+
and let :math:`\\Delta x_i = x_{i+1} - x_i` be the width of that bin.
2932
30-
The detector bins :math:`d_i` are normalized according to
33+
The detector is normalized according to
3134
3235
.. math::
3336
34-
d_i^\\text{Norm} = \\frac{d_i}{\\bar{m}_i} \\Delta x_i
35-
\\frac{\\sum_j\\,\\bar{m}_j}{\\sum_j\\,\\Delta x_j}
36-
37-
where :math:`\\Delta x_i = x_{i+1} - x_i` is the width of
38-
monitor bin :math:`i` (see below).
39-
This normalization leads to a result that has the same
40-
unit as the input detector data.
41-
42-
Monitor bin :math:`i` is chosen according to:
43-
44-
- *Histogrammed detector*: The monitor is
45-
`rebinned <https://scipp.github.io/generated/functions/scipp.rebin.html>`_
46-
to the detector binning. This distributes the monitor weights to the
47-
detector bins.
48-
- *Binned detector*: The monitor value for bin :math:`i` is determined via
49-
:func:`scipp.lookup`. This means that for each event, the monitor value
50-
is obtained from the monitor histogram at that event coordinate value.
51-
52-
.. Attention::
53-
54-
Masked bins in ``detector`` are ignored when clipping the monitor and therefore
55-
impact the normalization factor.
56-
The output's masked bins are normalized using the same factor and may
57-
be incorrect and even contain NaN.
58-
You should only drop masks after normalization if you know what you are doing.
59-
60-
This function is based on the implementation of
61-
`NormaliseToMonitor <https://docs.mantidproject.org/nightly/algorithms/NormaliseToMonitor-v1.html>`_
62-
in Mantid.
37+
d_i^\\text{Norm} = \\frac{d_i}{m_i} \\Delta x_i
6338
6439
Parameters
6540
----------
@@ -85,24 +60,21 @@ def normalize_by_monitor_histogram(
8560
normalize_by_monitor_integrated:
8661
Normalize by an integrated monitor.
8762
"""
63+
_check_monitor_range_contains_detector(monitor=monitor, detector=detector)
64+
8865
dim = monitor.dim
8966

67+
if detector.bins is None:
68+
monitor = monitor.rebin({dim: detector.coords[dim]})
9069
detector = _mask_detector_for_norm(detector=detector, monitor=monitor)
91-
clipped = _clip_monitor_to_detector_range(monitor=monitor, detector=detector)
92-
coord = clipped.coords[dim]
93-
delta_w = sc.DataArray(coord[1:] - coord[:-1], masks=clipped.masks)
94-
total_monitor_weight = broadcast_uncertainties(
95-
clipped.sum() / delta_w.sum(),
96-
prototype=clipped,
97-
mode=uncertainty_broadcast_mode,
98-
).data
99-
delta_w *= total_monitor_weight
70+
coord = monitor.coords[dim]
71+
delta_w = sc.DataArray(coord[1:] - coord[:-1], masks=monitor.masks)
10072
norm = broadcast_uncertainties(
101-
clipped / delta_w, prototype=detector, mode=uncertainty_broadcast_mode
73+
monitor / delta_w, prototype=detector, mode=uncertainty_broadcast_mode
10274
)
10375

10476
if detector.bins is None:
105-
return detector / norm
77+
return detector / norm.rebin({dim: detector.coords[dim]})
10678
return detector.bins / sc.lookup(norm, dim=dim)
10779

10880

@@ -114,23 +86,21 @@ def normalize_by_monitor_integrated(
11486
) -> sc.DataArray:
11587
"""Normalize detector data by an integrated monitor.
11688
117-
The monitor is integrated according to
118-
119-
.. math::
89+
This normalization accounts only for the integrated neutron flux,
90+
meaning measurement duration and source strength.
91+
It does *not* account for the (wavelength) profile of the incident beam.
92+
For that, see :func:`normalize_by_monitor_histogram`.
12093
121-
M = \\sum_{i=0}^{N-1}\\, m_i (x_{i+1} - x_i) I(x_i, x_{i+1}),
94+
Let :math:`d_i` be a detector event or the counts in a detector bin.
95+
The normalized detector is
12296
123-
where :math:`m_i` is the monitor intensity in bin :math:`i`,
124-
:math:`x_i` is the lower bin edge of bin :math:`i`, and
125-
:math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector.
97+
.. math::
12698
127-
.. Attention::
99+
d_i^\\text{Norm} &= d_i / M \\\\
100+
M &= \\sum_j\\, m_j (x_{j+1} - x_j)\\,,
128101
129-
Masked bins in ``detector`` are ignored when clipping the monitor and therefore
130-
impact the normalization factor.
131-
The output's masked bins are normalized using the same factor and may
132-
be incorrect and even contain NaN.
133-
You should only drop masks after normalization if you know what you are doing.
102+
where :math:`m_j` is the monitor counts in bin :math:`j` and
103+
:math:`x_j` is the lower bin edge of that bin.
134104
135105
Parameters
136106
----------
@@ -152,43 +122,37 @@ def normalize_by_monitor_integrated(
152122
See also
153123
--------
154124
normalize_by_monitor_histogram:
155-
Normalize by a monitor histogram without integration.
125+
Normalize by a monitor histogram.
156126
"""
127+
_check_monitor_range_contains_detector(monitor=monitor, detector=detector)
157128
detector = _mask_detector_for_norm(detector=detector, monitor=monitor)
158-
clipped = _clip_monitor_to_detector_range(monitor=monitor, detector=detector)
159-
coord = clipped.coords[clipped.dim]
160-
norm = (clipped * (coord[1:] - coord[:-1])).data.sum()
129+
coord = monitor.coords[monitor.dim]
130+
norm = (monitor * (coord[1:] - coord[:-1])).data.sum()
161131
norm = broadcast_uncertainties(
162132
norm, prototype=detector, mode=uncertainty_broadcast_mode
163133
)
164134
return detector / norm
165135

166136

167-
def _clip_monitor_to_detector_range(
137+
def _check_monitor_range_contains_detector(
168138
*, monitor: sc.DataArray, detector: sc.DataArray
169-
) -> sc.DataArray:
139+
) -> None:
170140
dim = monitor.dim
171141
if not monitor.coords.is_edges(dim):
172142
raise sc.CoordError(
173143
f"Monitor coordinate '{dim}' must be bin-edges to integrate the monitor."
174144
)
175145

176-
# Reduce with `all` instead of `any` to include bins in range calculations
177-
# that contain any unmasked data.
178-
masks = {
179-
name: mask.all(set(mask.dims) - {dim}) for name, mask in detector.masks.items()
180-
}
181-
182146
# Prefer a bin coord over an event coord because this makes the behavior for binned
183147
# and histogrammed data consistent. If we used an event coord, we might allow a
184148
# monitor range that is less than the detector bins which is fine for the events,
185149
# but would be wrong if the detector was subsequently histogrammed.
186150
if (det_coord := detector.coords.get(dim)) is not None:
187-
lo = sc.DataArray(det_coord[dim, :-1], masks=masks).nanmin().data
188-
hi = sc.DataArray(det_coord[dim, 1:], masks=masks).nanmax().data
151+
lo = det_coord[dim, :-1].nanmin()
152+
hi = det_coord[dim, 1:].nanmax()
189153
elif (det_coord := detector.bins.coords.get(dim)) is not None:
190-
lo = sc.DataArray(det_coord, masks=masks).nanmin().data
191-
hi = sc.DataArray(det_coord, masks=masks).nanmax().data
154+
lo = det_coord.nanmin()
155+
hi = det_coord.nanmax()
192156
else:
193157
raise sc.CoordError(
194158
f"Missing '{dim}' coordinate in detector for monitor normalization."
@@ -201,14 +165,6 @@ def _clip_monitor_to_detector_range(
201165
f"is smaller than the range of the detector ({lo:c} to {hi:c})."
202166
)
203167

204-
if detector.bins is None:
205-
# If we didn't rebin to the detector coord here, then, for a finer monitor
206-
# binning than detector, the lookup table would extract one monitor value for
207-
# each detector bin and ignore other values lying in the same detector bin.
208-
# But integration would pick up all monitor bins.
209-
return monitor.rebin({dim: det_coord})
210-
return monitor[dim, lo:hi]
211-
212168

213169
def _mask_detector_for_norm(
214170
*, detector: sc.DataArray, monitor: sc.DataArray

0 commit comments

Comments
 (0)