@@ -15,51 +15,26 @@ def normalize_by_monitor_histogram(
1515 monitor : sc .DataArray ,
1616 uncertainty_broadcast_mode : UncertaintyBroadcastMode ,
1717) -> sc .DataArray :
18- """Normalize detector data by a histogrammed monitor.
18+ """Normalize detector data by a normalized histogrammed monitor.
1919
20- First, the monitor is clipped to the range of the detector
20+ This normalization accounts for both the (wavelength) profile of the incident beam
21+ and the integrated neutron flux, meaning measurement duration and source strength.
2122
22- .. math::
23-
24- \\ bar{m}_i = m_i I(x_i, x_{i+1}),
23+ - For *event* detectors, the monitor values are mapped to the detector
24+ using :func:`scipp.lookup`. That is, for detector event :math:`d_i`,
25+ :math:`m_i` is the monitor bin value at the same coordinate.
26+ - For *histogram* detectors, the monitor is rebinned using to the detector
27+ binning using :func:`scipp.rebin`. Thus, detector value :math:`d_i` and
28+ monitor value :math:`m_i` correspond to the same bin.
2529
26- where :math:`m_i` is the monitor intensity in bin :math:`i`,
27- :math:`x_i` is the lower bin edge of bin :math:`i`, and
28- :math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector.
30+ In both cases, let :math:`x_i` be the lower bound of monitor bin :math:`i`
31+ and let :math:`\\ Delta x_i = x_{i+1} - x_i` be the width of that bin.
2932
30- The detector bins :math:`d_i` are normalized according to
33+ The detector is normalized according to
3134
3235 .. math::
3336
34- d_i^\\ text{Norm} = \\ frac{d_i}{\\ bar{m}_i} \\ Delta x_i
35- \\ frac{\\ sum_j\\ ,\\ bar{m}_j}{\\ sum_j\\ ,\\ Delta x_j}
36-
37- where :math:`\\ Delta x_i = x_{i+1} - x_i` is the width of
38- monitor bin :math:`i` (see below).
39- This normalization leads to a result that has the same
40- unit as the input detector data.
41-
42- Monitor bin :math:`i` is chosen according to:
43-
44- - *Histogrammed detector*: The monitor is
45- `rebinned <https://scipp.github.io/generated/functions/scipp.rebin.html>`_
46- to the detector binning. This distributes the monitor weights to the
47- detector bins.
48- - *Binned detector*: The monitor value for bin :math:`i` is determined via
49- :func:`scipp.lookup`. This means that for each event, the monitor value
50- is obtained from the monitor histogram at that event coordinate value.
51-
52- .. Attention::
53-
54- Masked bins in ``detector`` are ignored when clipping the monitor and therefore
55- impact the normalization factor.
56- The output's masked bins are normalized using the same factor and may
57- be incorrect and even contain NaN.
58- You should only drop masks after normalization if you know what you are doing.
59-
60- This function is based on the implementation of
61- `NormaliseToMonitor <https://docs.mantidproject.org/nightly/algorithms/NormaliseToMonitor-v1.html>`_
62- in Mantid.
37+ d_i^\\ text{Norm} = \\ frac{d_i}{m_i} \\ Delta x_i
6338
6439 Parameters
6540 ----------
@@ -85,24 +60,21 @@ def normalize_by_monitor_histogram(
8560 normalize_by_monitor_integrated:
8661 Normalize by an integrated monitor.
8762 """
63+ _check_monitor_range_contains_detector (monitor = monitor , detector = detector )
64+
8865 dim = monitor .dim
8966
67+ if detector .bins is None :
68+ monitor = monitor .rebin ({dim : detector .coords [dim ]})
9069 detector = _mask_detector_for_norm (detector = detector , monitor = monitor )
91- clipped = _clip_monitor_to_detector_range (monitor = monitor , detector = detector )
92- coord = clipped .coords [dim ]
93- delta_w = sc .DataArray (coord [1 :] - coord [:- 1 ], masks = clipped .masks )
94- total_monitor_weight = broadcast_uncertainties (
95- clipped .sum () / delta_w .sum (),
96- prototype = clipped ,
97- mode = uncertainty_broadcast_mode ,
98- ).data
99- delta_w *= total_monitor_weight
70+ coord = monitor .coords [dim ]
71+ delta_w = sc .DataArray (coord [1 :] - coord [:- 1 ], masks = monitor .masks )
10072 norm = broadcast_uncertainties (
101- clipped / delta_w , prototype = detector , mode = uncertainty_broadcast_mode
73+ monitor / delta_w , prototype = detector , mode = uncertainty_broadcast_mode
10274 )
10375
10476 if detector .bins is None :
105- return detector / norm
77+ return detector / norm . rebin ({ dim : detector . coords [ dim ]})
10678 return detector .bins / sc .lookup (norm , dim = dim )
10779
10880
@@ -114,23 +86,21 @@ def normalize_by_monitor_integrated(
11486) -> sc .DataArray :
11587 """Normalize detector data by an integrated monitor.
11688
117- The monitor is integrated according to
118-
119- .. math::
89+ This normalization accounts only for the integrated neutron flux,
90+ meaning measurement duration and source strength.
91+ It does *not* account for the (wavelength) profile of the incident beam.
92+ For that, see :func:`normalize_by_monitor_histogram`.
12093
121- M = \\ sum_{i=0}^{N-1}\\ , m_i (x_{i+1} - x_i) I(x_i, x_{i+1}),
94+ Let :math:`d_i` be a detector event or the counts in a detector bin.
95+ The normalized detector is
12296
123- where :math:`m_i` is the monitor intensity in bin :math:`i`,
124- :math:`x_i` is the lower bin edge of bin :math:`i`, and
125- :math:`I(x_i, x_{i+1})` selects bins that are within the range of the detector.
97+ .. math::
12698
127- .. Attention::
99+ d_i^\\ text{Norm} &= d_i / M \\ \\
100+ M &= \\ sum_j\\ , m_j (x_{j+1} - x_j)\\ ,,
128101
129- Masked bins in ``detector`` are ignored when clipping the monitor and therefore
130- impact the normalization factor.
131- The output's masked bins are normalized using the same factor and may
132- be incorrect and even contain NaN.
133- You should only drop masks after normalization if you know what you are doing.
102+ where :math:`m_j` is the monitor counts in bin :math:`j` and
103+ :math:`x_j` is the lower bin edge of that bin.
134104
135105 Parameters
136106 ----------
@@ -152,43 +122,37 @@ def normalize_by_monitor_integrated(
152122 See also
153123 --------
154124 normalize_by_monitor_histogram:
155- Normalize by a monitor histogram without integration .
125+ Normalize by a monitor histogram.
156126 """
127+ _check_monitor_range_contains_detector (monitor = monitor , detector = detector )
157128 detector = _mask_detector_for_norm (detector = detector , monitor = monitor )
158- clipped = _clip_monitor_to_detector_range (monitor = monitor , detector = detector )
159- coord = clipped .coords [clipped .dim ]
160- norm = (clipped * (coord [1 :] - coord [:- 1 ])).data .sum ()
129+ coord = monitor .coords [monitor .dim ]
130+ norm = (monitor * (coord [1 :] - coord [:- 1 ])).data .sum ()
161131 norm = broadcast_uncertainties (
162132 norm , prototype = detector , mode = uncertainty_broadcast_mode
163133 )
164134 return detector / norm
165135
166136
167- def _clip_monitor_to_detector_range (
137+ def _check_monitor_range_contains_detector (
168138 * , monitor : sc .DataArray , detector : sc .DataArray
169- ) -> sc . DataArray :
139+ ) -> None :
170140 dim = monitor .dim
171141 if not monitor .coords .is_edges (dim ):
172142 raise sc .CoordError (
173143 f"Monitor coordinate '{ dim } ' must be bin-edges to integrate the monitor."
174144 )
175145
176- # Reduce with `all` instead of `any` to include bins in range calculations
177- # that contain any unmasked data.
178- masks = {
179- name : mask .all (set (mask .dims ) - {dim }) for name , mask in detector .masks .items ()
180- }
181-
182146 # Prefer a bin coord over an event coord because this makes the behavior for binned
183147 # and histogrammed data consistent. If we used an event coord, we might allow a
184148 # monitor range that is less than the detector bins which is fine for the events,
185149 # but would be wrong if the detector was subsequently histogrammed.
186150 if (det_coord := detector .coords .get (dim )) is not None :
187- lo = sc . DataArray ( det_coord [dim , :- 1 ], masks = masks ) .nanmin (). data
188- hi = sc . DataArray ( det_coord [dim , 1 :], masks = masks ) .nanmax (). data
151+ lo = det_coord [dim , :- 1 ].nanmin ()
152+ hi = det_coord [dim , 1 :].nanmax ()
189153 elif (det_coord := detector .bins .coords .get (dim )) is not None :
190- lo = sc . DataArray ( det_coord , masks = masks ) .nanmin (). data
191- hi = sc . DataArray ( det_coord , masks = masks ) .nanmax (). data
154+ lo = det_coord .nanmin ()
155+ hi = det_coord .nanmax ()
192156 else :
193157 raise sc .CoordError (
194158 f"Missing '{ dim } ' coordinate in detector for monitor normalization."
@@ -201,14 +165,6 @@ def _clip_monitor_to_detector_range(
201165 f"is smaller than the range of the detector ({ lo :c} to { hi :c} )."
202166 )
203167
204- if detector .bins is None :
205- # If we didn't rebin to the detector coord here, then, for a finer monitor
206- # binning than detector, the lookup table would extract one monitor value for
207- # each detector bin and ignore other values lying in the same detector bin.
208- # But integration would pick up all monitor bins.
209- return monitor .rebin ({dim : det_coord })
210- return monitor [dim , lo :hi ]
211-
212168
213169def _mask_detector_for_norm (
214170 * , detector : sc .DataArray , monitor : sc .DataArray
0 commit comments