From 02682c7a8403beb5bb9af49083bc7a19acff7c02 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Wed, 2 Jul 2025 15:13:43 +0200 Subject: [PATCH 01/12] Add monitor normalization --- src/ess/reduce/correction.py | 193 ++++++++++++++ tests/correction_test.py | 476 +++++++++++++++++++++++++++++++++++ 2 files changed, 669 insertions(+) create mode 100644 src/ess/reduce/correction.py create mode 100644 tests/correction_test.py diff --git a/src/ess/reduce/correction.py b/src/ess/reduce/correction.py new file mode 100644 index 00000000..f81069fb --- /dev/null +++ b/src/ess/reduce/correction.py @@ -0,0 +1,193 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +"""Correction algorithms for neutron data reduction.""" + +import scipp as sc + +from .uncertainty import UncertaintyBroadcastMode, broadcast_uncertainties + + +def normalize_by_monitor_histogram( + detector: sc.DataArray, + *, + monitor: sc.DataArray, + uncertainty_broadcast_mode: UncertaintyBroadcastMode, +) -> sc.DataArray: + """Normalize detector data by a histogrammed monitor. + + First, the monitor is clipped to the range of the detector + + .. math:: + + \\bar{m}_i = m_i I(x_i, x_{i+1}), + + where :math:`m_i` is the monitor intensity in bin :math:`i`, + :math:`x_i` is the lower bin edge of bin :math:`i`, and + :math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector. + + The detector bins :math:`d_i` are normalized according to + + .. math:: + + d_i^\\text{Norm} = \\frac{d_i}{\\bar{m}_i} \\Delta x_i + \\frac{\\sum_j\\,\\bar{m}_j}{\\sum_j\\,\\Delta x_j} + + where :math:`\\Delta x_i` is the width of monitor bin :math:`i` (see below). + This normalization leads to a result that has the same + unit as the input detector data. + + Monitor bin :math:`i` is chosen according to: + + - *Histogrammed detector*: The monitor is + `rebinned `_ + to the detector binning. This distributes the monitor weights to the + detector bins. + - *Binned detector*: The monitor value for bin :math:`i` is determined via + :func:`scipp.lookup`. This means that for each event, the monitor value + is obtained from the monitor histogram at that event coordinate value. + + This function is based on the implementation in + `NormaliseToMonitor `_ + of Mantid. + + Parameters + ---------- + detector: + Input detector data. + Must have a coordinate named ``monitor.dim``, that is, the single + dimension name of the monitor. + monitor: + A histogrammed monitor. + Must be one-dimensional and have a dimension coordinate, typically "wavelength". + uncertainty_broadcast_mode: + Choose how uncertainties of the monitor are broadcast to the sample data. + + Returns + ------- + : + ``detector`` normalized by ``monitor``. + + See also + -------- + normalize_by_monitor_integrated: + Normalize by an integrated monitor. + """ + dim = monitor.dim + + clipped = _clip_monitor_to_detector_range(monitor=monitor, detector=detector) + coord = clipped.coords[dim] + delta_w = coord[1:] - coord[:-1] + total_monitor_weight = broadcast_uncertainties( + clipped.sum() / delta_w.sum(), + prototype=clipped, + mode=uncertainty_broadcast_mode, + ) + delta_w *= total_monitor_weight + norm = broadcast_uncertainties( + clipped / delta_w, prototype=detector, mode=uncertainty_broadcast_mode + ) + + if detector.bins is None: + return detector / norm + return detector.bins / sc.lookup(norm, dim=dim) + + +def normalize_by_monitor_integrated( + detector: sc.DataArray, + *, + monitor: sc.DataArray, + uncertainty_broadcast_mode: UncertaintyBroadcastMode, +) -> sc.DataArray: + """Normalize detector data by an integrated monitor. + + The monitor is integrated according to + + .. math:: + + M = \\sum_{i=0}^{N-1}\\, m_i (x_{i+1} - x_i) I(x_i, x_{i+1}), + + where :math:`m_i` is the monitor intensity in bin :math:`i`, + :math:`x_i` is the lower bin edge of bin :math:`i`, and + :math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector. + + Parameters + ---------- + detector: + Input detector data. + monitor: + A histogrammed monitor. + Must be one-dimensional and have a dimension coordinate, typically "wavelength". + uncertainty_broadcast_mode: + Choose how uncertainties of the monitor are broadcast to the sample data. + + Returns + ------- + : + `detector` normalized by a monitor. + + See also + -------- + normalize_by_monitor_histogram: + Normalize by a monitor histogram without integration. + """ + clipped = _clip_monitor_to_detector_range(monitor=monitor, detector=detector) + coord = clipped.coords[clipped.dim] + norm = (clipped * (coord[1:] - coord[:-1])).data.sum() + norm = broadcast_uncertainties( + norm, prototype=detector, mode=uncertainty_broadcast_mode + ) + return detector / norm + + +def _clip_monitor_to_detector_range( + *, monitor: sc.DataArray, detector: sc.DataArray +) -> sc.DataArray: + dim = monitor.dim + if not monitor.coords.is_edges(dim): + raise sc.CoordError( + f"Monitor coordinate '{dim}' must be bin-edges to integrate the monitor." + ) + + # Prefer a bin coord over an event coord because this makes the behavior for binned + # and histogrammed data consistent. If we used an event coord, we might allow a + # monitor range that is less than the detector bins which is fine for the vents, + # but would be wrong if the detector was subsequently histogrammed. + if dim in detector.coords: + det_coord = detector.coords[dim] + + # Mask zero-count bins, which are an artifact from the rectangular 2-D binning. + # The wavelength of those bins must be excluded when determining the range. + if detector.bins is None: + mask = detector.data == sc.scalar(0.0, unit=detector.unit) + else: + mask = detector.data.bins.size() == sc.scalar(0.0, unit=None) + lo = ( + sc.DataArray(det_coord[dim, :-1], masks={'zero_counts': mask}).nanmin().data + ) + hi = sc.DataArray(det_coord[dim, 1:], masks={'zero_counts': mask}).nanmax().data + + elif dim in detector.bins.coords: + det_coord = detector.bins.coords[dim] + # No need to mask here because we have the exact event coordinate values. + lo = det_coord.nanmin() + hi = det_coord.nanmax() + + else: + raise sc.CoordError( + f"Missing '{dim}' coordinate in detector for monitor normalization." + ) + + if monitor.coords[dim].min() > lo or monitor.coords[dim].max() < hi: + raise ValueError( + f"Cannot normalize by monitor: The {dim} range of the monitor " + f"({monitor.coords[dim].min().value} to {monitor.coords[dim].max().value}) " + f"is smaller than the range of the detector ({lo.value} to {hi.value})." + ) + + if detector.bins is None: + # If we didn't rebin to the detector coord here, then, for a finer monitor + # binning than detector, the lookup table would extract one monitor value for + # each detector bin and ignore other values lying in the same detector bin. + # But integration would pick up all monitor bins. + return monitor.rebin({dim: det_coord}) + return monitor[dim, lo:hi] diff --git a/tests/correction_test.py b/tests/correction_test.py new file mode 100644 index 00000000..3ed6fcbc --- /dev/null +++ b/tests/correction_test.py @@ -0,0 +1,476 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) + +import pytest +import scipp as sc +import scipp.testing + +from ess.reduce.correction import ( + normalize_by_monitor_histogram, + normalize_by_monitor_integrated, +) +from ess.reduce.uncertainty import UncertaintyBroadcastMode + + +def test_normalize_by_monitor_histogram_aligned_bins_w_event_coord() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 44 / 3, 55 / 3], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_aligned_bins_wo_event_coord() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 44 / 3, 55 / 3], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_aligned_bins_hist() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[44 / 3, 55 / 3], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_monitor_envelops_detector_bin() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-1, 1.5, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 55 / 4, 165 / 8], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_monitor_envelops_detector_bin_hist() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-1, 1.5, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + # These values are different from the case with binned data in + # test_normalize_by_monitor_histogram_monitor_envelops_detector_bin + # because the monitor gets rebinned to match the detector bins. + expected = sc.DataArray( + sc.array(dims=['w'], values=[11.2, 21.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')}, + ) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_detector_envelops_monitor_bin() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 1.5, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0, 2, 2.5], unit='Å')}, + ) + with pytest.raises(ValueError, match="smaller than the range of the detector"): + normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + +def test_normalize_by_monitor_histogram_detector_envelops_monitor_bin_hist() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 1.5, 3], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0, 2, 2.5], unit='Å')}, + ) + with pytest.raises(ValueError, match="smaller than the range of the detector"): + normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + +def test_normalize_by_monitor_histogram_monitor_extra_bins_in_monitor() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[4.0, 5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 44 / 3, 55 / 3], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_monitor_extra_bins_in_monitor_hist() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[4.0, 5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[44 / 3, 55 / 3], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_monitor_extra_bins_in_detector() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[-10, 0, 10, 30, 40], unit='counts'), + coords={'w': sc.arange('w', -1.0, 4.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0, 2, 3], unit='Å')}, + ) + with pytest.raises(ValueError, match="smaller than the range of the detector"): + normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + +def test_normalize_by_monitor_histogram_monitor_finer_bins_in_detector() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 44 / 3, 55 / 3], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_monitor_finer_bins_in_detector_hist() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 44 / 3, 55 / 3], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, + ) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_monitor_finer_bins_in_monitor() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 8.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 95 / 12, 95 / 3], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + + sc.testing.assert_allclose(normalized, expected) + + +def test_normalize_by_monitor_histogram_monitor_finer_bins_in_monitor_hist() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 8.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[380 / 39, 95 / 3], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + + sc.testing.assert_allclose(normalized, expected) + + +def test_normalize_by_monitor_histogram_zero_count_bins_are_ignored_hist() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30, 0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-0.5, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + # The monitor is rebinned to the detector bins, which introduces + # a 0/0 in the last bin. + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 11, 11, float('NaN')], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, + ) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_zero_count_bins_are_ignored() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-0.5, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 110 / 7, 110 / 7], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')) + + sc.testing.assert_allclose(normalized, expected) + + +def test_normalize_by_monitor_integrated_expected_results() -> None: + detector = sc.DataArray( + sc.arange('wavelength', 1, 4, unit='counts'), + coords={'wavelength': sc.arange('wavelength', 3.0, unit='Å')}, + ).bin(wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['wavelength'], values=[4.0, 5.0, 6.0], unit='counts'), + coords={ + 'wavelength': sc.array( + dims=['wavelength'], values=[0.0, 0.5, 2, 3], unit='Å' + ) + }, + ) + normalized = normalize_by_monitor_integrated( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + expected = detector / sc.scalar(4 * 0.5 + 5 * 1.5 + 6 * 1, unit='counts * Å') + sc.testing.assert_identical(normalized, expected) + + +@pytest.mark.parametrize('event_coord', [True, False]) +def test_normalize_by_monitor_integrated_ignores_monitor_values_out_of_range( + event_coord: bool, +) -> None: + detector = sc.DataArray( + sc.arange('wavelength', 4, unit='counts'), + coords={'wavelength': sc.arange('wavelength', 4.0, unit='Å')}, + ) + if event_coord: + # Make sure event at 3 is included + detector = detector.bin( + wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3.1], unit='Å') + ) + del detector.coords['wavelength'] + else: + detector = detector.bin( + wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3], unit='Å') + ) + del detector.bins.coords['wavelength'] + monitor = sc.DataArray( + sc.array(dims=['wavelength'], values=[4.0, 10.0], unit='counts'), + coords={ + 'wavelength': sc.array(dims=['wavelength'], values=[0.0, 3, 4], unit='Å') + }, + ) + normalized = normalize_by_monitor_integrated( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + expected = detector / sc.scalar(4.0 * 3, unit='counts') + sc.testing.assert_identical(normalized, expected) + + +@pytest.mark.parametrize('event_coord', [True, False]) +def test_normalize_by_monitor_integrated_uses_monitor_values_at_boundary( + event_coord: bool, +) -> None: + detector = sc.DataArray( + sc.arange('wavelength', 4, unit='counts'), + coords={'wavelength': sc.arange('wavelength', 4.0, unit='Å')}, + ) + if event_coord: + # Make sure event at 3 is included + detector = detector.bin( + wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3.1], unit='Å') + ) + del detector.coords['wavelength'] + else: + detector = detector.bin( + wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3], unit='Å') + ) + del detector.bins.coords['wavelength'] + monitor = sc.DataArray( + sc.array(dims=['wavelength'], values=[4.0, 10.0], unit='counts'), + coords={ + 'wavelength': sc.array(dims=['wavelength'], values=[0.0, 2, 4], unit='Å') + }, + ) + normalized = normalize_by_monitor_integrated( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + expected = detector / sc.scalar(4.0 * 2 + 10.0 * 2, unit='counts') + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_integrated_raises_if_monitor_range_too_narrow() -> None: + detector = sc.DataArray( + sc.arange('wavelength', 3, unit='counts'), + coords={'wavelength': sc.arange('wavelength', 3.0, unit='Å')}, + ).bin(wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['wavelength'], values=[4.0, 10.0], unit='counts'), + coords={ + 'wavelength': sc.array(dims=['wavelength'], values=[1.0, 3, 4], unit='Å') + }, + ) + with pytest.raises(ValueError, match="smaller than the range of the detector"): + normalize_by_monitor_integrated( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) From fcb95f9e71951e4733d4e4a1e0e22ff97a9e2cfc Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Mon, 20 Oct 2025 10:26:08 +0200 Subject: [PATCH 02/12] Rename correction to normalization --- src/ess/reduce/__init__.py | 4 ++-- src/ess/reduce/{correction.py => normalization.py} | 2 +- tests/{correction_test.py => normalization_test.py} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/ess/reduce/{correction.py => normalization.py} (99%) rename tests/{correction_test.py => normalization_test.py} (99%) diff --git a/src/ess/reduce/__init__.py b/src/ess/reduce/__init__.py index a9175c8b..d65bb3aa 100644 --- a/src/ess/reduce/__init__.py +++ b/src/ess/reduce/__init__.py @@ -4,7 +4,7 @@ import importlib.metadata -from . import nexus, time_of_flight, uncertainty +from . import nexus, normalization, time_of_flight, uncertainty try: __version__ = importlib.metadata.version("essreduce") @@ -13,4 +13,4 @@ del importlib -__all__ = ["nexus", "time_of_flight", "uncertainty"] +__all__ = ["nexus", "normalization", "time_of_flight", "uncertainty"] diff --git a/src/ess/reduce/correction.py b/src/ess/reduce/normalization.py similarity index 99% rename from src/ess/reduce/correction.py rename to src/ess/reduce/normalization.py index f81069fb..7cfa10ad 100644 --- a/src/ess/reduce/correction.py +++ b/src/ess/reduce/normalization.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2025 Scipp contributors (https://github.com/scipp) -"""Correction algorithms for neutron data reduction.""" +"""Normalization routines for neutron data reduction.""" import scipp as sc diff --git a/tests/correction_test.py b/tests/normalization_test.py similarity index 99% rename from tests/correction_test.py rename to tests/normalization_test.py index 3ed6fcbc..61c6e547 100644 --- a/tests/correction_test.py +++ b/tests/normalization_test.py @@ -5,7 +5,7 @@ import scipp as sc import scipp.testing -from ess.reduce.correction import ( +from ess.reduce.normalization import ( normalize_by_monitor_histogram, normalize_by_monitor_integrated, ) From 341ccd864041aa485eae431eea3a91594ca4cecd Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Mon, 20 Oct 2025 10:27:04 +0200 Subject: [PATCH 03/12] Fix typo --- src/ess/reduce/normalization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ess/reduce/normalization.py b/src/ess/reduce/normalization.py index 7cfa10ad..86866a52 100644 --- a/src/ess/reduce/normalization.py +++ b/src/ess/reduce/normalization.py @@ -150,7 +150,7 @@ def _clip_monitor_to_detector_range( # Prefer a bin coord over an event coord because this makes the behavior for binned # and histogrammed data consistent. If we used an event coord, we might allow a - # monitor range that is less than the detector bins which is fine for the vents, + # monitor range that is less than the detector bins which is fine for the events, # but would be wrong if the detector was subsequently histogrammed. if dim in detector.coords: det_coord = detector.coords[dim] From 214f6455bdfa23b5e8ad5c4887f799a72dd3b5a3 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Mon, 20 Oct 2025 10:33:40 +0200 Subject: [PATCH 04/12] Add normalization to docs --- docs/api-reference/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md index 50d92e43..59650582 100644 --- a/docs/api-reference/index.md +++ b/docs/api-reference/index.md @@ -30,6 +30,7 @@ live logging nexus + normalization streaming time_of_flight ui From ff6f184d807913a4fca8b856ed8d51300a40036c Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Mon, 20 Oct 2025 10:42:35 +0200 Subject: [PATCH 05/12] Improve docs --- src/ess/reduce/normalization.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ess/reduce/normalization.py b/src/ess/reduce/normalization.py index 86866a52..fe722a54 100644 --- a/src/ess/reduce/normalization.py +++ b/src/ess/reduce/normalization.py @@ -32,7 +32,8 @@ def normalize_by_monitor_histogram( d_i^\\text{Norm} = \\frac{d_i}{\\bar{m}_i} \\Delta x_i \\frac{\\sum_j\\,\\bar{m}_j}{\\sum_j\\,\\Delta x_j} - where :math:`\\Delta x_i` is the width of monitor bin :math:`i` (see below). + where :math:`\\Delta x_i = x_{i+1} - x_i` is the width of + monitor bin :math:`i` (see below). This normalization leads to a result that has the same unit as the input detector data. @@ -46,16 +47,16 @@ def normalize_by_monitor_histogram( :func:`scipp.lookup`. This means that for each event, the monitor value is obtained from the monitor histogram at that event coordinate value. - This function is based on the implementation in + This function is based on the implementation of `NormaliseToMonitor `_ - of Mantid. + in Mantid. Parameters ---------- detector: Input detector data. Must have a coordinate named ``monitor.dim``, that is, the single - dimension name of the monitor. + dimension name of the **monitor**. monitor: A histogrammed monitor. Must be one-dimensional and have a dimension coordinate, typically "wavelength". From f22929408c6209692b07986b4edc2e2ef84b31fb Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Mon, 20 Oct 2025 13:14:27 +0200 Subject: [PATCH 06/12] Respect masks in range calculation --- src/ess/reduce/normalization.py | 53 +++++++++++++++++++-------------- tests/normalization_test.py | 30 ++++++++++++------- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/ess/reduce/normalization.py b/src/ess/reduce/normalization.py index fe722a54..cdb8f0b4 100644 --- a/src/ess/reduce/normalization.py +++ b/src/ess/reduce/normalization.py @@ -7,6 +7,7 @@ from .uncertainty import UncertaintyBroadcastMode, broadcast_uncertainties +# TODO explain impact of masking, esp multi-dim def normalize_by_monitor_histogram( detector: sc.DataArray, *, @@ -47,6 +48,14 @@ def normalize_by_monitor_histogram( :func:`scipp.lookup`. This means that for each event, the monitor value is obtained from the monitor histogram at that event coordinate value. + .. Attention:: + + Masked bins in ``detector`` are ignored when clipping the monitor and therefore + impact the normalization factor. + The output's masked bins are normalized using the same factor and may + be incorrect and even contain NaN. + You should only drop masks after normalization if you know what you are doing. + This function is based on the implementation of `NormaliseToMonitor `_ in Mantid. @@ -111,6 +120,14 @@ def normalize_by_monitor_integrated( :math:`x_i` is the lower bin edge of bin :math:`i`, and :math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector. + .. Attention:: + + Masked bins in ``detector`` are ignored when clipping the monitor and therefore + impact the normalization factor. + The output's masked bins are normalized using the same factor and may + be incorrect and even contain NaN. + You should only drop masks after normalization if you know what you are doing. + Parameters ---------- detector: @@ -149,30 +166,22 @@ def _clip_monitor_to_detector_range( f"Monitor coordinate '{dim}' must be bin-edges to integrate the monitor." ) + # Reduce with `all` instead of `any` to include bins in range calculations + # that contain any unmasked data. + masks = { + name: mask.all(set(mask.dims) - {dim}) for name, mask in detector.masks.items() + } + # Prefer a bin coord over an event coord because this makes the behavior for binned # and histogrammed data consistent. If we used an event coord, we might allow a # monitor range that is less than the detector bins which is fine for the events, # but would be wrong if the detector was subsequently histogrammed. - if dim in detector.coords: - det_coord = detector.coords[dim] - - # Mask zero-count bins, which are an artifact from the rectangular 2-D binning. - # The wavelength of those bins must be excluded when determining the range. - if detector.bins is None: - mask = detector.data == sc.scalar(0.0, unit=detector.unit) - else: - mask = detector.data.bins.size() == sc.scalar(0.0, unit=None) - lo = ( - sc.DataArray(det_coord[dim, :-1], masks={'zero_counts': mask}).nanmin().data - ) - hi = sc.DataArray(det_coord[dim, 1:], masks={'zero_counts': mask}).nanmax().data - - elif dim in detector.bins.coords: - det_coord = detector.bins.coords[dim] - # No need to mask here because we have the exact event coordinate values. - lo = det_coord.nanmin() - hi = det_coord.nanmax() - + if (det_coord := detector.coords.get(dim)) is not None: + lo = sc.DataArray(det_coord[dim, :-1], masks=masks).nanmin().data + hi = sc.DataArray(det_coord[dim, 1:], masks=masks).nanmax().data + elif (det_coord := detector.bins.coords.get(dim)) is not None: + lo = sc.DataArray(det_coord, masks=masks).nanmin().data + hi = sc.DataArray(det_coord, masks=masks).nanmax().data else: raise sc.CoordError( f"Missing '{dim}' coordinate in detector for monitor normalization." @@ -181,8 +190,8 @@ def _clip_monitor_to_detector_range( if monitor.coords[dim].min() > lo or monitor.coords[dim].max() < hi: raise ValueError( f"Cannot normalize by monitor: The {dim} range of the monitor " - f"({monitor.coords[dim].min().value} to {monitor.coords[dim].max().value}) " - f"is smaller than the range of the detector ({lo.value} to {hi.value})." + f"({monitor.coords[dim].min():c} to {monitor.coords[dim].max():c}) " + f"is smaller than the range of the detector ({lo:c} to {hi:c})." ) if detector.bins is None: diff --git a/tests/normalization_test.py b/tests/normalization_test.py index 61c6e547..79de3fff 100644 --- a/tests/normalization_test.py +++ b/tests/normalization_test.py @@ -319,10 +319,11 @@ def test_normalize_by_monitor_histogram_monitor_finer_bins_in_monitor_hist() -> sc.testing.assert_allclose(normalized, expected) -def test_normalize_by_monitor_histogram_zero_count_bins_are_ignored_hist() -> None: +def test_normalize_by_monitor_histogram_masked_bins_are_ignored_hist() -> None: detector = sc.DataArray( sc.array(dims=['w'], values=[0, 10, 30, 0], unit='counts'), coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, + masks={'m': sc.array(dims=['w'], values=[True, False, False, True])}, ) monitor = sc.DataArray( 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 expected = sc.DataArray( sc.array(dims=['w'], values=[0.0, 11, 11, float('NaN')], unit='counts'), coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, + masks={'m': sc.array(dims=['w'], values=[True, False, False, True])}, ) sc.testing.assert_identical(normalized, expected) -def test_normalize_by_monitor_histogram_zero_count_bins_are_ignored() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')) +def test_normalize_by_monitor_histogram_masked_bins_are_ignored() -> None: + detector = ( + sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30, 40], unit='counts'), + coords={'w': sc.arange('w', 4.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')) + .assign_masks(m=sc.array(dims=['w'], values=[True, False, False, True])) + ) monitor = sc.DataArray( sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), 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: uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) - expected = sc.DataArray( - sc.array(dims=['w'], values=[0.0, 110 / 7, 110 / 7], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')) + expected = ( + sc.DataArray( + sc.array(dims=['w'], values=[0.0, 110 / 7, 110 / 7, 0], unit='counts'), + coords={'w': sc.arange('w', 4.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')) + .assign_masks(m=sc.array(dims=['w'], values=[True, False, False, True])) + ) sc.testing.assert_allclose(normalized, expected) From b4987e05b24a481126a4163d5bac631f2e723ddc Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Mon, 20 Oct 2025 15:29:28 +0200 Subject: [PATCH 07/12] Apply monitor masks --- src/ess/reduce/normalization.py | 47 +++++++++- tests/normalization_test.py | 147 ++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 3 deletions(-) diff --git a/src/ess/reduce/normalization.py b/src/ess/reduce/normalization.py index cdb8f0b4..07db6fe8 100644 --- a/src/ess/reduce/normalization.py +++ b/src/ess/reduce/normalization.py @@ -2,12 +2,13 @@ # Copyright (c) 2025 Scipp contributors (https://github.com/scipp) """Normalization routines for neutron data reduction.""" +import itertools + import scipp as sc from .uncertainty import UncertaintyBroadcastMode, broadcast_uncertainties -# TODO explain impact of masking, esp multi-dim def normalize_by_monitor_histogram( detector: sc.DataArray, *, @@ -84,14 +85,15 @@ def normalize_by_monitor_histogram( """ dim = monitor.dim + detector = _mask_detector_for_norm(detector=detector, monitor=monitor) clipped = _clip_monitor_to_detector_range(monitor=monitor, detector=detector) coord = clipped.coords[dim] - delta_w = coord[1:] - coord[:-1] + delta_w = sc.DataArray(coord[1:] - coord[:-1], masks=clipped.masks) total_monitor_weight = broadcast_uncertainties( clipped.sum() / delta_w.sum(), prototype=clipped, mode=uncertainty_broadcast_mode, - ) + ).data delta_w *= total_monitor_weight norm = broadcast_uncertainties( clipped / delta_w, prototype=detector, mode=uncertainty_broadcast_mode @@ -148,6 +150,7 @@ def normalize_by_monitor_integrated( normalize_by_monitor_histogram: Normalize by a monitor histogram without integration. """ + detector = _mask_detector_for_norm(detector=detector, monitor=monitor) clipped = _clip_monitor_to_detector_range(monitor=monitor, detector=detector) coord = clipped.coords[clipped.dim] norm = (clipped * (coord[1:] - coord[:-1])).data.sum() @@ -201,3 +204,41 @@ def _clip_monitor_to_detector_range( # But integration would pick up all monitor bins. return monitor.rebin({dim: det_coord}) return monitor[dim, lo:hi] + + +def _mask_detector_for_norm( + *, detector: sc.DataArray, monitor: sc.DataArray +) -> sc.DataArray: + """Mask the detector where the monitor is masked. + + For performance, this applies the monitor mask to the detector bins. + This can lead to masking more vents than strictly necessary if we + used an event mask. + """ + if (monitor_mask := _monitor_mask(monitor)) is None: + return detector + + # Use rebin to reshape the mask to the detector: + dim = monitor.dim + mask = sc.DataArray(monitor_mask, coords={dim: monitor.coords[dim]}).rebin( + {dim: detector.coords[dim]} + ).data != sc.scalar(0, unit=None) + return detector.assign_masks({"_monitor_mask": mask}) + + +def _monitor_mask(monitor: sc.DataArray) -> sc.Variable | None: + """Mask nonfinite monitor values and combine all masks.""" + masks = monitor.masks.values() + + finite = sc.isfinite(monitor.data) + if not finite.all(): + masks = itertools.chain(masks, (~finite,)) + + mask = None + for m in masks: + if mask is None: + mask = m + else: + mask |= m + + return mask diff --git a/tests/normalization_test.py b/tests/normalization_test.py index 79de3fff..10fe7444 100644 --- a/tests/normalization_test.py +++ b/tests/normalization_test.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2025 Scipp contributors (https://github.com/scipp) +import numpy as np import pytest import scipp as sc import scipp.testing @@ -484,3 +485,149 @@ def test_normalize_by_monitor_integrated_raises_if_monitor_range_too_narrow() -> monitor=monitor, uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) + + +def test_normalize_by_monitor_histogram_monitor_mask_aligned_bins() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')}, + masks={'m': sc.array(dims=['w'], values=[False, True, False])}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = ( + sc.DataArray( + sc.array(dims=['w'], values=[0.0, 9.6, 0, 216 / 7], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + .assign_masks(_monitor_mask=sc.array(dims=['w'], values=[False, True, False])) + ) + + sc.testing.assert_allclose(normalized, expected) + + +def test_normalize_by_monitor_histogram_monitor_mask_multiple() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')}, + masks={ + 'm1': sc.array(dims=['w'], values=[False, True, False]), + 'm2': sc.array(dims=['w'], values=[False, True, True]), + }, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = ( + sc.DataArray( + sc.array(dims=['w'], values=[0.0, 10, 0, 0], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + .assign_masks(_monitor_mask=sc.array(dims=['w'], values=[False, True, True])) + ) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_monitor_mask_unaligned_bins() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0, 7.0, 8.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3.5, 4, 7], unit='Å')}, + masks={'m': sc.array(dims=['w'], values=[False, True, False, False])}, + ) + + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = ( + sc.DataArray( + sc.array(dims=['w'], values=[0.0, 0, 0, 30], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + .assign_masks(_monitor_mask=sc.array(dims=['w'], values=[True, True, False])) + ) + + sc.testing.assert_identical(normalized, expected) + + +def test_normalize_by_monitor_histogram_monitor_mask_at_edge() -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + masks={'m': sc.array(dims=['w'], values=[False, True])}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = ( + sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 0], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + .assign_masks(_monitor_mask=sc.array(dims=['w'], values=[False, True])) + ) + + sc.testing.assert_identical(normalized, expected) + + +@pytest.mark.parametrize("nonfinite_value", [np.nan, np.inf]) +def test_normalize_by_monitor_histogram_nonfinite_in_monitor_is_masked( + nonfinite_value: float, +) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[nonfinite_value, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = ( + sc.DataArray( + sc.array(dims=['w'], values=[0.0, 0.0, 65 / 6, 585 / 14], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + .assign_masks(_monitor_mask=sc.array(dims=['w'], values=[True, False, False])) + ) + + sc.testing.assert_allclose(normalized, expected) From eeafdcb40b4551912504be9eef1485d133b2d7f0 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Fri, 24 Oct 2025 10:56:30 +0200 Subject: [PATCH 08/12] Fix typo --- src/ess/reduce/normalization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ess/reduce/normalization.py b/src/ess/reduce/normalization.py index 07db6fe8..b8ff0618 100644 --- a/src/ess/reduce/normalization.py +++ b/src/ess/reduce/normalization.py @@ -212,7 +212,7 @@ def _mask_detector_for_norm( """Mask the detector where the monitor is masked. For performance, this applies the monitor mask to the detector bins. - This can lead to masking more vents than strictly necessary if we + This can lead to masking more events than strictly necessary if we used an event mask. """ if (monitor_mask := _monitor_mask(monitor)) is None: From 7cba862538f5549130ae0fce01b29a3aecd7dec9 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Fri, 24 Oct 2025 11:07:49 +0200 Subject: [PATCH 09/12] Do not use in-place op The in-place or modified an input mask. --- src/ess/reduce/normalization.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/ess/reduce/normalization.py b/src/ess/reduce/normalization.py index b8ff0618..0d977776 100644 --- a/src/ess/reduce/normalization.py +++ b/src/ess/reduce/normalization.py @@ -2,7 +2,7 @@ # Copyright (c) 2025 Scipp contributors (https://github.com/scipp) """Normalization routines for neutron data reduction.""" -import itertools +import functools import scipp as sc @@ -228,17 +228,12 @@ def _mask_detector_for_norm( def _monitor_mask(monitor: sc.DataArray) -> sc.Variable | None: """Mask nonfinite monitor values and combine all masks.""" - masks = monitor.masks.values() + masks = list(monitor.masks.values()) finite = sc.isfinite(monitor.data) if not finite.all(): - masks = itertools.chain(masks, (~finite,)) + masks.append(~finite) - mask = None - for m in masks: - if mask is None: - mask = m - else: - mask |= m - - return mask + if not masks: + return None + return functools.reduce(sc.logical_or, masks) From 2397d65a2daf1b71750bb2ed798e6576f9231618 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Fri, 24 Oct 2025 11:11:10 +0200 Subject: [PATCH 10/12] Add explanation of _monitor_mask --- src/ess/reduce/normalization.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ess/reduce/normalization.py b/src/ess/reduce/normalization.py index 0d977776..34cd9648 100644 --- a/src/ess/reduce/normalization.py +++ b/src/ess/reduce/normalization.py @@ -77,6 +77,8 @@ def normalize_by_monitor_histogram( ------- : ``detector`` normalized by ``monitor``. + If the monitor has masks or contains non-finite values, the output has a mask + called '_monitor_mask' constructed from the monitor masks and non-finite values. See also -------- @@ -144,6 +146,8 @@ def normalize_by_monitor_integrated( ------- : `detector` normalized by a monitor. + If the monitor has masks or contains non-finite values, the output has a mask + called '_monitor_mask' constructed from the monitor masks and non-finite values. See also -------- From 53b861fdbd77bb58c4030f5932de4d574c97b274 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Fri, 7 Nov 2025 16:21:18 +0100 Subject: [PATCH 11/12] Do not clip and no extra integral --- src/ess/reduce/normalization.py | 128 ++-- tests/normalization_test.py | 1200 ++++++++++++++++--------------- 2 files changed, 665 insertions(+), 663 deletions(-) diff --git a/src/ess/reduce/normalization.py b/src/ess/reduce/normalization.py index 34cd9648..2abf47b4 100644 --- a/src/ess/reduce/normalization.py +++ b/src/ess/reduce/normalization.py @@ -15,51 +15,26 @@ def normalize_by_monitor_histogram( monitor: sc.DataArray, uncertainty_broadcast_mode: UncertaintyBroadcastMode, ) -> sc.DataArray: - """Normalize detector data by a histogrammed monitor. + """Normalize detector data by a normalized histogrammed monitor. - First, the monitor is clipped to the range of the detector + This normalization accounts for both the (wavelength) profile of the incident beam + and the integrated neutron flux, meaning measurement duration and source strength. - .. math:: - - \\bar{m}_i = m_i I(x_i, x_{i+1}), + - For *event* detectors, the monitor values are mapped to the detector + using :func:`scipp.lookup`. That is, for detector event :math:`d_i`, + :math:`m_i` is the monitor bin value at the same coordinate. + - For *histogram* detectors, the monitor is rebinned using to the detector + binning using :func:`scipp.rebin`. Thus, detector value :math:`d_i` and + monitor value :math:`m_i` correspond to the same bin. - where :math:`m_i` is the monitor intensity in bin :math:`i`, - :math:`x_i` is the lower bin edge of bin :math:`i`, and - :math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector. + In both cases, let :math:`x_i` be the lower bound of monitor bin :math:`i` + and let :math:`\\Delta x_i = x_{i+1} - x_i` be the width of that bin. - The detector bins :math:`d_i` are normalized according to + The detector is normalized according to .. math:: - d_i^\\text{Norm} = \\frac{d_i}{\\bar{m}_i} \\Delta x_i - \\frac{\\sum_j\\,\\bar{m}_j}{\\sum_j\\,\\Delta x_j} - - where :math:`\\Delta x_i = x_{i+1} - x_i` is the width of - monitor bin :math:`i` (see below). - This normalization leads to a result that has the same - unit as the input detector data. - - Monitor bin :math:`i` is chosen according to: - - - *Histogrammed detector*: The monitor is - `rebinned `_ - to the detector binning. This distributes the monitor weights to the - detector bins. - - *Binned detector*: The monitor value for bin :math:`i` is determined via - :func:`scipp.lookup`. This means that for each event, the monitor value - is obtained from the monitor histogram at that event coordinate value. - - .. Attention:: - - Masked bins in ``detector`` are ignored when clipping the monitor and therefore - impact the normalization factor. - The output's masked bins are normalized using the same factor and may - be incorrect and even contain NaN. - You should only drop masks after normalization if you know what you are doing. - - This function is based on the implementation of - `NormaliseToMonitor `_ - in Mantid. + d_i^\\text{Norm} = \\frac{d_i}{m_i} \\Delta x_i Parameters ---------- @@ -85,24 +60,21 @@ def normalize_by_monitor_histogram( normalize_by_monitor_integrated: Normalize by an integrated monitor. """ + _check_monitor_range_contains_detector(monitor=monitor, detector=detector) + dim = monitor.dim + if detector.bins is None: + monitor = monitor.rebin({dim: detector.coords[dim]}) detector = _mask_detector_for_norm(detector=detector, monitor=monitor) - clipped = _clip_monitor_to_detector_range(monitor=monitor, detector=detector) - coord = clipped.coords[dim] - delta_w = sc.DataArray(coord[1:] - coord[:-1], masks=clipped.masks) - total_monitor_weight = broadcast_uncertainties( - clipped.sum() / delta_w.sum(), - prototype=clipped, - mode=uncertainty_broadcast_mode, - ).data - delta_w *= total_monitor_weight + coord = monitor.coords[dim] + delta_w = sc.DataArray(coord[1:] - coord[:-1], masks=monitor.masks) norm = broadcast_uncertainties( - clipped / delta_w, prototype=detector, mode=uncertainty_broadcast_mode + monitor / delta_w, prototype=detector, mode=uncertainty_broadcast_mode ) if detector.bins is None: - return detector / norm + return detector / norm.rebin({dim: detector.coords[dim]}) return detector.bins / sc.lookup(norm, dim=dim) @@ -114,23 +86,21 @@ def normalize_by_monitor_integrated( ) -> sc.DataArray: """Normalize detector data by an integrated monitor. - The monitor is integrated according to - - .. math:: + This normalization accounts only for the integrated neutron flux, + meaning measurement duration and source strength. + It does *not* account for the (wavelength) profile of the incident beam. + For that, see :func:`normalize_by_monitor_histogram`. - M = \\sum_{i=0}^{N-1}\\, m_i (x_{i+1} - x_i) I(x_i, x_{i+1}), + Let :math:`d_i` be a detector event or the counts in a detector bin. + The normalized detector is - where :math:`m_i` is the monitor intensity in bin :math:`i`, - :math:`x_i` is the lower bin edge of bin :math:`i`, and - :math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector. + .. math:: - .. Attention:: + d_i^\\text{Norm} &= d_i / M \\\\ + M &= \\sum_j\\, m_j (x_{j+1} - x_j)\\,, - Masked bins in ``detector`` are ignored when clipping the monitor and therefore - impact the normalization factor. - The output's masked bins are normalized using the same factor and may - be incorrect and even contain NaN. - You should only drop masks after normalization if you know what you are doing. + where :math:`m_j` is the monitor counts in bin :math:`j` and + :math:`x_j` is the lower bin edge of that bin. Parameters ---------- @@ -152,43 +122,37 @@ def normalize_by_monitor_integrated( See also -------- normalize_by_monitor_histogram: - Normalize by a monitor histogram without integration. + Normalize by a monitor histogram. """ + _check_monitor_range_contains_detector(monitor=monitor, detector=detector) detector = _mask_detector_for_norm(detector=detector, monitor=monitor) - clipped = _clip_monitor_to_detector_range(monitor=monitor, detector=detector) - coord = clipped.coords[clipped.dim] - norm = (clipped * (coord[1:] - coord[:-1])).data.sum() + coord = monitor.coords[monitor.dim] + norm = (monitor * (coord[1:] - coord[:-1])).data.sum() norm = broadcast_uncertainties( norm, prototype=detector, mode=uncertainty_broadcast_mode ) return detector / norm -def _clip_monitor_to_detector_range( +def _check_monitor_range_contains_detector( *, monitor: sc.DataArray, detector: sc.DataArray -) -> sc.DataArray: +) -> None: dim = monitor.dim if not monitor.coords.is_edges(dim): raise sc.CoordError( f"Monitor coordinate '{dim}' must be bin-edges to integrate the monitor." ) - # Reduce with `all` instead of `any` to include bins in range calculations - # that contain any unmasked data. - masks = { - name: mask.all(set(mask.dims) - {dim}) for name, mask in detector.masks.items() - } - # Prefer a bin coord over an event coord because this makes the behavior for binned # and histogrammed data consistent. If we used an event coord, we might allow a # monitor range that is less than the detector bins which is fine for the events, # but would be wrong if the detector was subsequently histogrammed. if (det_coord := detector.coords.get(dim)) is not None: - lo = sc.DataArray(det_coord[dim, :-1], masks=masks).nanmin().data - hi = sc.DataArray(det_coord[dim, 1:], masks=masks).nanmax().data + lo = det_coord[dim, :-1].nanmin() + hi = det_coord[dim, 1:].nanmax() elif (det_coord := detector.bins.coords.get(dim)) is not None: - lo = sc.DataArray(det_coord, masks=masks).nanmin().data - hi = sc.DataArray(det_coord, masks=masks).nanmax().data + lo = det_coord.nanmin() + hi = det_coord.nanmax() else: raise sc.CoordError( f"Missing '{dim}' coordinate in detector for monitor normalization." @@ -201,14 +165,6 @@ def _clip_monitor_to_detector_range( f"is smaller than the range of the detector ({lo:c} to {hi:c})." ) - if detector.bins is None: - # If we didn't rebin to the detector coord here, then, for a finer monitor - # binning than detector, the lookup table would extract one monitor value for - # each detector bin and ignore other values lying in the same detector bin. - # But integration would pick up all monitor bins. - return monitor.rebin({dim: det_coord}) - return monitor[dim, lo:hi] - def _mask_detector_for_norm( *, detector: sc.DataArray, monitor: sc.DataArray diff --git a/tests/normalization_test.py b/tests/normalization_test.py index 10fe7444..37d7e588 100644 --- a/tests/normalization_test.py +++ b/tests/normalization_test.py @@ -13,621 +13,667 @@ from ess.reduce.uncertainty import UncertaintyBroadcastMode -def test_normalize_by_monitor_histogram_aligned_bins_w_event_coord() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = sc.DataArray( - sc.array(dims=['w'], values=[0.0, 44 / 3, 55 / 3], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_aligned_bins_wo_event_coord() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = sc.DataArray( - sc.array(dims=['w'], values=[0.0, 44 / 3, 55 / 3], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_aligned_bins_hist() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[10, 30], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - ) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = sc.DataArray( - sc.array(dims=['w'], values=[44 / 3, 55 / 3], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - ) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_monitor_envelops_detector_bin() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[-1, 1.5, 3], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = sc.DataArray( - sc.array(dims=['w'], values=[0.0, 55 / 4, 165 / 8], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_monitor_envelops_detector_bin_hist() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[10, 30], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')}, - ) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[-1, 1.5, 3], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - # These values are different from the case with binned data in - # test_normalize_by_monitor_histogram_monitor_envelops_detector_bin - # because the monitor gets rebinned to match the detector bins. - expected = sc.DataArray( - sc.array(dims=['w'], values=[11.2, 21.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')}, - ) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_detector_envelops_monitor_bin() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 1.5, 3], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0, 2, 2.5], unit='Å')}, - ) - with pytest.raises(ValueError, match="smaller than the range of the detector"): - normalize_by_monitor_histogram( +class TestNormalizeByMonitorHistogram: + def test_aligned_bins_w_event_coord(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( detector, monitor=monitor, uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 4, 5], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + + sc.testing.assert_identical(normalized, expected) -def test_normalize_by_monitor_histogram_detector_envelops_monitor_bin_hist() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[10, 30], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 1.5, 3], unit='Å')}, - ) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0, 2, 2.5], unit='Å')}, - ) - with pytest.raises(ValueError, match="smaller than the range of the detector"): - normalize_by_monitor_histogram( + def test_aligned_bins_wo_event_coord(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( detector, monitor=monitor, uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 4, 5], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + + sc.testing.assert_identical(normalized, expected) -def test_normalize_by_monitor_histogram_monitor_extra_bins_in_monitor() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[4.0, 5.0, 6.0, 7.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = sc.DataArray( - sc.array(dims=['w'], values=[0.0, 44 / 3, 55 / 3], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_monitor_extra_bins_in_monitor_hist() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[10, 30], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - ) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[4.0, 5.0, 6.0, 7.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = sc.DataArray( - sc.array(dims=['w'], values=[44 / 3, 55 / 3], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - ) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_monitor_extra_bins_in_detector() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[-10, 0, 10, 30, 40], unit='counts'), - coords={'w': sc.arange('w', -1.0, 4.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0, 2, 3], unit='Å')}, - ) - with pytest.raises(ValueError, match="smaller than the range of the detector"): - normalize_by_monitor_histogram( + def test_aligned_bins_hist(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( detector, monitor=monitor, uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) + expected = sc.DataArray( + sc.array(dims=['w'], values=[4.0, 5.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) -def test_normalize_by_monitor_histogram_monitor_finer_bins_in_detector() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = sc.DataArray( - sc.array(dims=['w'], values=[0.0, 44 / 3, 55 / 3], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_monitor_finer_bins_in_detector_hist() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, - ) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = sc.DataArray( - sc.array(dims=['w'], values=[0.0, 44 / 3, 55 / 3], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, - ) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_monitor_finer_bins_in_monitor() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 8.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = sc.DataArray( - sc.array(dims=['w'], values=[0.0, 95 / 12, 95 / 3], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) - - sc.testing.assert_allclose(normalized, expected) - - -def test_normalize_by_monitor_histogram_monitor_finer_bins_in_monitor_hist() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[10, 30], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - ) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 8.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = sc.DataArray( - sc.array(dims=['w'], values=[380 / 39, 95 / 3], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - ) - - sc.testing.assert_allclose(normalized, expected) - - -def test_normalize_by_monitor_histogram_masked_bins_are_ignored_hist() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30, 0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, - masks={'m': sc.array(dims=['w'], values=[True, False, False, True])}, - ) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[-0.5, 2, 3], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - # The monitor is rebinned to the detector bins, which introduces - # a 0/0 in the last bin. - expected = sc.DataArray( - sc.array(dims=['w'], values=[0.0, 11, 11, float('NaN')], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, - masks={'m': sc.array(dims=['w'], values=[True, False, False, True])}, - ) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_masked_bins_are_ignored() -> None: - detector = ( - sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30, 40], unit='counts'), - coords={'w': sc.arange('w', 4.0, unit='Å')}, + sc.testing.assert_identical(normalized, expected) + + def test_monitor_envelops_detector_bin(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-1, 1.5, 3], unit='Å')}, ) - .bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')) - .assign_masks(m=sc.array(dims=['w'], values=[True, False, False, True])) - ) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[-0.5, 2, 3], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = ( - sc.DataArray( - sc.array(dims=['w'], values=[0.0, 110 / 7, 110 / 7, 0], unit='counts'), - coords={'w': sc.arange('w', 4.0, unit='Å')}, + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 5, 7.5], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')) + + sc.testing.assert_identical(normalized, expected) + + def test_monitor_envelops_detector_bin_hist( + self, + ) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-1, 1.5, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) - .bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')) - .assign_masks(m=sc.array(dims=['w'], values=[True, False, False, True])) - ) - sc.testing.assert_allclose(normalized, expected) + # These values are different from the case with binned data in + # test_normalize_by_monitor_histogram_monitor_envelops_detector_bin + # because the monitor gets rebinned to match the detector bins. + expected = sc.DataArray( + sc.array(dims=['w'], values=[4, 15 / 2], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 2.5], unit='Å')}, + ) + sc.testing.assert_identical(normalized, expected) -def test_normalize_by_monitor_integrated_expected_results() -> None: - detector = sc.DataArray( - sc.arange('wavelength', 1, 4, unit='counts'), - coords={'wavelength': sc.arange('wavelength', 3.0, unit='Å')}, - ).bin(wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['wavelength'], values=[4.0, 5.0, 6.0], unit='counts'), - coords={ - 'wavelength': sc.array( - dims=['wavelength'], values=[0.0, 0.5, 2, 3], unit='Å' + def test_detector_envelops_monitor_bin(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 1.5, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0, 2, 2.5], unit='Å')}, + ) + with pytest.raises(ValueError, match="smaller than the range of the detector"): + normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + def test_detector_envelops_monitor_bin_hist( + self, + ) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 1.5, 3], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0, 2, 2.5], unit='Å')}, + ) + with pytest.raises(ValueError, match="smaller than the range of the detector"): + normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) - }, - ) - normalized = normalize_by_monitor_integrated( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - expected = detector / sc.scalar(4 * 0.5 + 5 * 1.5 + 6 * 1, unit='counts * Å') - sc.testing.assert_identical(normalized, expected) - - -@pytest.mark.parametrize('event_coord', [True, False]) -def test_normalize_by_monitor_integrated_ignores_monitor_values_out_of_range( - event_coord: bool, -) -> None: - detector = sc.DataArray( - sc.arange('wavelength', 4, unit='counts'), - coords={'wavelength': sc.arange('wavelength', 4.0, unit='Å')}, - ) - if event_coord: - # Make sure event at 3 is included - detector = detector.bin( - wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3.1], unit='Å') - ) - del detector.coords['wavelength'] - else: - detector = detector.bin( - wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3], unit='Å') - ) - del detector.bins.coords['wavelength'] - monitor = sc.DataArray( - sc.array(dims=['wavelength'], values=[4.0, 10.0], unit='counts'), - coords={ - 'wavelength': sc.array(dims=['wavelength'], values=[0.0, 3, 4], unit='Å') - }, - ) - normalized = normalize_by_monitor_integrated( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - expected = detector / sc.scalar(4.0 * 3, unit='counts') - sc.testing.assert_identical(normalized, expected) - - -@pytest.mark.parametrize('event_coord', [True, False]) -def test_normalize_by_monitor_integrated_uses_monitor_values_at_boundary( - event_coord: bool, -) -> None: - detector = sc.DataArray( - sc.arange('wavelength', 4, unit='counts'), - coords={'wavelength': sc.arange('wavelength', 4.0, unit='Å')}, - ) - if event_coord: - # Make sure event at 3 is included - detector = detector.bin( - wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3.1], unit='Å') - ) - del detector.coords['wavelength'] - else: - detector = detector.bin( - wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3], unit='Å') - ) - del detector.bins.coords['wavelength'] - monitor = sc.DataArray( - sc.array(dims=['wavelength'], values=[4.0, 10.0], unit='counts'), - coords={ - 'wavelength': sc.array(dims=['wavelength'], values=[0.0, 2, 4], unit='Å') - }, - ) - normalized = normalize_by_monitor_integrated( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - expected = detector / sc.scalar(4.0 * 2 + 10.0 * 2, unit='counts') - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_integrated_raises_if_monitor_range_too_narrow() -> None: - detector = sc.DataArray( - sc.arange('wavelength', 3, unit='counts'), - coords={'wavelength': sc.arange('wavelength', 3.0, unit='Å')}, - ).bin(wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['wavelength'], values=[4.0, 10.0], unit='counts'), - coords={ - 'wavelength': sc.array(dims=['wavelength'], values=[1.0, 3, 4], unit='Å') - }, - ) - with pytest.raises(ValueError, match="smaller than the range of the detector"): - normalize_by_monitor_integrated( + + def test_extra_bins_in_monitor(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[4.0, 5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 4, 5], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + + sc.testing.assert_identical(normalized, expected) + + def test_extra_bins_in_monitor_hist(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[4.0, 5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( detector, monitor=monitor, uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) + expected = sc.DataArray( + sc.array(dims=['w'], values=[4.0, 5], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + + sc.testing.assert_identical(normalized, expected) + + def test_extra_bins_in_detector(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[-10, 0, 10, 30, 40], unit='counts'), + coords={'w': sc.arange('w', -1.0, 4.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[-1.0, 0, 2, 3, 4], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0, 2, 3], unit='Å')}, + ) + with pytest.raises(ValueError, match="smaller than the range of the detector"): + normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) -def test_normalize_by_monitor_histogram_monitor_mask_aligned_bins() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), - coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0, 7.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')}, - masks={'m': sc.array(dims=['w'], values=[False, True, False])}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = ( - sc.DataArray( - sc.array(dims=['w'], values=[0.0, 9.6, 0, 216 / 7], unit='counts'), + def test_finer_bins_in_detector(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 4, 5], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')) + + sc.testing.assert_identical(normalized, expected) + + def test_finer_bins_in_detector_hist(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 4, 5], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, + ) + + sc.testing.assert_identical(normalized, expected) + + def test_finer_bins_in_monitor(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 8.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[0.0, 5 / 4, 5], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + + sc.testing.assert_allclose(normalized, expected) + + def test_finer_bins_in_monitor_hist(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[10, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 8.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 1, 2, 3], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = sc.DataArray( + sc.array(dims=['w'], values=[20 / 13, 5], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + ) + + sc.testing.assert_allclose(normalized, expected) + + def test_monitor_mask_aligned_bins(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')}, + masks={'m': sc.array(dims=['w'], values=[False, True, False])}, ) - .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) - .assign_masks(_monitor_mask=sc.array(dims=['w'], values=[False, True, False])) - ) - - sc.testing.assert_allclose(normalized, expected) - - -def test_normalize_by_monitor_histogram_monitor_mask_multiple() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), - coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0, 7.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')}, - masks={ - 'm1': sc.array(dims=['w'], values=[False, True, False]), - 'm2': sc.array(dims=['w'], values=[False, True, True]), - }, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = ( - sc.DataArray( - sc.array(dims=['w'], values=[0.0, 10, 0, 0], unit='counts'), + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = ( + sc.DataArray( + sc.array(dims=['w'], values=[0.0, 4, 0, 90 / 7], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + .assign_masks( + _monitor_mask=sc.array(dims=['w'], values=[False, True, False]) + ) + ) + + sc.testing.assert_allclose(normalized, expected) + + def test_monitor_mask_multiple(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')}, + masks={ + 'm1': sc.array(dims=['w'], values=[False, True, False]), + 'm2': sc.array(dims=['w'], values=[False, True, True]), + }, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = ( + sc.DataArray( + sc.array(dims=['w'], values=[0.0, 4, 0, 0], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + .assign_masks( + _monitor_mask=sc.array(dims=['w'], values=[False, True, True]) + ) ) - .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) - .assign_masks(_monitor_mask=sc.array(dims=['w'], values=[False, True, True])) - ) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_monitor_mask_unaligned_bins() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), - coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0, 7.0, 8.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3.5, 4, 7], unit='Å')}, - masks={'m': sc.array(dims=['w'], values=[False, True, False, False])}, - ) - - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = ( - sc.DataArray( - sc.array(dims=['w'], values=[0.0, 0, 0, 30], unit='counts'), + + sc.testing.assert_identical(normalized, expected) + + def test_monitor_and_detector_mask_aligned_bins(self) -> None: + detector = ( + sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + .assign_masks(d=sc.array(dims=['w'], values=[True, False, False])) + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')}, + masks={'m': sc.array(dims=['w'], values=[False, True, False])}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = ( + sc.DataArray( + sc.array(dims=['w'], values=[0.0, 4, 0, 90 / 7], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + .assign_masks( + d=sc.array(dims=['w'], values=[True, False, False]), + _monitor_mask=sc.array(dims=['w'], values=[False, True, False]), + ) + ) + + sc.testing.assert_allclose(normalized, expected) + + def test_monitor_mask_unaligned_bins(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[-10, 10, 20, 30], unit='counts'), coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0, 7.0, 8.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3.5, 4, 7], unit='Å')}, + masks={'m': sc.array(dims=['w'], values=[False, True, False, False])}, + ) + + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = ( + sc.DataArray( + sc.array(dims=['w'], values=[-4, 0, 0, 45 / 4], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + .assign_masks( + _monitor_mask=sc.array(dims=['w'], values=[True, True, False]) + ) + ) + + sc.testing.assert_identical(normalized, expected) + + def test_monitor_mask_at_edge(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, + masks={'m': sc.array(dims=['w'], values=[False, True])}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = ( + sc.DataArray( + sc.array(dims=['w'], values=[0, 4, 0], unit='counts'), + coords={'w': sc.arange('w', 3.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + .assign_masks(_monitor_mask=sc.array(dims=['w'], values=[False, True])) + ) + + sc.testing.assert_identical(normalized, expected) + + @pytest.mark.parametrize("nonfinite_value", [np.nan, np.inf]) + def test_nonfinite_in_monitor_gets_masked( + self, + nonfinite_value: float, + ) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[nonfinite_value, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')}, + ) + normalized = normalize_by_monitor_histogram( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + expected = ( + sc.DataArray( + sc.array( + dims=['w'], + values=[1 / nonfinite_value, 1 / nonfinite_value, 10 / 3, 90 / 7], + unit='counts', + ), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ) + .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + .assign_masks( + _monitor_mask=sc.array(dims=['w'], values=[True, False, False]) + ) + ) + + sc.testing.assert_allclose(normalized, expected) + + def test_independent_of_monitor_binning_bin(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[3, 10, 20, 30], unit='counts'), + coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + + monitor1 = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[1.0, 2, 4, 8], unit='Å')}, + ) + monitor2 = monitor1.rebin( + w=sc.array(dims=['w'], values=[1.0, 2, 3, 4, 7], unit='Å') + ) + + normalized1 = normalize_by_monitor_histogram( + detector, + monitor=monitor1, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + normalized2 = normalize_by_monitor_histogram( + detector, + monitor=monitor2, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + sc.testing.assert_identical(normalized1, normalized2) + + def test_independent_of_monitor_binning_hist(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[10, 20, 30], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')}, + ) + + monitor1 = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[1.0, 2, 4, 8], unit='Å')}, + ) + monitor2 = monitor1.rebin( + w=sc.array(dims=['w'], values=[1.0, 2, 3, 4, 7], unit='Å') + ) + + normalized1 = normalize_by_monitor_histogram( + detector, + monitor=monitor1, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + normalized2 = normalize_by_monitor_histogram( + detector, + monitor=monitor2, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) - .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) - .assign_masks(_monitor_mask=sc.array(dims=['w'], values=[True, True, False])) - ) - - sc.testing.assert_identical(normalized, expected) - - -def test_normalize_by_monitor_histogram_monitor_mask_at_edge() -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 30], unit='counts'), - coords={'w': sc.arange('w', 3.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[5.0, 6.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')}, - masks={'m': sc.array(dims=['w'], values=[False, True])}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = ( - sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 0], unit='counts'), + + sc.testing.assert_identical(normalized1, normalized2) + + +class TestNormalizeByMonitorIntegrated: + def test_expected_results_bin(self) -> None: + detector = sc.DataArray( + sc.arange('w', 1, 4, unit='counts'), coords={'w': sc.arange('w', 3.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[4.0, 5.0, 6.0, 10], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 0.5, 2, 3, 4], unit='Å')}, ) - .bin(w=sc.array(dims=['w'], values=[0.0, 2, 3], unit='Å')) - .assign_masks(_monitor_mask=sc.array(dims=['w'], values=[False, True])) - ) - - sc.testing.assert_identical(normalized, expected) - - -@pytest.mark.parametrize("nonfinite_value", [np.nan, np.inf]) -def test_normalize_by_monitor_histogram_nonfinite_in_monitor_is_masked( - nonfinite_value: float, -) -> None: - detector = sc.DataArray( - sc.array(dims=['w'], values=[0, 10, 20, 30], unit='counts'), - coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, - ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) - monitor = sc.DataArray( - sc.array(dims=['w'], values=[nonfinite_value, 6.0, 7.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')}, - ) - normalized = normalize_by_monitor_histogram( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - - expected = ( - sc.DataArray( - sc.array(dims=['w'], values=[0.0, 0.0, 65 / 6, 585 / 14], unit='counts'), + normalized = normalize_by_monitor_integrated( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + expected = detector / sc.scalar( + 4 * 0.5 + 5 * 1.5 + 6 * 1 + 10 * 1, unit='counts * Å' + ) + sc.testing.assert_identical(normalized, expected) + + def test_expected_results_hist(self) -> None: + detector = sc.DataArray( + sc.arange('w', 1, 4, unit='counts'), + coords={'w': sc.arange('w', 4.0, unit='Å')}, + ) + monitor = sc.DataArray( + sc.array(dims=['w'], values=[4.0, 5.0, 6.0, 10], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[0.0, 0.5, 2, 3, 4], unit='Å')}, + ) + normalized = normalize_by_monitor_integrated( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + expected = detector / sc.scalar( + 4 * 0.5 + 5 * 1.5 + 6 * 1 + 10 * 1, unit='counts * Å' + ) + sc.testing.assert_identical(normalized, expected) + + @pytest.mark.parametrize('event_coord', [True, False]) + def test_uses_monitor_values_at_boundary( + self, + event_coord: bool, + ) -> None: + detector = sc.DataArray( + sc.arange('wavelength', 4, unit='counts'), + coords={'wavelength': sc.arange('wavelength', 4.0, unit='Å')}, + ) + if event_coord: + # Make sure event at 3 is included + detector = detector.bin( + wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3.1], unit='Å') + ) + del detector.coords['wavelength'] + else: + detector = detector.bin( + wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3], unit='Å') + ) + del detector.bins.coords['wavelength'] + monitor = sc.DataArray( + sc.array(dims=['wavelength'], values=[4.0, 10.0], unit='counts'), + coords={ + 'wavelength': sc.array( + dims=['wavelength'], values=[0.0, 2, 4], unit='Å' + ) + }, + ) + normalized = normalize_by_monitor_integrated( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + expected = detector / sc.scalar(4.0 * 2 + 10.0 * 2, unit='counts') + sc.testing.assert_identical(normalized, expected) + + def test_raises_if_monitor_range_too_narrow( + self, + ) -> None: + detector = sc.DataArray( + sc.arange('wavelength', 3, unit='counts'), + coords={'wavelength': sc.arange('wavelength', 3.0, unit='Å')}, + ).bin(wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3], unit='Å')) + monitor = sc.DataArray( + sc.array(dims=['wavelength'], values=[4.0, 10.0], unit='counts'), + coords={ + 'wavelength': sc.array( + dims=['wavelength'], values=[1.0, 3, 4], unit='Å' + ) + }, + ) + with pytest.raises(ValueError, match="smaller than the range of the detector"): + normalize_by_monitor_integrated( + detector, + monitor=monitor, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + + def test_independent_of_monitor_binning_bin(self) -> None: + detector = sc.DataArray( + sc.array(dims=['w'], values=[3, 10, 20, 30], unit='counts'), coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, + ).bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) + + monitor1 = sc.DataArray( + sc.array(dims=['w'], values=[5.0, 6.0, 7.0], unit='counts'), + coords={'w': sc.array(dims=['w'], values=[1.0, 2, 4, 8], unit='Å')}, + ) + monitor2 = monitor1.rebin( + w=sc.array(dims=['w'], values=[1.0, 2, 3, 4, 7], unit='Å') + ) + + normalized1 = normalize_by_monitor_integrated( + detector, + monitor=monitor1, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, + ) + normalized2 = normalize_by_monitor_integrated( + detector, + monitor=monitor2, + uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) - .bin(w=sc.array(dims=['w'], values=[1.0, 3, 4, 7], unit='Å')) - .assign_masks(_monitor_mask=sc.array(dims=['w'], values=[True, False, False])) - ) - sc.testing.assert_allclose(normalized, expected) + sc.testing.assert_identical(normalized1, normalized2) From 99fa7f9cde94a315c396d85a12638a29d868e4a7 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Wynen Date: Mon, 10 Nov 2025 14:52:09 +0100 Subject: [PATCH 12/12] Use simple sum, not integral --- src/ess/reduce/normalization.py | 13 +++++---- tests/normalization_test.py | 50 ++++----------------------------- 2 files changed, 12 insertions(+), 51 deletions(-) diff --git a/src/ess/reduce/normalization.py b/src/ess/reduce/normalization.py index 2abf47b4..46e5e502 100644 --- a/src/ess/reduce/normalization.py +++ b/src/ess/reduce/normalization.py @@ -96,11 +96,13 @@ def normalize_by_monitor_integrated( .. math:: - d_i^\\text{Norm} &= d_i / M \\\\ - M &= \\sum_j\\, m_j (x_{j+1} - x_j)\\,, + d_i^\\text{Norm} = \\frac{d_i}{\\sum_j\\, m_j} - where :math:`m_j` is the monitor counts in bin :math:`j` and - :math:`x_j` is the lower bin edge of that bin. + where :math:`m_j` is the monitor counts in bin :math:`j`. + Note that this is not a true integral but only a sum over monitor events. + + The result depends on the range of the monitor but not its + binning within that range. Parameters ---------- @@ -126,8 +128,7 @@ def normalize_by_monitor_integrated( """ _check_monitor_range_contains_detector(monitor=monitor, detector=detector) detector = _mask_detector_for_norm(detector=detector, monitor=monitor) - coord = monitor.coords[monitor.dim] - norm = (monitor * (coord[1:] - coord[:-1])).data.sum() + norm = monitor.data.sum() norm = broadcast_uncertainties( norm, prototype=detector, mode=uncertainty_broadcast_mode ) diff --git a/tests/normalization_test.py b/tests/normalization_test.py index 37d7e588..a4d3d2e4 100644 --- a/tests/normalization_test.py +++ b/tests/normalization_test.py @@ -569,9 +569,7 @@ def test_expected_results_bin(self) -> None: monitor=monitor, uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) - expected = detector / sc.scalar( - 4 * 0.5 + 5 * 1.5 + 6 * 1 + 10 * 1, unit='counts * Å' - ) + expected = detector / monitor.sum() sc.testing.assert_identical(normalized, expected) def test_expected_results_hist(self) -> None: @@ -588,45 +586,7 @@ def test_expected_results_hist(self) -> None: monitor=monitor, uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) - expected = detector / sc.scalar( - 4 * 0.5 + 5 * 1.5 + 6 * 1 + 10 * 1, unit='counts * Å' - ) - sc.testing.assert_identical(normalized, expected) - - @pytest.mark.parametrize('event_coord', [True, False]) - def test_uses_monitor_values_at_boundary( - self, - event_coord: bool, - ) -> None: - detector = sc.DataArray( - sc.arange('wavelength', 4, unit='counts'), - coords={'wavelength': sc.arange('wavelength', 4.0, unit='Å')}, - ) - if event_coord: - # Make sure event at 3 is included - detector = detector.bin( - wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3.1], unit='Å') - ) - del detector.coords['wavelength'] - else: - detector = detector.bin( - wavelength=sc.array(dims=['wavelength'], values=[0.0, 2, 3], unit='Å') - ) - del detector.bins.coords['wavelength'] - monitor = sc.DataArray( - sc.array(dims=['wavelength'], values=[4.0, 10.0], unit='counts'), - coords={ - 'wavelength': sc.array( - dims=['wavelength'], values=[0.0, 2, 4], unit='Å' - ) - }, - ) - normalized = normalize_by_monitor_integrated( - detector, - monitor=monitor, - uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, - ) - expected = detector / sc.scalar(4.0 * 2 + 10.0 * 2, unit='counts') + expected = detector / monitor.sum() sc.testing.assert_identical(normalized, expected) def test_raises_if_monitor_range_too_narrow( @@ -651,7 +611,7 @@ def test_raises_if_monitor_range_too_narrow( uncertainty_broadcast_mode=UncertaintyBroadcastMode.fail, ) - def test_independent_of_monitor_binning_bin(self) -> None: + def test_independent_of_monitor_binning(self) -> None: detector = sc.DataArray( sc.array(dims=['w'], values=[3, 10, 20, 30], unit='counts'), coords={'w': sc.arange('w', 1.0, 5.0, unit='Å')}, @@ -659,10 +619,10 @@ def test_independent_of_monitor_binning_bin(self) -> None: monitor1 = sc.DataArray( sc.array(dims=['w'], values=[5.0, 6.0, 7.0], unit='counts'), - coords={'w': sc.array(dims=['w'], values=[1.0, 2, 4, 8], unit='Å')}, + coords={'w': sc.array(dims=['w'], values=[1.0, 2, 4, 7], unit='Å')}, ) monitor2 = monitor1.rebin( - w=sc.array(dims=['w'], values=[1.0, 2, 3, 4, 7], unit='Å') + w=sc.array(dims=['w'], values=[1.0, 2, 3, 5, 7], unit='Å') ) normalized1 = normalize_by_monitor_integrated(