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.
- modacor/__init__.py +30 -0
- modacor/dataclasses/__init__.py +0 -0
- modacor/dataclasses/basedata.py +973 -0
- modacor/dataclasses/databundle.py +23 -0
- modacor/dataclasses/helpers.py +45 -0
- modacor/dataclasses/messagehandler.py +75 -0
- modacor/dataclasses/process_step.py +233 -0
- modacor/dataclasses/process_step_describer.py +146 -0
- modacor/dataclasses/processing_data.py +59 -0
- modacor/dataclasses/trace_event.py +118 -0
- modacor/dataclasses/uncertainty_tools.py +132 -0
- modacor/dataclasses/validators.py +84 -0
- modacor/debug/pipeline_tracer.py +548 -0
- modacor/io/__init__.py +33 -0
- modacor/io/csv/__init__.py +0 -0
- modacor/io/csv/csv_sink.py +114 -0
- modacor/io/csv/csv_source.py +210 -0
- modacor/io/hdf/__init__.py +27 -0
- modacor/io/hdf/hdf_source.py +120 -0
- modacor/io/io_sink.py +41 -0
- modacor/io/io_sinks.py +61 -0
- modacor/io/io_source.py +164 -0
- modacor/io/io_sources.py +208 -0
- modacor/io/processing_path.py +113 -0
- modacor/io/tiled/__init__.py +16 -0
- modacor/io/tiled/tiled_source.py +403 -0
- modacor/io/yaml/__init__.py +27 -0
- modacor/io/yaml/yaml_source.py +116 -0
- modacor/modules/__init__.py +53 -0
- modacor/modules/base_modules/__init__.py +0 -0
- modacor/modules/base_modules/append_processing_data.py +329 -0
- modacor/modules/base_modules/append_sink.py +141 -0
- modacor/modules/base_modules/append_source.py +181 -0
- modacor/modules/base_modules/bitwise_or_masks.py +113 -0
- modacor/modules/base_modules/combine_uncertainties.py +120 -0
- modacor/modules/base_modules/combine_uncertainties_max.py +105 -0
- modacor/modules/base_modules/divide.py +82 -0
- modacor/modules/base_modules/find_scale_factor1d.py +373 -0
- modacor/modules/base_modules/multiply.py +77 -0
- modacor/modules/base_modules/multiply_databundles.py +73 -0
- modacor/modules/base_modules/poisson_uncertainties.py +69 -0
- modacor/modules/base_modules/reduce_dimensionality.py +252 -0
- modacor/modules/base_modules/sink_processing_data.py +80 -0
- modacor/modules/base_modules/subtract.py +80 -0
- modacor/modules/base_modules/subtract_databundles.py +67 -0
- modacor/modules/base_modules/units_label_update.py +66 -0
- modacor/modules/instrument_modules/__init__.py +0 -0
- modacor/modules/instrument_modules/readme.md +9 -0
- modacor/modules/technique_modules/__init__.py +0 -0
- modacor/modules/technique_modules/scattering/__init__.py +0 -0
- modacor/modules/technique_modules/scattering/geometry_helpers.py +114 -0
- modacor/modules/technique_modules/scattering/index_pixels.py +492 -0
- modacor/modules/technique_modules/scattering/indexed_averager.py +628 -0
- modacor/modules/technique_modules/scattering/pixel_coordinates_3d.py +417 -0
- modacor/modules/technique_modules/scattering/solid_angle_correction.py +63 -0
- modacor/modules/technique_modules/scattering/xs_geometry.py +571 -0
- modacor/modules/technique_modules/scattering/xs_geometry_from_pixel_coordinates.py +293 -0
- modacor/runner/__init__.py +0 -0
- modacor/runner/pipeline.py +749 -0
- modacor/runner/process_step_registry.py +224 -0
- modacor/tests/__init__.py +27 -0
- modacor/tests/dataclasses/test_basedata.py +519 -0
- modacor/tests/dataclasses/test_basedata_operations.py +439 -0
- modacor/tests/dataclasses/test_basedata_to_base_units.py +57 -0
- modacor/tests/dataclasses/test_process_step_describer.py +73 -0
- modacor/tests/dataclasses/test_processstep.py +282 -0
- modacor/tests/debug/test_tracing_integration.py +188 -0
- modacor/tests/integration/__init__.py +0 -0
- modacor/tests/integration/test_pipeline_run.py +238 -0
- modacor/tests/io/__init__.py +27 -0
- modacor/tests/io/csv/__init__.py +0 -0
- modacor/tests/io/csv/test_csv_source.py +156 -0
- modacor/tests/io/hdf/__init__.py +27 -0
- modacor/tests/io/hdf/test_hdf_source.py +92 -0
- modacor/tests/io/test_io_sources.py +119 -0
- modacor/tests/io/tiled/__init__.py +12 -0
- modacor/tests/io/tiled/test_tiled_source.py +120 -0
- modacor/tests/io/yaml/__init__.py +27 -0
- modacor/tests/io/yaml/static_data_example.yaml +26 -0
- modacor/tests/io/yaml/test_yaml_source.py +47 -0
- modacor/tests/modules/__init__.py +27 -0
- modacor/tests/modules/base_modules/__init__.py +27 -0
- modacor/tests/modules/base_modules/test_append_processing_data.py +219 -0
- modacor/tests/modules/base_modules/test_append_sink.py +76 -0
- modacor/tests/modules/base_modules/test_append_source.py +180 -0
- modacor/tests/modules/base_modules/test_bitwise_or_masks.py +264 -0
- modacor/tests/modules/base_modules/test_combine_uncertainties.py +105 -0
- modacor/tests/modules/base_modules/test_combine_uncertainties_max.py +109 -0
- modacor/tests/modules/base_modules/test_divide.py +140 -0
- modacor/tests/modules/base_modules/test_find_scale_factor1d.py +220 -0
- modacor/tests/modules/base_modules/test_multiply.py +113 -0
- modacor/tests/modules/base_modules/test_multiply_databundles.py +136 -0
- modacor/tests/modules/base_modules/test_poisson_uncertainties.py +61 -0
- modacor/tests/modules/base_modules/test_reduce_dimensionality.py +358 -0
- modacor/tests/modules/base_modules/test_sink_processing_data.py +119 -0
- modacor/tests/modules/base_modules/test_subtract.py +111 -0
- modacor/tests/modules/base_modules/test_subtract_databundles.py +136 -0
- modacor/tests/modules/base_modules/test_units_label_update.py +91 -0
- modacor/tests/modules/technique_modules/__init__.py +0 -0
- modacor/tests/modules/technique_modules/scattering/__init__.py +0 -0
- modacor/tests/modules/technique_modules/scattering/test_geometry_helpers.py +198 -0
- modacor/tests/modules/technique_modules/scattering/test_index_pixels.py +426 -0
- modacor/tests/modules/technique_modules/scattering/test_indexed_averaging.py +559 -0
- modacor/tests/modules/technique_modules/scattering/test_pixel_coordinates_3d.py +282 -0
- modacor/tests/modules/technique_modules/scattering/test_xs_geometry_from_pixel_coordinates.py +224 -0
- modacor/tests/modules/technique_modules/scattering/test_xsgeometry.py +635 -0
- modacor/tests/requirements.txt +12 -0
- modacor/tests/runner/test_pipeline.py +438 -0
- modacor/tests/runner/test_process_step_registry.py +65 -0
- modacor/tests/test_import.py +43 -0
- modacor/tests/test_modacor.py +17 -0
- modacor/tests/test_units.py +79 -0
- modacor/units.py +97 -0
- modacor-1.0.0.dist-info/METADATA +482 -0
- modacor-1.0.0.dist-info/RECORD +120 -0
- modacor-1.0.0.dist-info/WHEEL +5 -0
- modacor-1.0.0.dist-info/licenses/AUTHORS.md +11 -0
- modacor-1.0.0.dist-info/licenses/LICENSE +11 -0
- modacor-1.0.0.dist-info/licenses/LICENSE.txt +11 -0
- 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)
|