|
| 1 | +# SPDX-License-Identifier: BSD-3-Clause |
| 2 | +# Copyright (c) 2025 Scipp contributors (https://github.com/scipp) |
| 3 | +"""Correction algorithms for neutron data reduction.""" |
| 4 | + |
| 5 | +import scipp as sc |
| 6 | + |
| 7 | +from .uncertainty import UncertaintyBroadcastMode, broadcast_uncertainties |
| 8 | + |
| 9 | + |
| 10 | +def normalize_by_monitor_histogram( |
| 11 | + detector: sc.DataArray, |
| 12 | + *, |
| 13 | + monitor: sc.DataArray, |
| 14 | + uncertainty_broadcast_mode: UncertaintyBroadcastMode, |
| 15 | +) -> sc.DataArray: |
| 16 | + """Normalize detector data by a histogrammed monitor. |
| 17 | +
|
| 18 | + First, the monitor is clipped to the range of the detector |
| 19 | +
|
| 20 | + .. math:: |
| 21 | +
|
| 22 | + \\bar{m}_i = m_i I(x_i, x_{i+1}), |
| 23 | +
|
| 24 | + where :math:`m_i` is the monitor intensity in bin :math:`i`, |
| 25 | + :math:`x_i` is the lower bin edge of bin :math:`i`, and |
| 26 | + :math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector. |
| 27 | +
|
| 28 | + The detector bins :math:`d_i` are normalized according to |
| 29 | +
|
| 30 | + .. math:: |
| 31 | +
|
| 32 | + d_i^\\text{Norm} = \\frac{d_i}{\\bar{m}_i} \\Delta x_i |
| 33 | + \\frac{\\sum_j\\,\\bar{m}_j}{\\sum_j\\,\\Delta x_j} |
| 34 | +
|
| 35 | + where :math:`\\Delta x_i` is the width of monitor bin :math:`i` (see below). |
| 36 | + This normalization leads to a result that has the same |
| 37 | + unit as the input detector data. |
| 38 | +
|
| 39 | + Monitor bin :math:`i` is chosen according to: |
| 40 | +
|
| 41 | + - *Histogrammed detector*: The monitor is |
| 42 | + `rebinned <https://scipp.github.io/generated/functions/scipp.rebin.html>`_ |
| 43 | + to the detector binning. This distributes the monitor weights to the |
| 44 | + detector bins. |
| 45 | + - *Binned detector*: The monitor value for bin :math:`i` is determined via |
| 46 | + :func:`scipp.lookup`. This means that for each event, the monitor value |
| 47 | + is obtained from the monitor histogram at that event coordinate value. |
| 48 | +
|
| 49 | + This function is based on the implementation in |
| 50 | + `NormaliseToMonitor <https://docs.mantidproject.org/nightly/algorithms/NormaliseToMonitor-v1.html>`_ |
| 51 | + of Mantid. |
| 52 | +
|
| 53 | + Parameters |
| 54 | + ---------- |
| 55 | + detector: |
| 56 | + Input detector data. |
| 57 | + Must have a coordinate named ``monitor.dim``, that is, the single |
| 58 | + dimension name of the monitor. |
| 59 | + monitor: |
| 60 | + A histogrammed monitor. |
| 61 | + Must be one-dimensional and have a dimension coordinate, typically "wavelength". |
| 62 | + uncertainty_broadcast_mode: |
| 63 | + Choose how uncertainties of the monitor are broadcast to the sample data. |
| 64 | +
|
| 65 | + Returns |
| 66 | + ------- |
| 67 | + : |
| 68 | + ``detector`` normalized by ``monitor``. |
| 69 | +
|
| 70 | + See also |
| 71 | + -------- |
| 72 | + normalize_by_monitor_integrated: |
| 73 | + Normalize by an integrated monitor. |
| 74 | + """ |
| 75 | + dim = monitor.dim |
| 76 | + |
| 77 | + clipped = _clip_monitor_to_detector_range(monitor=monitor, detector=detector) |
| 78 | + coord = clipped.coords[dim] |
| 79 | + delta_w = coord[1:] - coord[:-1] |
| 80 | + total_monitor_weight = broadcast_uncertainties( |
| 81 | + clipped.sum() / delta_w.sum(), |
| 82 | + prototype=clipped, |
| 83 | + mode=uncertainty_broadcast_mode, |
| 84 | + ) |
| 85 | + delta_w *= total_monitor_weight |
| 86 | + norm = broadcast_uncertainties( |
| 87 | + clipped / delta_w, prototype=detector, mode=uncertainty_broadcast_mode |
| 88 | + ) |
| 89 | + |
| 90 | + if detector.bins is None: |
| 91 | + return detector / norm |
| 92 | + return detector.bins / sc.lookup(norm, dim=dim) |
| 93 | + |
| 94 | + |
| 95 | +def normalize_by_monitor_integrated( |
| 96 | + detector: sc.DataArray, |
| 97 | + *, |
| 98 | + monitor: sc.DataArray, |
| 99 | + uncertainty_broadcast_mode: UncertaintyBroadcastMode, |
| 100 | +) -> sc.DataArray: |
| 101 | + """Normalize detector data by an integrated monitor. |
| 102 | +
|
| 103 | + The monitor is integrated according to |
| 104 | +
|
| 105 | + .. math:: |
| 106 | +
|
| 107 | + M = \\sum_{i=0}^{N-1}\\, m_i (x_{i+1} - x_i) I(x_i, x_{i+1}), |
| 108 | +
|
| 109 | + where :math:`m_i` is the monitor intensity in bin :math:`i`, |
| 110 | + :math:`x_i` is the lower bin edge of bin :math:`i`, and |
| 111 | + :math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector. |
| 112 | +
|
| 113 | + Parameters |
| 114 | + ---------- |
| 115 | + detector: |
| 116 | + Input detector data. |
| 117 | + monitor: |
| 118 | + A histogrammed monitor. |
| 119 | + Must be one-dimensional and have a dimension coordinate, typically "wavelength". |
| 120 | + uncertainty_broadcast_mode: |
| 121 | + Choose how uncertainties of the monitor are broadcast to the sample data. |
| 122 | +
|
| 123 | + Returns |
| 124 | + ------- |
| 125 | + : |
| 126 | + `detector` normalized by a monitor. |
| 127 | +
|
| 128 | + See also |
| 129 | + -------- |
| 130 | + normalize_by_monitor_histogram: |
| 131 | + Normalize by a monitor histogram without integration. |
| 132 | + """ |
| 133 | + clipped = _clip_monitor_to_detector_range(monitor=monitor, detector=detector) |
| 134 | + coord = clipped.coords[clipped.dim] |
| 135 | + norm = (clipped * (coord[1:] - coord[:-1])).data.sum() |
| 136 | + norm = broadcast_uncertainties( |
| 137 | + norm, prototype=detector, mode=uncertainty_broadcast_mode |
| 138 | + ) |
| 139 | + return detector / norm |
| 140 | + |
| 141 | + |
| 142 | +def _clip_monitor_to_detector_range( |
| 143 | + *, monitor: sc.DataArray, detector: sc.DataArray |
| 144 | +) -> sc.DataArray: |
| 145 | + dim = monitor.dim |
| 146 | + if not monitor.coords.is_edges(dim): |
| 147 | + raise sc.CoordError( |
| 148 | + f"Monitor coordinate '{dim}' must be bin-edges to integrate the monitor." |
| 149 | + ) |
| 150 | + |
| 151 | + # Prefer a bin coord over an event coord because this makes the behavior for binned |
| 152 | + # and histogrammed data consistent. If we used an event coord, we might allow a |
| 153 | + # monitor range that is less than the detector bins which is fine for the vents, |
| 154 | + # but would be wrong if the detector was subsequently histogrammed. |
| 155 | + if dim in detector.coords: |
| 156 | + det_coord = detector.coords[dim] |
| 157 | + |
| 158 | + # Mask zero-count bins, which are an artifact from the rectangular 2-D binning. |
| 159 | + # The wavelength of those bins must be excluded when determining the range. |
| 160 | + if detector.bins is None: |
| 161 | + mask = detector.data == sc.scalar(0.0, unit=detector.unit) |
| 162 | + else: |
| 163 | + mask = detector.data.bins.size() == sc.scalar(0.0, unit=None) |
| 164 | + lo = ( |
| 165 | + sc.DataArray(det_coord[dim, :-1], masks={'zero_counts': mask}).nanmin().data |
| 166 | + ) |
| 167 | + hi = sc.DataArray(det_coord[dim, 1:], masks={'zero_counts': mask}).nanmax().data |
| 168 | + |
| 169 | + elif dim in detector.bins.coords: |
| 170 | + det_coord = detector.bins.coords[dim] |
| 171 | + # No need to mask here because we have the exact event coordinate values. |
| 172 | + lo = det_coord.nanmin() |
| 173 | + hi = det_coord.nanmax() |
| 174 | + |
| 175 | + else: |
| 176 | + raise sc.CoordError( |
| 177 | + f"Missing '{dim}' coordinate in detector for monitor normalization." |
| 178 | + ) |
| 179 | + |
| 180 | + if monitor.coords[dim].min() > lo or monitor.coords[dim].max() < hi: |
| 181 | + raise ValueError( |
| 182 | + f"Cannot normalize by monitor: The {dim} range of the monitor " |
| 183 | + f"({monitor.coords[dim].min().value} to {monitor.coords[dim].max().value}) " |
| 184 | + f"is smaller than the range of the detector ({lo.value} to {hi.value})." |
| 185 | + ) |
| 186 | + |
| 187 | + if detector.bins is None: |
| 188 | + # If we didn't rebin to the detector coord here, then, for a finer monitor |
| 189 | + # binning than detector, the lookup table would extract one monitor value for |
| 190 | + # each detector bin and ignore other values lying in the same detector bin. |
| 191 | + # But integration would pick up all monitor bins. |
| 192 | + return monitor.rebin({dim: det_coord}) |
| 193 | + return monitor[dim, lo:hi] |
0 commit comments