modacor 1.0.0__py3-none-any.whl

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 (120) hide show
  1. modacor/__init__.py +30 -0
  2. modacor/dataclasses/__init__.py +0 -0
  3. modacor/dataclasses/basedata.py +973 -0
  4. modacor/dataclasses/databundle.py +23 -0
  5. modacor/dataclasses/helpers.py +45 -0
  6. modacor/dataclasses/messagehandler.py +75 -0
  7. modacor/dataclasses/process_step.py +233 -0
  8. modacor/dataclasses/process_step_describer.py +146 -0
  9. modacor/dataclasses/processing_data.py +59 -0
  10. modacor/dataclasses/trace_event.py +118 -0
  11. modacor/dataclasses/uncertainty_tools.py +132 -0
  12. modacor/dataclasses/validators.py +84 -0
  13. modacor/debug/pipeline_tracer.py +548 -0
  14. modacor/io/__init__.py +33 -0
  15. modacor/io/csv/__init__.py +0 -0
  16. modacor/io/csv/csv_sink.py +114 -0
  17. modacor/io/csv/csv_source.py +210 -0
  18. modacor/io/hdf/__init__.py +27 -0
  19. modacor/io/hdf/hdf_source.py +120 -0
  20. modacor/io/io_sink.py +41 -0
  21. modacor/io/io_sinks.py +61 -0
  22. modacor/io/io_source.py +164 -0
  23. modacor/io/io_sources.py +208 -0
  24. modacor/io/processing_path.py +113 -0
  25. modacor/io/tiled/__init__.py +16 -0
  26. modacor/io/tiled/tiled_source.py +403 -0
  27. modacor/io/yaml/__init__.py +27 -0
  28. modacor/io/yaml/yaml_source.py +116 -0
  29. modacor/modules/__init__.py +53 -0
  30. modacor/modules/base_modules/__init__.py +0 -0
  31. modacor/modules/base_modules/append_processing_data.py +329 -0
  32. modacor/modules/base_modules/append_sink.py +141 -0
  33. modacor/modules/base_modules/append_source.py +181 -0
  34. modacor/modules/base_modules/bitwise_or_masks.py +113 -0
  35. modacor/modules/base_modules/combine_uncertainties.py +120 -0
  36. modacor/modules/base_modules/combine_uncertainties_max.py +105 -0
  37. modacor/modules/base_modules/divide.py +82 -0
  38. modacor/modules/base_modules/find_scale_factor1d.py +373 -0
  39. modacor/modules/base_modules/multiply.py +77 -0
  40. modacor/modules/base_modules/multiply_databundles.py +73 -0
  41. modacor/modules/base_modules/poisson_uncertainties.py +69 -0
  42. modacor/modules/base_modules/reduce_dimensionality.py +252 -0
  43. modacor/modules/base_modules/sink_processing_data.py +80 -0
  44. modacor/modules/base_modules/subtract.py +80 -0
  45. modacor/modules/base_modules/subtract_databundles.py +67 -0
  46. modacor/modules/base_modules/units_label_update.py +66 -0
  47. modacor/modules/instrument_modules/__init__.py +0 -0
  48. modacor/modules/instrument_modules/readme.md +9 -0
  49. modacor/modules/technique_modules/__init__.py +0 -0
  50. modacor/modules/technique_modules/scattering/__init__.py +0 -0
  51. modacor/modules/technique_modules/scattering/geometry_helpers.py +114 -0
  52. modacor/modules/technique_modules/scattering/index_pixels.py +492 -0
  53. modacor/modules/technique_modules/scattering/indexed_averager.py +628 -0
  54. modacor/modules/technique_modules/scattering/pixel_coordinates_3d.py +417 -0
  55. modacor/modules/technique_modules/scattering/solid_angle_correction.py +63 -0
  56. modacor/modules/technique_modules/scattering/xs_geometry.py +571 -0
  57. modacor/modules/technique_modules/scattering/xs_geometry_from_pixel_coordinates.py +293 -0
  58. modacor/runner/__init__.py +0 -0
  59. modacor/runner/pipeline.py +749 -0
  60. modacor/runner/process_step_registry.py +224 -0
  61. modacor/tests/__init__.py +27 -0
  62. modacor/tests/dataclasses/test_basedata.py +519 -0
  63. modacor/tests/dataclasses/test_basedata_operations.py +439 -0
  64. modacor/tests/dataclasses/test_basedata_to_base_units.py +57 -0
  65. modacor/tests/dataclasses/test_process_step_describer.py +73 -0
  66. modacor/tests/dataclasses/test_processstep.py +282 -0
  67. modacor/tests/debug/test_tracing_integration.py +188 -0
  68. modacor/tests/integration/__init__.py +0 -0
  69. modacor/tests/integration/test_pipeline_run.py +238 -0
  70. modacor/tests/io/__init__.py +27 -0
  71. modacor/tests/io/csv/__init__.py +0 -0
  72. modacor/tests/io/csv/test_csv_source.py +156 -0
  73. modacor/tests/io/hdf/__init__.py +27 -0
  74. modacor/tests/io/hdf/test_hdf_source.py +92 -0
  75. modacor/tests/io/test_io_sources.py +119 -0
  76. modacor/tests/io/tiled/__init__.py +12 -0
  77. modacor/tests/io/tiled/test_tiled_source.py +120 -0
  78. modacor/tests/io/yaml/__init__.py +27 -0
  79. modacor/tests/io/yaml/static_data_example.yaml +26 -0
  80. modacor/tests/io/yaml/test_yaml_source.py +47 -0
  81. modacor/tests/modules/__init__.py +27 -0
  82. modacor/tests/modules/base_modules/__init__.py +27 -0
  83. modacor/tests/modules/base_modules/test_append_processing_data.py +219 -0
  84. modacor/tests/modules/base_modules/test_append_sink.py +76 -0
  85. modacor/tests/modules/base_modules/test_append_source.py +180 -0
  86. modacor/tests/modules/base_modules/test_bitwise_or_masks.py +264 -0
  87. modacor/tests/modules/base_modules/test_combine_uncertainties.py +105 -0
  88. modacor/tests/modules/base_modules/test_combine_uncertainties_max.py +109 -0
  89. modacor/tests/modules/base_modules/test_divide.py +140 -0
  90. modacor/tests/modules/base_modules/test_find_scale_factor1d.py +220 -0
  91. modacor/tests/modules/base_modules/test_multiply.py +113 -0
  92. modacor/tests/modules/base_modules/test_multiply_databundles.py +136 -0
  93. modacor/tests/modules/base_modules/test_poisson_uncertainties.py +61 -0
  94. modacor/tests/modules/base_modules/test_reduce_dimensionality.py +358 -0
  95. modacor/tests/modules/base_modules/test_sink_processing_data.py +119 -0
  96. modacor/tests/modules/base_modules/test_subtract.py +111 -0
  97. modacor/tests/modules/base_modules/test_subtract_databundles.py +136 -0
  98. modacor/tests/modules/base_modules/test_units_label_update.py +91 -0
  99. modacor/tests/modules/technique_modules/__init__.py +0 -0
  100. modacor/tests/modules/technique_modules/scattering/__init__.py +0 -0
  101. modacor/tests/modules/technique_modules/scattering/test_geometry_helpers.py +198 -0
  102. modacor/tests/modules/technique_modules/scattering/test_index_pixels.py +426 -0
  103. modacor/tests/modules/technique_modules/scattering/test_indexed_averaging.py +559 -0
  104. modacor/tests/modules/technique_modules/scattering/test_pixel_coordinates_3d.py +282 -0
  105. modacor/tests/modules/technique_modules/scattering/test_xs_geometry_from_pixel_coordinates.py +224 -0
  106. modacor/tests/modules/technique_modules/scattering/test_xsgeometry.py +635 -0
  107. modacor/tests/requirements.txt +12 -0
  108. modacor/tests/runner/test_pipeline.py +438 -0
  109. modacor/tests/runner/test_process_step_registry.py +65 -0
  110. modacor/tests/test_import.py +43 -0
  111. modacor/tests/test_modacor.py +17 -0
  112. modacor/tests/test_units.py +79 -0
  113. modacor/units.py +97 -0
  114. modacor-1.0.0.dist-info/METADATA +482 -0
  115. modacor-1.0.0.dist-info/RECORD +120 -0
  116. modacor-1.0.0.dist-info/WHEEL +5 -0
  117. modacor-1.0.0.dist-info/licenses/AUTHORS.md +11 -0
  118. modacor-1.0.0.dist-info/licenses/LICENSE +11 -0
  119. modacor-1.0.0.dist-info/licenses/LICENSE.txt +11 -0
  120. modacor-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,559 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+ # /usr/bin/env python3
3
+ # -*- coding: utf-8 -*-
4
+
5
+ from __future__ import annotations
6
+
7
+ __coding__ = "utf-8"
8
+ __authors__ = ["Brian R. Pauw"] # add names to the list as appropriate
9
+ __copyright__ = "Copyright 2025, The MoDaCor team"
10
+ __date__ = "30/11/2025"
11
+ __status__ = "Development" # "Development", "Production"
12
+ # end of header and standard imports
13
+ __version__ = "20251130.1"
14
+
15
+ """
16
+ Tests for the IndexedAverager processing step.
17
+
18
+ We test:
19
+ - Basic 1D averaging with a simple, hand-crafted pixel_index map.
20
+ - Correct handling of Mask and pixel_index == -1.
21
+ - Uncertainty propagation from per-pixel uncertainties to bin-mean.
22
+ - SEM ("SEM" key) behaviour for signal.
23
+ - Integration-style test using prepare_execution() + calculate().
24
+ """
25
+
26
+ import numpy as np
27
+ import pytest
28
+ from numpy.testing import assert_allclose
29
+
30
+ from modacor import ureg
31
+ from modacor.dataclasses.basedata import BaseData
32
+ from modacor.dataclasses.databundle import DataBundle
33
+ from modacor.dataclasses.processing_data import ProcessingData
34
+ from modacor.io.io_sources import IoSources
35
+ from modacor.modules.technique_modules.scattering.indexed_averager import IndexedAverager
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Small helpers to build simple test databundles
39
+ # ---------------------------------------------------------------------------
40
+
41
+
42
+ def make_1d_bundle_basic():
43
+ """
44
+ Simple 1D test bundle with 4 pixels and 2 bins.
45
+
46
+ pixel_index:
47
+ - pixels 0, 1 -> bin 0
48
+ - pixels 2, 3 -> bin 1
49
+ """
50
+ signal = np.array([1.0, 2.0, 3.0, 4.0], dtype=float)
51
+ Q = np.array([10.0, 20.0, 30.0, 40.0], dtype=float)
52
+ Psi = np.array([0.0, 0.0, np.pi, np.pi], dtype=float)
53
+
54
+ signal_bd = BaseData(
55
+ signal=signal,
56
+ units=ureg.dimensionless,
57
+ rank_of_data=1,
58
+ )
59
+ Q_bd = BaseData(
60
+ signal=Q,
61
+ units=ureg.dimensionless,
62
+ rank_of_data=1,
63
+ )
64
+ Psi_bd = BaseData(
65
+ signal=Psi,
66
+ units=ureg.radian,
67
+ rank_of_data=1,
68
+ )
69
+ pixel_index = np.array([0, 0, 1, 1], dtype=float)
70
+ pix_bd = BaseData(
71
+ signal=pixel_index,
72
+ units=ureg.dimensionless,
73
+ rank_of_data=1,
74
+ )
75
+
76
+ bundle = DataBundle(
77
+ {
78
+ "signal": signal_bd,
79
+ "Q": Q_bd,
80
+ "Psi": Psi_bd,
81
+ "pixel_index": pix_bd,
82
+ }
83
+ )
84
+ return bundle
85
+
86
+
87
+ def make_1d_bundle_with_mask_and_uncertainties():
88
+ """
89
+ 1D test bundle with:
90
+ - Mask on one pixel.
91
+ - Per-pixel uncertainties on signal and Q.
92
+ - 2 bins as before.
93
+
94
+ pixel_index:
95
+ - pixel 0 -> bin 0
96
+ - pixel 1 -> bin 0 (masked)
97
+ - pixel 2 -> bin 1
98
+ - pixel 3 -> -1 (ignored)
99
+ """
100
+ signal = np.array([1.0, 2.0, 3.0, 4.0], dtype=float)
101
+ Q = np.array([10.0, 20.0, 30.0, 40.0], dtype=float)
102
+ Psi = np.array([0.0, 0.0, np.pi, np.pi], dtype=float)
103
+
104
+ # uncertainties: "sigma" for signal, "dq" for Q
105
+ sigma_signal = np.array([0.1, 0.1, 0.2, 0.2], dtype=float)
106
+ dq = np.array([0.5, 0.5, 1.0, 1.0], dtype=float)
107
+
108
+ signal_bd = BaseData(
109
+ signal=signal,
110
+ units=ureg.dimensionless,
111
+ uncertainties={"sigma": sigma_signal},
112
+ rank_of_data=1,
113
+ )
114
+ Q_bd = BaseData(
115
+ signal=Q,
116
+ units=ureg.dimensionless,
117
+ uncertainties={"dq": dq},
118
+ rank_of_data=1,
119
+ )
120
+ Psi_bd = BaseData(
121
+ signal=Psi,
122
+ units=ureg.radian,
123
+ rank_of_data=1,
124
+ )
125
+
126
+ pixel_index = np.array([0, 0, 1, -1], dtype=float)
127
+ pix_bd = BaseData(
128
+ signal=pixel_index,
129
+ units=ureg.dimensionless,
130
+ rank_of_data=1,
131
+ )
132
+
133
+ # Mask out pixel 1; booleans, True = masked
134
+ mask_arr = np.array([False, True, False, False], dtype=bool)
135
+ mask_bd = BaseData(
136
+ signal=mask_arr,
137
+ units=ureg.dimensionless,
138
+ rank_of_data=1,
139
+ )
140
+
141
+ bundle = DataBundle(
142
+ {
143
+ "signal": signal_bd,
144
+ "Q": Q_bd,
145
+ "Psi": Psi_bd,
146
+ "pixel_index": pix_bd,
147
+ "Mask": mask_bd,
148
+ }
149
+ )
150
+ return bundle
151
+
152
+
153
+ def make_2d_bundle_basic():
154
+ """
155
+ Simple 2D test bundle (2 x 3) with 3 bins.
156
+
157
+ Layout (row-major):
158
+
159
+ signal =
160
+ [[1, 2, 3],
161
+ [4, 5, 6]]
162
+
163
+ Q =
164
+ [[10, 20, 30],
165
+ [40, 50, 60]]
166
+
167
+ Psi = all zeros (for a trivial circular mean)
168
+
169
+ pixel_index =
170
+ [[0, 0, 1],
171
+ [1, 2, 2]]
172
+
173
+ So:
174
+ bin 0: pixels (0,0), (0,1) → signal 1,2; Q 10,20
175
+ bin 1: pixels (0,2), (1,0) → signal 3,4; Q 30,40
176
+ bin 2: pixels (1,1), (1,2) → signal 5,6; Q 50,60
177
+ """
178
+ signal = np.array(
179
+ [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
180
+ dtype=float,
181
+ )
182
+ Q = np.array(
183
+ [[10.0, 20.0, 30.0], [40.0, 50.0, 60.0]],
184
+ dtype=float,
185
+ )
186
+ # Psi all zeros in radians → mean Psi per bin is trivially 0
187
+ Psi = np.zeros_like(signal, dtype=float)
188
+
189
+ # Simple per-pixel uncertainties (same everywhere)
190
+ sigma_signal = np.full_like(signal, 0.1, dtype=float)
191
+ dq = np.full_like(signal, 0.5, dtype=float)
192
+
193
+ signal_bd = BaseData(
194
+ signal=signal,
195
+ units=ureg.dimensionless,
196
+ uncertainties={"sigma": sigma_signal},
197
+ rank_of_data=2,
198
+ )
199
+ Q_bd = BaseData(
200
+ signal=Q,
201
+ units=ureg.dimensionless,
202
+ uncertainties={"dq": dq},
203
+ rank_of_data=2,
204
+ )
205
+ Psi_bd = BaseData(
206
+ signal=Psi,
207
+ units=ureg.radian,
208
+ rank_of_data=2,
209
+ )
210
+
211
+ pixel_index = np.array(
212
+ [[0, 0, 1], [1, 2, 2]],
213
+ dtype=float,
214
+ )
215
+ pix_bd = BaseData(
216
+ signal=pixel_index,
217
+ units=ureg.dimensionless,
218
+ rank_of_data=2,
219
+ )
220
+
221
+ bundle = DataBundle(
222
+ {
223
+ "signal": signal_bd,
224
+ "Q": Q_bd,
225
+ "Psi": Psi_bd,
226
+ "pixel_index": pix_bd,
227
+ }
228
+ )
229
+ return bundle
230
+
231
+
232
+ # ---------------------------------------------------------------------------
233
+ # Tests
234
+ # ---------------------------------------------------------------------------
235
+
236
+
237
+ def test_indexedaverager_1d_basic_unweighted_mean():
238
+ """
239
+ For a simple 1D bundle without mask and with equal weights:
240
+ - Bin 0 averages pixels 0 and 1.
241
+ - Bin 1 averages pixels 2 and 3.
242
+ We check signal, Q, Psi means and axis wiring.
243
+ """
244
+ step = IndexedAverager(io_sources=IoSources())
245
+
246
+ processing_data = ProcessingData()
247
+ processing_data["bundle"] = make_1d_bundle_basic()
248
+
249
+ step.processing_data = processing_data
250
+ step.configuration.update(
251
+ {
252
+ "with_processing_keys": ["bundle"],
253
+ "output_processing_key": None,
254
+ "averaging_direction": "azimuthal",
255
+ "use_signal_weights": True,
256
+ "use_signal_uncertainty_weights": False,
257
+ "uncertainty_weight_key": None,
258
+ }
259
+ )
260
+
261
+ step.prepare_execution()
262
+ out = step.calculate()
263
+
264
+ assert "bundle" in out
265
+ db_out = out["bundle"]
266
+
267
+ assert "signal" in db_out
268
+ assert "Q" in db_out
269
+ assert "Psi" in db_out
270
+
271
+ sig_1d = db_out["signal"]
272
+ Q_1d = db_out["Q"]
273
+ Psi_1d = db_out["Psi"]
274
+
275
+ # 2 bins
276
+ assert sig_1d.signal.shape == (2,)
277
+ assert Q_1d.signal.shape == (2,)
278
+ assert Psi_1d.signal.shape == (2,)
279
+
280
+ # Simple averages:
281
+ # bin 0: (1+2)/2 = 1.5, (10+20)/2 = 15, Psi = 0
282
+ # bin 1: (3+4)/2 = 3.5, (30+40)/2 = 35, Psi = pi
283
+ assert_allclose(sig_1d.signal, np.array([1.5, 3.5]), rtol=1e-12, atol=1e-12)
284
+ assert_allclose(Q_1d.signal, np.array([15.0, 35.0]), rtol=1e-12, atol=1e-12)
285
+ assert_allclose(Psi_1d.signal, np.array([0.0, np.pi]), rtol=1e-12, atol=1e-12)
286
+
287
+ # Axis wiring: for averaging_direction="azimuthal", signal axes should reference Q
288
+ assert len(sig_1d.axes) == 1
289
+ assert sig_1d.axes[0] is Q_1d
290
+
291
+
292
+ def test_indexedaverager_mask_and_negative_index():
293
+ """
294
+ Check that:
295
+ - Masked pixels are excluded.
296
+ - Pixels with pixel_index == -1 are excluded.
297
+ - Means are computed only from remaining pixels.
298
+ """
299
+ step = IndexedAverager(io_sources=IoSources())
300
+
301
+ processing_data = ProcessingData()
302
+ processing_data["bundle"] = make_1d_bundle_with_mask_and_uncertainties()
303
+
304
+ step.processing_data = processing_data
305
+ step.configuration.update(
306
+ {
307
+ "with_processing_keys": ["bundle"],
308
+ "output_processing_key": None,
309
+ "averaging_direction": "azimuthal",
310
+ "use_signal_weights": True,
311
+ "use_signal_uncertainty_weights": False,
312
+ "uncertainty_weight_key": None,
313
+ }
314
+ )
315
+
316
+ step.prepare_execution()
317
+ out = step.calculate()
318
+
319
+ db_out = out["bundle"]
320
+ sig_1d = db_out["signal"]
321
+ Q_1d = db_out["Q"]
322
+ Psi_1d = db_out["Psi"]
323
+
324
+ # Valid pixels:
325
+ # - pixel 0: index 0, not masked -> bin 0
326
+ # - pixel 1: index 0, masked -> ignored
327
+ # - pixel 2: index 1, not masked -> bin 1
328
+ # - pixel 3: index -1 -> ignored
329
+ # So:
330
+ # bin 0: signal=1, Q=10, Psi=0
331
+ # bin 1: signal=3, Q=30, Psi=pi
332
+ assert_allclose(sig_1d.signal, np.array([1.0, 3.0]), rtol=1e-12, atol=1e-12)
333
+ assert_allclose(Q_1d.signal, np.array([10.0, 30.0]), rtol=1e-12, atol=1e-12)
334
+ assert_allclose(Psi_1d.signal, np.array([0.0, np.pi]), rtol=1e-12, atol=1e-12)
335
+
336
+
337
+ def test_indexedaverager_uncertainty_propagation_and_sem():
338
+ """
339
+ For the basic 1D setup without mask, with a simple per-pixel uncertainty:
340
+ - Check that per-bin propagated uncertainties on signal match the expected
341
+ sqrt(sum(w^2 sigma^2)) / sum_w for equal weights.
342
+ - Check that SEM ("SEM") is present and behaves as expected.
343
+ """
344
+ step = IndexedAverager(io_sources=IoSources())
345
+
346
+ # Start from the basic bundle and add a sigma uncertainty
347
+ bundle = make_1d_bundle_basic()
348
+ sigma_signal = np.array([0.1, 0.1, 0.2, 0.2], dtype=float)
349
+ bundle["signal"].uncertainties = {"sigma": sigma_signal}
350
+
351
+ processing_data = ProcessingData()
352
+ processing_data["bundle"] = bundle
353
+
354
+ step.processing_data = processing_data
355
+ step.configuration.update(
356
+ {
357
+ "with_processing_keys": ["bundle"],
358
+ "output_processing_key": None,
359
+ "averaging_direction": "azimuthal",
360
+ "use_signal_weights": True,
361
+ "use_signal_uncertainty_weights": False,
362
+ "uncertainty_weight_key": None,
363
+ }
364
+ )
365
+
366
+ step.prepare_execution()
367
+ out = step.calculate()
368
+
369
+ sig_1d = out["bundle"]["signal"]
370
+ assert "sigma" in sig_1d.uncertainties
371
+ assert "SEM" in sig_1d.uncertainties
372
+ assert "STD" in sig_1d.uncertainties
373
+
374
+ # Expected propagated sigma on bin means:
375
+ # bin 0: pixels 0,1, sigma=0.1 each
376
+ # sigma_mean0 = sqrt(0.1^2 + 0.1^2) / 2 = 0.1 / sqrt(2)
377
+ # bin 1: pixels 2,3, sigma=0.2 each
378
+ # sigma_mean1 = sqrt(0.2^2 + 0.2^2) / 2 = 0.2 / sqrt(2)
379
+ expected_sigma = np.array([0.1 / np.sqrt(2.0), 0.2 / np.sqrt(2.0)], dtype=float)
380
+
381
+ assert_allclose(sig_1d.uncertainties["sigma"], expected_sigma, rtol=1e-12, atol=1e-12)
382
+
383
+ # SEM from scatter:
384
+ # For basic bundle signal = [1,2,3,4] and means [1.5, 3.5]
385
+ # bin 0: dev = [-0.5, +0.5], sum(dev^2) = 0.5, sum_w=2 -> var_spread=0.25
386
+ # bin 1: same pattern
387
+ # Effective N_eff = (sum_w^2 / sum_w2) = 4/2 = 2
388
+ # sem = sqrt(var_spread / N_eff) = sqrt(0.25/2) = sqrt(0.125)
389
+ expected_sem = np.full(2, np.sqrt(0.125), dtype=float)
390
+ expected_std = np.full(2, 0.5, dtype=float)
391
+
392
+ sem = sig_1d.uncertainties["SEM"]
393
+ std = sig_1d.uncertainties["STD"]
394
+ assert_allclose(sem, expected_sem, rtol=1e-12, atol=1e-12)
395
+ assert_allclose(std, expected_std, rtol=1e-12, atol=1e-12)
396
+
397
+
398
+ def test_indexedaverager_stats_keys_selection():
399
+ """
400
+ If stats_keys is provided, only those BaseData entries receive SEM/STD.
401
+ """
402
+ step = IndexedAverager(io_sources=IoSources())
403
+
404
+ processing_data = ProcessingData()
405
+ processing_data["bundle"] = make_1d_bundle_basic()
406
+
407
+ step.processing_data = processing_data
408
+ step.configuration.update(
409
+ {
410
+ "with_processing_keys": ["bundle"],
411
+ "output_processing_key": None,
412
+ "averaging_direction": "azimuthal",
413
+ "use_signal_weights": True,
414
+ "use_signal_uncertainty_weights": False,
415
+ "uncertainty_weight_key": None,
416
+ "stats_keys": ["Q"],
417
+ }
418
+ )
419
+
420
+ step.prepare_execution()
421
+ out = step.calculate()
422
+
423
+ sig_1d = out["bundle"]["signal"]
424
+ q_1d = out["bundle"]["Q"]
425
+
426
+ assert "SEM" not in sig_1d.uncertainties
427
+ assert "STD" not in sig_1d.uncertainties
428
+ assert "SEM" in q_1d.uncertainties
429
+ assert "STD" in q_1d.uncertainties
430
+
431
+
432
+ def test_indexedaverager_raises_on_missing_uncertainty_weight_key():
433
+ """
434
+ If use_signal_uncertainty_weights=True but uncertainty_weight_key is None,
435
+ a ValueError should be raised.
436
+ """
437
+ step = IndexedAverager(io_sources=IoSources())
438
+
439
+ processing_data = ProcessingData()
440
+ processing_data["bundle"] = make_1d_bundle_basic()
441
+
442
+ step.processing_data = processing_data
443
+ step.configuration.update(
444
+ {
445
+ "with_processing_keys": ["bundle"],
446
+ "output_processing_key": None,
447
+ "averaging_direction": "azimuthal",
448
+ "use_signal_weights": True,
449
+ "use_signal_uncertainty_weights": True,
450
+ "uncertainty_weight_key": None,
451
+ }
452
+ )
453
+
454
+ step.prepare_execution()
455
+ with pytest.raises(ValueError):
456
+ _ = step.calculate()
457
+
458
+
459
+ def test_indexedaverager_prepare_and_calculate_integration_radial_axis():
460
+ """
461
+ Integration-style test:
462
+ - Build a small 1D bundle.
463
+ - Run prepare_execution() and calculate().
464
+ - Check that for averaging_direction='radial', signal.axes[0] references Psi.
465
+ """
466
+ step = IndexedAverager(io_sources=IoSources())
467
+
468
+ processing_data = ProcessingData()
469
+ processing_data["bundle"] = make_1d_bundle_basic()
470
+
471
+ step.processing_data = processing_data
472
+ step.configuration.update(
473
+ {
474
+ "with_processing_keys": ["bundle"],
475
+ "output_processing_key": None,
476
+ "averaging_direction": "radial",
477
+ "use_signal_weights": True,
478
+ "use_signal_uncertainty_weights": False,
479
+ "uncertainty_weight_key": None,
480
+ }
481
+ )
482
+
483
+ step.prepare_execution()
484
+ out = step.calculate()
485
+
486
+ db_out = out["bundle"]
487
+ sig_1d = db_out["signal"]
488
+ Psi_1d = db_out["Psi"]
489
+
490
+ assert len(sig_1d.axes) == 1
491
+ # For radial averaging, we expect signal.axes[0] to be Psi
492
+ assert sig_1d.axes[0] is Psi_1d
493
+
494
+
495
+ def test_indexedaverager_2d_basic_unweighted_mean():
496
+ """
497
+ 2D main-use-case test:
498
+
499
+ - 2x3 signal, Q, Psi, pixel_index (3 bins).
500
+ - No mask, equal weights.
501
+ - Check binned 1D means for signal, Q, Psi.
502
+ - Check propagated signal uncertainty shape and values.
503
+ """
504
+ step = IndexedAverager(io_sources=IoSources())
505
+
506
+ processing_data = ProcessingData()
507
+ processing_data["img"] = make_2d_bundle_basic()
508
+
509
+ step.processing_data = processing_data
510
+ step.configuration.update(
511
+ {
512
+ "with_processing_keys": ["img"],
513
+ "output_processing_key": None,
514
+ "averaging_direction": "azimuthal",
515
+ "use_signal_weights": True,
516
+ "use_signal_uncertainty_weights": False,
517
+ "uncertainty_weight_key": None,
518
+ }
519
+ )
520
+
521
+ step.prepare_execution()
522
+ out = step.calculate()
523
+
524
+ assert "img" in out
525
+ db_out = out["img"]
526
+
527
+ sig_1d = db_out["signal"]
528
+ Q_1d = db_out["Q"]
529
+ Psi_1d = db_out["Psi"]
530
+
531
+ # We expect 3 bins
532
+ assert sig_1d.signal.shape == (3,)
533
+ assert Q_1d.signal.shape == (3,)
534
+ assert Psi_1d.signal.shape == (3,)
535
+
536
+ # Means (from the docstring of make_2d_bundle_basic):
537
+ # bin 0: (1, 2) -> 1.5; (10, 20) -> 15
538
+ # bin 1: (3, 4) -> 3.5; (30, 40) -> 35
539
+ # bin 2: (5, 6) -> 5.5; (50, 60) -> 55
540
+ expected_signal = np.array([1.5, 3.5, 5.5], dtype=float)
541
+ expected_Q = np.array([15.0, 35.0, 55.0], dtype=float)
542
+ expected_Psi = np.zeros(3, dtype=float) # all Psi were zero
543
+
544
+ assert_allclose(sig_1d.signal, expected_signal, rtol=1e-12, atol=1e-12)
545
+ assert_allclose(Q_1d.signal, expected_Q, rtol=1e-12, atol=1e-12)
546
+ assert_allclose(Psi_1d.signal, expected_Psi, rtol=1e-12, atol=1e-12)
547
+
548
+ # For averaging_direction="azimuthal", axis should reference Q
549
+ assert len(sig_1d.axes) == 1
550
+ assert sig_1d.axes[0] is Q_1d
551
+
552
+ # Uncertainty propagation sanity check:
553
+ # signal sigma per pixel = 0.1 everywhere, equal weights.
554
+ # Each bin has 2 pixels → sigma_mean = 0.1 / sqrt(2) for all bins.
555
+ assert "sigma" in sig_1d.uncertainties
556
+ sigma_binned = sig_1d.uncertainties["sigma"]
557
+ expected_sigma = np.full(3, 0.1 / np.sqrt(2.0), dtype=float)
558
+
559
+ assert_allclose(sigma_binned, expected_sigma, rtol=1e-12, atol=1e-12)