essreduce 25.11.2__tar.gz → 25.11.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. {essreduce-25.11.2 → essreduce-25.11.3}/PKG-INFO +1 -1
  2. {essreduce-25.11.2 → essreduce-25.11.3}/docs/api-reference/index.md +1 -0
  3. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/__init__.py +2 -2
  4. essreduce-25.11.3/src/ess/reduce/normalization.py +215 -0
  5. {essreduce-25.11.2 → essreduce-25.11.3}/src/essreduce.egg-info/PKG-INFO +1 -1
  6. {essreduce-25.11.2 → essreduce-25.11.3}/src/essreduce.egg-info/SOURCES.txt +2 -0
  7. essreduce-25.11.3/tests/normalization_test.py +741 -0
  8. {essreduce-25.11.2 → essreduce-25.11.3}/tests/time_of_flight/wfm_test.py +2 -2
  9. {essreduce-25.11.2 → essreduce-25.11.3}/.copier-answers.ess.yml +0 -0
  10. {essreduce-25.11.2 → essreduce-25.11.3}/.copier-answers.yml +0 -0
  11. {essreduce-25.11.2 → essreduce-25.11.3}/.github/ISSUE_TEMPLATE/high-level-requirement.yml +0 -0
  12. {essreduce-25.11.2 → essreduce-25.11.3}/.github/dependabot.yml +0 -0
  13. {essreduce-25.11.2 → essreduce-25.11.3}/.github/workflows/ci.yml +0 -0
  14. {essreduce-25.11.2 → essreduce-25.11.3}/.github/workflows/docs.yml +0 -0
  15. {essreduce-25.11.2 → essreduce-25.11.3}/.github/workflows/nightly_at_main.yml +0 -0
  16. {essreduce-25.11.2 → essreduce-25.11.3}/.github/workflows/nightly_at_main_lower_bound.yml +0 -0
  17. {essreduce-25.11.2 → essreduce-25.11.3}/.github/workflows/nightly_at_release.yml +0 -0
  18. {essreduce-25.11.2 → essreduce-25.11.3}/.github/workflows/python-version-ci +0 -0
  19. {essreduce-25.11.2 → essreduce-25.11.3}/.github/workflows/release.yml +0 -0
  20. {essreduce-25.11.2 → essreduce-25.11.3}/.github/workflows/test.yml +0 -0
  21. {essreduce-25.11.2 → essreduce-25.11.3}/.github/workflows/unpinned.yml +0 -0
  22. {essreduce-25.11.2 → essreduce-25.11.3}/.github/workflows/weekly_windows_macos.yml +0 -0
  23. {essreduce-25.11.2 → essreduce-25.11.3}/.gitignore +0 -0
  24. {essreduce-25.11.2 → essreduce-25.11.3}/.pre-commit-config.yaml +0 -0
  25. {essreduce-25.11.2 → essreduce-25.11.3}/.python-version +0 -0
  26. {essreduce-25.11.2 → essreduce-25.11.3}/CODE_OF_CONDUCT.md +0 -0
  27. {essreduce-25.11.2 → essreduce-25.11.3}/CONTRIBUTING.md +0 -0
  28. {essreduce-25.11.2 → essreduce-25.11.3}/LICENSE +0 -0
  29. {essreduce-25.11.2 → essreduce-25.11.3}/MANIFEST.in +0 -0
  30. {essreduce-25.11.2 → essreduce-25.11.3}/README.md +0 -0
  31. {essreduce-25.11.2 → essreduce-25.11.3}/docs/_static/anaconda-icon.js +0 -0
  32. {essreduce-25.11.2 → essreduce-25.11.3}/docs/_static/favicon.svg +0 -0
  33. {essreduce-25.11.2 → essreduce-25.11.3}/docs/_static/logo-dark.svg +0 -0
  34. {essreduce-25.11.2 → essreduce-25.11.3}/docs/_static/logo.svg +0 -0
  35. {essreduce-25.11.2 → essreduce-25.11.3}/docs/_templates/class-template.rst +0 -0
  36. {essreduce-25.11.2 → essreduce-25.11.3}/docs/_templates/doc_version.html +0 -0
  37. {essreduce-25.11.2 → essreduce-25.11.3}/docs/_templates/module-template.rst +0 -0
  38. {essreduce-25.11.2 → essreduce-25.11.3}/docs/about/index.md +0 -0
  39. {essreduce-25.11.2 → essreduce-25.11.3}/docs/conf.py +0 -0
  40. {essreduce-25.11.2 → essreduce-25.11.3}/docs/developer/coding-conventions.md +0 -0
  41. {essreduce-25.11.2 → essreduce-25.11.3}/docs/developer/dependency-management.md +0 -0
  42. {essreduce-25.11.2 → essreduce-25.11.3}/docs/developer/getting-started.md +0 -0
  43. {essreduce-25.11.2 → essreduce-25.11.3}/docs/developer/gui.ipynb +0 -0
  44. {essreduce-25.11.2 → essreduce-25.11.3}/docs/developer/index.md +0 -0
  45. {essreduce-25.11.2 → essreduce-25.11.3}/docs/index.md +0 -0
  46. {essreduce-25.11.2 → essreduce-25.11.3}/docs/user-guide/index.md +0 -0
  47. {essreduce-25.11.2 → essreduce-25.11.3}/docs/user-guide/installation.md +0 -0
  48. {essreduce-25.11.2 → essreduce-25.11.3}/docs/user-guide/reduction-workflow-guidelines.md +0 -0
  49. {essreduce-25.11.2 → essreduce-25.11.3}/docs/user-guide/tof/dream.ipynb +0 -0
  50. {essreduce-25.11.2 → essreduce-25.11.3}/docs/user-guide/tof/frame-unwrapping.ipynb +0 -0
  51. {essreduce-25.11.2 → essreduce-25.11.3}/docs/user-guide/tof/index.md +0 -0
  52. {essreduce-25.11.2 → essreduce-25.11.3}/docs/user-guide/tof/wfm.ipynb +0 -0
  53. {essreduce-25.11.2 → essreduce-25.11.3}/docs/user-guide/widget.md +0 -0
  54. {essreduce-25.11.2 → essreduce-25.11.3}/pyproject.toml +0 -0
  55. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/base.in +0 -0
  56. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/base.txt +0 -0
  57. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/basetest.in +0 -0
  58. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/basetest.txt +0 -0
  59. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/ci.in +0 -0
  60. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/ci.txt +0 -0
  61. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/dev.in +0 -0
  62. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/dev.txt +0 -0
  63. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/docs.in +0 -0
  64. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/docs.txt +0 -0
  65. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/make_base.py +0 -0
  66. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/mypy.in +0 -0
  67. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/mypy.txt +0 -0
  68. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/nightly.in +0 -0
  69. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/nightly.txt +0 -0
  70. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/static.in +0 -0
  71. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/static.txt +0 -0
  72. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/test.in +0 -0
  73. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/test.txt +0 -0
  74. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/wheels.in +0 -0
  75. {essreduce-25.11.2 → essreduce-25.11.3}/requirements/wheels.txt +0 -0
  76. {essreduce-25.11.2 → essreduce-25.11.3}/resources/logo.svg +0 -0
  77. {essreduce-25.11.2 → essreduce-25.11.3}/setup.cfg +0 -0
  78. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/data/__init__.py +0 -0
  79. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/data/_registry.py +0 -0
  80. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/live/__init__.py +0 -0
  81. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/live/raw.py +0 -0
  82. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/live/roi.py +0 -0
  83. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/live/workflow.py +0 -0
  84. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/logging.py +0 -0
  85. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/nexus/__init__.py +0 -0
  86. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/nexus/_nexus_loader.py +0 -0
  87. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/nexus/json_generator.py +0 -0
  88. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/nexus/json_nexus.py +0 -0
  89. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/nexus/types.py +0 -0
  90. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/nexus/workflow.py +0 -0
  91. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/parameter.py +0 -0
  92. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/py.typed +0 -0
  93. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/scripts/grow_nexus.py +0 -0
  94. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/streaming.py +0 -0
  95. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/__init__.py +0 -0
  96. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/eto_to_tof.py +0 -0
  97. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/fakes.py +0 -0
  98. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/interpolator_numba.py +0 -0
  99. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/interpolator_scipy.py +0 -0
  100. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/lut.py +0 -0
  101. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/resample.py +0 -0
  102. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/types.py +0 -0
  103. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/time_of_flight/workflow.py +0 -0
  104. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/ui.py +0 -0
  105. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/uncertainty.py +0 -0
  106. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/__init__.py +0 -0
  107. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/_base.py +0 -0
  108. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/_binedges_widget.py +0 -0
  109. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/_bounds_widget.py +0 -0
  110. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/_config.py +0 -0
  111. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/_filename_widget.py +0 -0
  112. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/_linspace_widget.py +0 -0
  113. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/_optional_widget.py +0 -0
  114. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/_spinner.py +0 -0
  115. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/_string_widget.py +0 -0
  116. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/_switchable_widget.py +0 -0
  117. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/widgets/_vector_widget.py +0 -0
  118. {essreduce-25.11.2 → essreduce-25.11.3}/src/ess/reduce/workflow.py +0 -0
  119. {essreduce-25.11.2 → essreduce-25.11.3}/src/essreduce.egg-info/dependency_links.txt +0 -0
  120. {essreduce-25.11.2 → essreduce-25.11.3}/src/essreduce.egg-info/entry_points.txt +0 -0
  121. {essreduce-25.11.2 → essreduce-25.11.3}/src/essreduce.egg-info/requires.txt +0 -0
  122. {essreduce-25.11.2 → essreduce-25.11.3}/src/essreduce.egg-info/top_level.txt +0 -0
  123. {essreduce-25.11.2 → essreduce-25.11.3}/tests/accumulators_test.py +0 -0
  124. {essreduce-25.11.2 → essreduce-25.11.3}/tests/conftest.py +0 -0
  125. {essreduce-25.11.2 → essreduce-25.11.3}/tests/live/raw_test.py +0 -0
  126. {essreduce-25.11.2 → essreduce-25.11.3}/tests/live/roi_test.py +0 -0
  127. {essreduce-25.11.2 → essreduce-25.11.3}/tests/nexus/json_generator_test.py +0 -0
  128. {essreduce-25.11.2 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/array_dataset.json +0 -0
  129. {essreduce-25.11.2 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/dataset.json +0 -0
  130. {essreduce-25.11.2 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/detector.json +0 -0
  131. {essreduce-25.11.2 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/entry.json +0 -0
  132. {essreduce-25.11.2 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/event_data.json +0 -0
  133. {essreduce-25.11.2 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/instrument.json +0 -0
  134. {essreduce-25.11.2 → essreduce-25.11.3}/tests/nexus/json_nexus_examples/log.json +0 -0
  135. {essreduce-25.11.2 → essreduce-25.11.3}/tests/nexus/json_nexus_test.py +0 -0
  136. {essreduce-25.11.2 → essreduce-25.11.3}/tests/nexus/nexus_loader_test.py +0 -0
  137. {essreduce-25.11.2 → essreduce-25.11.3}/tests/nexus/workflow_test.py +0 -0
  138. {essreduce-25.11.2 → essreduce-25.11.3}/tests/package_test.py +0 -0
  139. {essreduce-25.11.2 → essreduce-25.11.3}/tests/scripts/test_grow_nexus.py +0 -0
  140. {essreduce-25.11.2 → essreduce-25.11.3}/tests/streaming_test.py +0 -0
  141. {essreduce-25.11.2 → essreduce-25.11.3}/tests/time_of_flight/interpolator_test.py +0 -0
  142. {essreduce-25.11.2 → essreduce-25.11.3}/tests/time_of_flight/lut_test.py +0 -0
  143. {essreduce-25.11.2 → essreduce-25.11.3}/tests/time_of_flight/resample_tests.py +0 -0
  144. {essreduce-25.11.2 → essreduce-25.11.3}/tests/time_of_flight/unwrap_test.py +0 -0
  145. {essreduce-25.11.2 → essreduce-25.11.3}/tests/time_of_flight/workflow_test.py +0 -0
  146. {essreduce-25.11.2 → essreduce-25.11.3}/tests/uncertainty_test.py +0 -0
  147. {essreduce-25.11.2 → essreduce-25.11.3}/tests/widget_test.py +0 -0
  148. {essreduce-25.11.2 → essreduce-25.11.3}/tools/shrink_nexus.py +0 -0
  149. {essreduce-25.11.2 → essreduce-25.11.3}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: essreduce
3
- Version: 25.11.2
3
+ Version: 25.11.3
4
4
  Summary: Common data reduction tools for the ESS facility
5
5
  Author: Scipp contributors
6
6
  License-Expression: BSD-3-Clause
@@ -30,6 +30,7 @@
30
30
  live
31
31
  logging
32
32
  nexus
33
+ normalization
33
34
  streaming
34
35
  time_of_flight
35
36
  ui
@@ -4,7 +4,7 @@
4
4
 
5
5
  import importlib.metadata
6
6
 
7
- from . import nexus, time_of_flight, uncertainty
7
+ from . import nexus, normalization, time_of_flight, uncertainty
8
8
 
9
9
  try:
10
10
  __version__ = importlib.metadata.version("essreduce")
@@ -13,4 +13,4 @@ except importlib.metadata.PackageNotFoundError:
13
13
 
14
14
  del importlib
15
15
 
16
- __all__ = ["nexus", "time_of_flight", "uncertainty"]
16
+ __all__ = ["nexus", "normalization", "time_of_flight", "uncertainty"]
@@ -0,0 +1,215 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+ # Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
3
+ """Normalization routines for neutron data reduction."""
4
+
5
+ import functools
6
+
7
+ import scipp as sc
8
+
9
+ from .uncertainty import UncertaintyBroadcastMode, broadcast_uncertainties
10
+
11
+
12
+ def normalize_by_monitor_histogram(
13
+ detector: sc.DataArray,
14
+ *,
15
+ monitor: sc.DataArray,
16
+ uncertainty_broadcast_mode: UncertaintyBroadcastMode,
17
+ ) -> sc.DataArray:
18
+ """Normalize detector data by a normalized histogrammed monitor.
19
+
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.
22
+
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.
29
+
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.
32
+
33
+ The detector is normalized according to
34
+
35
+ .. math::
36
+
37
+ d_i^\\text{Norm} = \\frac{d_i}{m_i} \\Delta x_i
38
+
39
+ Parameters
40
+ ----------
41
+ detector:
42
+ Input detector data.
43
+ Must have a coordinate named ``monitor.dim``, that is, the single
44
+ dimension name of the **monitor**.
45
+ monitor:
46
+ A histogrammed monitor.
47
+ Must be one-dimensional and have a dimension coordinate, typically "wavelength".
48
+ uncertainty_broadcast_mode:
49
+ Choose how uncertainties of the monitor are broadcast to the sample data.
50
+
51
+ Returns
52
+ -------
53
+ :
54
+ ``detector`` normalized by ``monitor``.
55
+ If the monitor has masks or contains non-finite values, the output has a mask
56
+ called '_monitor_mask' constructed from the monitor masks and non-finite values.
57
+
58
+ See also
59
+ --------
60
+ normalize_by_monitor_integrated:
61
+ Normalize by an integrated monitor.
62
+ """
63
+ _check_monitor_range_contains_detector(monitor=monitor, detector=detector)
64
+
65
+ dim = monitor.dim
66
+
67
+ if detector.bins is None:
68
+ monitor = monitor.rebin({dim: detector.coords[dim]})
69
+ detector = _mask_detector_for_norm(detector=detector, monitor=monitor)
70
+ coord = monitor.coords[dim]
71
+ delta_w = sc.DataArray(coord[1:] - coord[:-1], masks=monitor.masks)
72
+ norm = broadcast_uncertainties(
73
+ monitor / delta_w, prototype=detector, mode=uncertainty_broadcast_mode
74
+ )
75
+
76
+ if detector.bins is None:
77
+ return detector / norm.rebin({dim: detector.coords[dim]})
78
+ return detector.bins / sc.lookup(norm, dim=dim)
79
+
80
+
81
+ def normalize_by_monitor_integrated(
82
+ detector: sc.DataArray,
83
+ *,
84
+ monitor: sc.DataArray,
85
+ uncertainty_broadcast_mode: UncertaintyBroadcastMode,
86
+ ) -> sc.DataArray:
87
+ """Normalize detector data by an integrated monitor.
88
+
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`.
93
+
94
+ Let :math:`d_i` be a detector event or the counts in a detector bin.
95
+ The normalized detector is
96
+
97
+ .. math::
98
+
99
+ d_i^\\text{Norm} = \\frac{d_i}{\\sum_j\\, m_j}
100
+
101
+ where :math:`m_j` is the monitor counts in bin :math:`j`.
102
+ Note that this is not a true integral but only a sum over monitor events.
103
+
104
+ The result depends on the range of the monitor but not its
105
+ binning within that range.
106
+
107
+ Parameters
108
+ ----------
109
+ detector:
110
+ Input detector data.
111
+ monitor:
112
+ A histogrammed monitor.
113
+ Must be one-dimensional and have a dimension coordinate, typically "wavelength".
114
+ uncertainty_broadcast_mode:
115
+ Choose how uncertainties of the monitor are broadcast to the sample data.
116
+
117
+ Returns
118
+ -------
119
+ :
120
+ `detector` normalized by a monitor.
121
+ If the monitor has masks or contains non-finite values, the output has a mask
122
+ called '_monitor_mask' constructed from the monitor masks and non-finite values.
123
+
124
+ See also
125
+ --------
126
+ normalize_by_monitor_histogram:
127
+ Normalize by a monitor histogram.
128
+ """
129
+ _check_monitor_range_contains_detector(monitor=monitor, detector=detector)
130
+ detector = _mask_detector_for_norm(detector=detector, monitor=monitor)
131
+ norm = monitor.nansum().data
132
+ norm = broadcast_uncertainties(
133
+ norm, prototype=detector, mode=uncertainty_broadcast_mode
134
+ )
135
+ return detector / norm
136
+
137
+
138
+ def _check_monitor_range_contains_detector(
139
+ *, monitor: sc.DataArray, detector: sc.DataArray
140
+ ) -> None:
141
+ dim = monitor.dim
142
+ if not monitor.coords.is_edges(dim):
143
+ raise sc.CoordError(
144
+ f"Monitor coordinate '{dim}' must be bin-edges to integrate the monitor."
145
+ )
146
+
147
+ # Prefer a bin coord over an event coord because this makes the behavior for binned
148
+ # and histogrammed data consistent. If we used an event coord, we might allow a
149
+ # monitor range that is less than the detector bins which is fine for the events,
150
+ # but would be wrong if the detector was subsequently histogrammed.
151
+ if (det_coord := detector.coords.get(dim)) is not None:
152
+ lo = det_coord[dim, :-1].nanmin()
153
+ hi = det_coord[dim, 1:].nanmax()
154
+ elif (det_coord := detector.bins.coords.get(dim)) is not None:
155
+ lo = det_coord.nanmin()
156
+ hi = det_coord.nanmax()
157
+ else:
158
+ raise sc.CoordError(
159
+ f"Missing '{dim}' coordinate in detector for monitor normalization."
160
+ )
161
+
162
+ if monitor.coords[dim].min() > lo or monitor.coords[dim].max() < hi:
163
+ raise ValueError(
164
+ f"Cannot normalize by monitor: The {dim} range of the monitor "
165
+ f"({monitor.coords[dim].min():c} to {monitor.coords[dim].max():c}) "
166
+ f"is smaller than the range of the detector ({lo:c} to {hi:c})."
167
+ )
168
+
169
+
170
+ def _mask_detector_for_norm(
171
+ *, detector: sc.DataArray, monitor: sc.DataArray
172
+ ) -> sc.DataArray:
173
+ """Mask the detector where the monitor is masked.
174
+
175
+ For performance, this applies the monitor mask to the detector bins.
176
+ This can lead to masking more events than strictly necessary if we
177
+ used an event mask.
178
+ """
179
+ dim = monitor.dim
180
+
181
+ if (monitor_mask := _monitor_mask(monitor)) is None:
182
+ return detector
183
+
184
+ if (detector_coord := detector.coords.get(monitor.dim)) is not None:
185
+ # Apply the mask to the bins or a dense detector.
186
+ # Use rebin to reshape the mask to the detector.
187
+ mask = sc.DataArray(monitor_mask, coords={dim: monitor.coords[dim]}).rebin(
188
+ {dim: detector_coord}
189
+ ).data != sc.scalar(0, unit=None)
190
+ return detector.assign_masks({"_monitor_mask": mask})
191
+
192
+ # else: Apply the mask to the events.
193
+ if dim not in detector.bins.coords:
194
+ raise sc.CoordError(
195
+ f"Detector must have coordinate '{dim}' to mask by monitor."
196
+ )
197
+ event_mask = sc.lookup(
198
+ sc.DataArray(monitor_mask, coords={dim: monitor.coords[dim]})
199
+ )[detector.bins.coords[dim]]
200
+ return detector.bins.assign_masks({"_monitor_mask": event_mask})
201
+
202
+
203
+ def _monitor_mask(monitor: sc.DataArray) -> sc.Variable | None:
204
+ """Mask nonfinite and zero monitor values and combine all masks."""
205
+ masks = list(monitor.masks.values())
206
+
207
+ finite = sc.isfinite(monitor.data)
208
+ nonzero = monitor.data != sc.scalar(0, unit=monitor.unit)
209
+ valid = finite & nonzero
210
+ if not valid.all():
211
+ masks.append(~valid)
212
+
213
+ if not masks:
214
+ return None
215
+ return functools.reduce(sc.logical_or, masks)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: essreduce
3
- Version: 25.11.2
3
+ Version: 25.11.3
4
4
  Summary: Common data reduction tools for the ESS facility
5
5
  Author: Scipp contributors
6
6
  License-Expression: BSD-3-Clause
@@ -70,6 +70,7 @@ requirements/wheels.txt
70
70
  resources/logo.svg
71
71
  src/ess/reduce/__init__.py
72
72
  src/ess/reduce/logging.py
73
+ src/ess/reduce/normalization.py
73
74
  src/ess/reduce/parameter.py
74
75
  src/ess/reduce/py.typed
75
76
  src/ess/reduce/streaming.py
@@ -118,6 +119,7 @@ src/essreduce.egg-info/requires.txt
118
119
  src/essreduce.egg-info/top_level.txt
119
120
  tests/accumulators_test.py
120
121
  tests/conftest.py
122
+ tests/normalization_test.py
121
123
  tests/package_test.py
122
124
  tests/streaming_test.py
123
125
  tests/uncertainty_test.py