Skip to content

Commit 489c65e

Browse files
committed
Respect masks in range calculation
1 parent 876cc78 commit 489c65e

File tree

2 files changed

+51
-32
lines changed

2 files changed

+51
-32
lines changed

src/ess/reduce/normalization.py

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .uncertainty import UncertaintyBroadcastMode, broadcast_uncertainties
88

99

10+
# TODO explain impact of masking, esp multi-dim
1011
def normalize_by_monitor_histogram(
1112
detector: sc.DataArray,
1213
*,
@@ -47,6 +48,14 @@ def normalize_by_monitor_histogram(
4748
:func:`scipp.lookup`. This means that for each event, the monitor value
4849
is obtained from the monitor histogram at that event coordinate value.
4950
51+
.. Attention::
52+
53+
Masked bins in ``detector`` are ignored when clipping the monitor and therefore
54+
impact the normalization factor.
55+
The output's masked bins are normalized using the same factor and may
56+
be incorrect and even contain NaN.
57+
You should only drop masks after normalization if you know what you are doing.
58+
5059
This function is based on the implementation of
5160
`NormaliseToMonitor <https://docs.mantidproject.org/nightly/algorithms/NormaliseToMonitor-v1.html>`_
5261
in Mantid.
@@ -111,6 +120,14 @@ def normalize_by_monitor_integrated(
111120
:math:`x_i` is the lower bin edge of bin :math:`i`, and
112121
:math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector.
113122
123+
.. Attention::
124+
125+
Masked bins in ``detector`` are ignored when clipping the monitor and therefore
126+
impact the normalization factor.
127+
The output's masked bins are normalized using the same factor and may
128+
be incorrect and even contain NaN.
129+
You should only drop masks after normalization if you know what you are doing.
130+
114131
Parameters
115132
----------
116133
detector:
@@ -149,30 +166,22 @@ def _clip_monitor_to_detector_range(
149166
f"Monitor coordinate '{dim}' must be bin-edges to integrate the monitor."
150167
)
151168

169+
# Reduce with `all` instead of `any` to include bins in range calculations
170+
# that contain any unmasked data.
171+
masks = {
172+
name: mask.all(set(mask.dims) - {dim}) for name, mask in detector.masks.items()
173+
}
174+
152175
# Prefer a bin coord over an event coord because this makes the behavior for binned
153176
# and histogrammed data consistent. If we used an event coord, we might allow a
154177
# monitor range that is less than the detector bins which is fine for the events,
155178
# but would be wrong if the detector was subsequently histogrammed.
156-
if dim in detector.coords:
157-
det_coord = detector.coords[dim]
158-
159-
# Mask zero-count bins, which are an artifact from the rectangular 2-D binning.
160-
# The wavelength of those bins must be excluded when determining the range.
161-
if detector.bins is None:
162-
mask = detector.data == sc.scalar(0.0, unit=detector.unit)
163-
else:
164-
mask = detector.data.bins.size() == sc.scalar(0.0, unit=None)
165-
lo = (
166-
sc.DataArray(det_coord[dim, :-1], masks={'zero_counts': mask}).nanmin().data
167-
)
168-
hi = sc.DataArray(det_coord[dim, 1:], masks={'zero_counts': mask}).nanmax().data
169-
170-
elif dim in detector.bins.coords:
171-
det_coord = detector.bins.coords[dim]
172-
# No need to mask here because we have the exact event coordinate values.
173-
lo = det_coord.nanmin()
174-
hi = det_coord.nanmax()
175-
179+
if (det_coord := detector.coords.get(dim)) is not None:
180+
lo = sc.DataArray(det_coord[dim, :-1], masks=masks).nanmin().data
181+
hi = sc.DataArray(det_coord[dim, 1:], masks=masks).nanmax().data
182+
elif (det_coord := detector.bins.coords.get(dim)) is not None:
183+
lo = sc.DataArray(det_coord, masks=masks).nanmin().data
184+
hi = sc.DataArray(det_coord, masks=masks).nanmax().data
176185
else:
177186
raise sc.CoordError(
178187
f"Missing '{dim}' coordinate in detector for monitor normalization."
@@ -181,8 +190,8 @@ def _clip_monitor_to_detector_range(
181190
if monitor.coords[dim].min() > lo or monitor.coords[dim].max() < hi:
182191
raise ValueError(
183192
f"Cannot normalize by monitor: The {dim} range of the monitor "
184-
f"({monitor.coords[dim].min().value} to {monitor.coords[dim].max().value}) "
185-
f"is smaller than the range of the detector ({lo.value} to {hi.value})."
193+
f"({monitor.coords[dim].min():c} to {monitor.coords[dim].max():c}) "
194+
f"is smaller than the range of the detector ({lo:c} to {hi:c})."
186195
)
187196

188197
if detector.bins is None:

tests/normalization_test.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,11 @@ def test_normalize_by_monitor_histogram_monitor_finer_bins_in_monitor_hist() ->
319319
sc.testing.assert_allclose(normalized, expected)
320320

321321

322-
def test_normalize_by_monitor_histogram_zero_count_bins_are_ignored_hist() -> None:
322+
def test_normalize_by_monitor_histogram_masked_bins_are_ignored_hist() -> None:
323323
detector = sc.DataArray(
324324
sc.array(dims=['w'], values=[0, 10, 30, 0], unit='counts'),
325325
coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')},
326+
masks={'m': sc.array(dims=['w'], values=[True, False, False, True])},
326327
)
327328
monitor = sc.DataArray(
328329
sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'),
@@ -339,16 +340,21 @@ def test_normalize_by_monitor_histogram_zero_count_bins_are_ignored_hist() -> No
339340
expected = sc.DataArray(
340341
sc.array(dims=['w'], values=[0.0, 11, 11, float('NaN')], unit='counts'),
341342
coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')},
343+
masks={'m': sc.array(dims=['w'], values=[True, False, False, True])},
342344
)
343345

344346
sc.testing.assert_identical(normalized, expected)
345347

346348

347-
def test_normalize_by_monitor_histogram_zero_count_bins_are_ignored() -> None:
348-
detector = sc.DataArray(
349-
sc.array(dims=['w'], values=[0, 10, 30], unit='counts'),
350-
coords={'w': sc.arange('w', 3.0, unit='Å')},
351-
).bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å'))
349+
def test_normalize_by_monitor_histogram_masked_bins_are_ignored() -> None:
350+
detector = (
351+
sc.DataArray(
352+
sc.array(dims=['w'], values=[0, 10, 30, 40], unit='counts'),
353+
coords={'w': sc.arange('w', 4.0, unit='Å')},
354+
)
355+
.bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å'))
356+
.assign_masks(m=sc.array(dims=['w'], values=[True, False, False, True]))
357+
)
352358
monitor = sc.DataArray(
353359
sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'),
354360
coords={'w': sc.array(dims=['w'], values=[-0.5, 2, 3], unit='Å')},
@@ -359,10 +365,14 @@ def test_normalize_by_monitor_histogram_zero_count_bins_are_ignored() -> None:
359365
uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail,
360366
)
361367

362-
expected = sc.DataArray(
363-
sc.array(dims=['w'], values=[0.0, 110 / 7, 110 / 7], unit='counts'),
364-
coords={'w': sc.arange('w', 3.0, unit='Å')},
365-
).bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å'))
368+
expected = (
369+
sc.DataArray(
370+
sc.array(dims=['w'], values=[0.0, 110 / 7, 110 / 7, 0], unit='counts'),
371+
coords={'w': sc.arange('w', 4.0, unit='Å')},
372+
)
373+
.bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å'))
374+
.assign_masks(m=sc.array(dims=['w'], values=[True, False, False, True]))
375+
)
366376

367377
sc.testing.assert_allclose(normalized, expected)
368378

0 commit comments

Comments
 (0)