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,417 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# /usr/bin/env python3
|
|
3
|
+
# -*- coding: utf-8 -*-
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from modacor.dataclasses.helpers import basedata_from_sources
|
|
10
|
+
from modacor.dataclasses.process_step_describer import ProcessStepDescriber
|
|
11
|
+
|
|
12
|
+
__coding__ = "utf-8"
|
|
13
|
+
__authors__ = ["Brian R. Pauw"] # add names to the list as appropriate
|
|
14
|
+
__copyright__ = "Copyright 2026, The MoDaCor team"
|
|
15
|
+
__date__ = "04/01/2026"
|
|
16
|
+
__status__ = "Development"
|
|
17
|
+
|
|
18
|
+
__version__ = "20260103.1"
|
|
19
|
+
__all__ = ["CanonicalDetectorFrame", "PixelCoordinates3D"]
|
|
20
|
+
|
|
21
|
+
from typing import Dict, Tuple
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
# import pint
|
|
26
|
+
from attrs import define
|
|
27
|
+
|
|
28
|
+
from modacor import ureg
|
|
29
|
+
from modacor.dataclasses.basedata import BaseData
|
|
30
|
+
from modacor.dataclasses.messagehandler import MessageHandler
|
|
31
|
+
from modacor.dataclasses.process_step import ProcessStep
|
|
32
|
+
from modacor.modules.technique_modules.scattering.geometry_helpers import (
|
|
33
|
+
prepare_static_scalar,
|
|
34
|
+
require_scalar,
|
|
35
|
+
unit_vec3,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = MessageHandler(name=__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@define(frozen=True, slots=True)
|
|
42
|
+
class CanonicalDetectorFrame:
|
|
43
|
+
"""
|
|
44
|
+
Canonical detector frame for pixel coordinate calculation.
|
|
45
|
+
|
|
46
|
+
Coordinates are in lab-frame NeXus axes (x, y, z), z= along beam, y=up, x=left when looking downstream from source.
|
|
47
|
+
Beam center is implicitly encoded as the lab-frame cartesian offset of the detector origin to the beam center in length units.
|
|
48
|
+
- det_coord_z: lab-frame z-coordinate of the beam intersection with the detector plane (x=y=0 on that plane). units of length
|
|
49
|
+
- det_coord_x: lab-frame x-coordinate of the detector origin. This indicates the offset of the detector origin to the beam center in the lab frame. units of length.
|
|
50
|
+
- det_coord_y: lab-frame y-coordinate of the detector origin. This indicates the offset of the detector origin to the beam center in the lab frame. units of length.
|
|
51
|
+
- e_fast/e_slow/e_normal: unit vectors in lab frame (shape (3,)), defining detector orientation.
|
|
52
|
+
- pixel_pitch_{slow,fast}: scalar length/pixel
|
|
53
|
+
|
|
54
|
+
Notes:
|
|
55
|
+
Tilt support will be integrated when needed following the NeXus pitch, yaw, roll for rotations around x, y, z.
|
|
56
|
+
This implementation assumes a non-moving, planar detector.
|
|
57
|
+
For other, instrument-specific implementations, subclass PixelCoordinates3D and replace _load_canonical_frame().
|
|
58
|
+
Origin of the detector is at pixel with index (0,0).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
det_coord_z: BaseData
|
|
62
|
+
det_coord_x: BaseData
|
|
63
|
+
det_coord_y: BaseData
|
|
64
|
+
|
|
65
|
+
e_fast: np.ndarray
|
|
66
|
+
e_slow: np.ndarray
|
|
67
|
+
e_normal: np.ndarray
|
|
68
|
+
|
|
69
|
+
pixel_pitch_slow: BaseData
|
|
70
|
+
pixel_pitch_fast: BaseData
|
|
71
|
+
|
|
72
|
+
def __attrs_post_init__(self):
|
|
73
|
+
object.__setattr__(self, "det_coord_z", prepare_static_scalar(self.det_coord_z, require_units=ureg.m))
|
|
74
|
+
object.__setattr__(self, "det_coord_x", prepare_static_scalar(self.det_coord_x, require_units=ureg.m))
|
|
75
|
+
object.__setattr__(self, "det_coord_y", prepare_static_scalar(self.det_coord_y, require_units=ureg.m))
|
|
76
|
+
|
|
77
|
+
object.__setattr__(
|
|
78
|
+
self,
|
|
79
|
+
"pixel_pitch_slow",
|
|
80
|
+
prepare_static_scalar(self.pixel_pitch_slow, require_units=ureg.m / ureg.pixel),
|
|
81
|
+
)
|
|
82
|
+
object.__setattr__(
|
|
83
|
+
self,
|
|
84
|
+
"pixel_pitch_fast",
|
|
85
|
+
prepare_static_scalar(self.pixel_pitch_fast, require_units=ureg.m / ureg.pixel),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class PixelCoordinates3D(ProcessStep):
|
|
90
|
+
"""
|
|
91
|
+
Primary arrays module: compute 3D pixel center coordinates in lab-frame NeXus-like axes.
|
|
92
|
+
|
|
93
|
+
Outputs (BaseData, length units, detector shape):
|
|
94
|
+
- coord_x
|
|
95
|
+
- coord_y
|
|
96
|
+
- coord_z
|
|
97
|
+
|
|
98
|
+
Notes:
|
|
99
|
+
- output coordinate ndim is clamped to RoD (which can never be larger than signal.ndim), so we never produce arrays larger than the detector.
|
|
100
|
+
- Planar detector assumed; tilt support will be implemented (following NeXus pitch, yaw, roll for rotations around x, y, z) in the future as needed.
|
|
101
|
+
- no sensor thickness offset applied, it is assumed the photon detection happens at the coordinates computed.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
documentation = ProcessStepDescriber(
|
|
105
|
+
calling_name="Add 3D pixel coordinates (generic)",
|
|
106
|
+
calling_id="PixelCoordinates3D",
|
|
107
|
+
calling_module_path=Path(__file__),
|
|
108
|
+
calling_version=__version__,
|
|
109
|
+
required_data_keys=["signal"],
|
|
110
|
+
arguments={
|
|
111
|
+
"det_coord_z_source": {
|
|
112
|
+
"type": (str, type(None)),
|
|
113
|
+
"required": True,
|
|
114
|
+
"default": None,
|
|
115
|
+
"doc": "IoSources key for detector z-coordinate signal.",
|
|
116
|
+
},
|
|
117
|
+
"det_coord_z_units_source": {
|
|
118
|
+
"type": (str, type(None)),
|
|
119
|
+
"default": None,
|
|
120
|
+
"doc": "IoSources key for detector z-coordinate units.",
|
|
121
|
+
},
|
|
122
|
+
"det_coord_z_uncertainties_sources": {
|
|
123
|
+
"type": dict,
|
|
124
|
+
"default": {},
|
|
125
|
+
"doc": "Uncertainty sources for detector z-coordinate.",
|
|
126
|
+
},
|
|
127
|
+
"det_coord_x_source": {
|
|
128
|
+
"type": (str, type(None)),
|
|
129
|
+
"required": True,
|
|
130
|
+
"default": None,
|
|
131
|
+
"doc": "IoSources key for detector x-coordinate signal.",
|
|
132
|
+
},
|
|
133
|
+
"det_coord_x_units_source": {
|
|
134
|
+
"type": (str, type(None)),
|
|
135
|
+
"default": None,
|
|
136
|
+
"doc": "IoSources key for detector x-coordinate units.",
|
|
137
|
+
},
|
|
138
|
+
"det_coord_x_uncertainties_sources": {
|
|
139
|
+
"type": dict,
|
|
140
|
+
"default": {},
|
|
141
|
+
"doc": "Uncertainty sources for detector x-coordinate.",
|
|
142
|
+
},
|
|
143
|
+
"det_coord_y_source": {
|
|
144
|
+
"type": (str, type(None)),
|
|
145
|
+
"required": True,
|
|
146
|
+
"default": None,
|
|
147
|
+
"doc": "IoSources key for detector y-coordinate signal.",
|
|
148
|
+
},
|
|
149
|
+
"det_coord_y_units_source": {
|
|
150
|
+
"type": (str, type(None)),
|
|
151
|
+
"default": None,
|
|
152
|
+
"doc": "IoSources key for detector y-coordinate units.",
|
|
153
|
+
},
|
|
154
|
+
"det_coord_y_uncertainties_sources": {
|
|
155
|
+
"type": dict,
|
|
156
|
+
"default": {},
|
|
157
|
+
"doc": "Uncertainty sources for detector y-coordinate.",
|
|
158
|
+
},
|
|
159
|
+
"pixel_pitch_slow_source": {
|
|
160
|
+
"type": (str, type(None)),
|
|
161
|
+
"required": True,
|
|
162
|
+
"default": None,
|
|
163
|
+
"doc": "IoSources key for slow-axis pixel pitch signal.",
|
|
164
|
+
},
|
|
165
|
+
"pixel_pitch_slow_units_source": {
|
|
166
|
+
"type": (str, type(None)),
|
|
167
|
+
"default": None,
|
|
168
|
+
"doc": "IoSources key for slow-axis pixel pitch units.",
|
|
169
|
+
},
|
|
170
|
+
"pixel_pitch_slow_uncertainties_sources": {
|
|
171
|
+
"type": dict,
|
|
172
|
+
"default": {},
|
|
173
|
+
"doc": "Uncertainty sources for slow-axis pixel pitch.",
|
|
174
|
+
},
|
|
175
|
+
"pixel_pitch_fast_source": {
|
|
176
|
+
"type": (str, type(None)),
|
|
177
|
+
"required": True,
|
|
178
|
+
"default": None,
|
|
179
|
+
"doc": "IoSources key for fast-axis pixel pitch signal.",
|
|
180
|
+
},
|
|
181
|
+
"pixel_pitch_fast_units_source": {
|
|
182
|
+
"type": (str, type(None)),
|
|
183
|
+
"default": None,
|
|
184
|
+
"doc": "IoSources key for fast-axis pixel pitch units.",
|
|
185
|
+
},
|
|
186
|
+
"pixel_pitch_fast_uncertainties_sources": {
|
|
187
|
+
"type": dict,
|
|
188
|
+
"default": {},
|
|
189
|
+
"doc": "Uncertainty sources for fast-axis pixel pitch.",
|
|
190
|
+
},
|
|
191
|
+
"basis_fast": {
|
|
192
|
+
"type": tuple,
|
|
193
|
+
"default": (1.0, 0.0, 0.0),
|
|
194
|
+
"doc": "Basis vector for the fast detector axis.",
|
|
195
|
+
},
|
|
196
|
+
"basis_slow": {
|
|
197
|
+
"type": tuple,
|
|
198
|
+
"default": (0.0, 1.0, 0.0),
|
|
199
|
+
"doc": "Basis vector for the slow detector axis.",
|
|
200
|
+
},
|
|
201
|
+
"basis_normal": {
|
|
202
|
+
"type": tuple,
|
|
203
|
+
"default": (0.0, 0.0, 1.0),
|
|
204
|
+
"doc": "Basis vector for the detector normal.",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
modifies={
|
|
208
|
+
"coord_x": ["signal", "uncertainties"],
|
|
209
|
+
"coord_y": ["signal", "uncertainties"],
|
|
210
|
+
"coord_z": ["signal", "uncertainties"],
|
|
211
|
+
},
|
|
212
|
+
step_keywords=["geometry", "coordinates", "detector"],
|
|
213
|
+
step_doc="Computes 3D pixel center coordinates in lab-frame axes.",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _load_from_sources(self, key: str) -> BaseData:
|
|
217
|
+
return basedata_from_sources(
|
|
218
|
+
io_sources=self.io_sources,
|
|
219
|
+
signal_source=self.configuration.get(f"{key}_source"),
|
|
220
|
+
units_source=self.configuration.get(f"{key}_units_source", None),
|
|
221
|
+
uncertainty_sources=self.configuration.get(f"{key}_uncertainties_sources", {}),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def _load_canonical_frame(
|
|
225
|
+
self,
|
|
226
|
+
*,
|
|
227
|
+
RoD: int,
|
|
228
|
+
detector_shape: Tuple[int, ...],
|
|
229
|
+
reference_signal: BaseData,
|
|
230
|
+
) -> CanonicalDetectorFrame:
|
|
231
|
+
det_coord_z = prepare_static_scalar(
|
|
232
|
+
self._load_from_sources("det_coord_z"), require_units=ureg.m, uncertainty_key="detector_position_jitter"
|
|
233
|
+
) # scalar length
|
|
234
|
+
det_coord_x = prepare_static_scalar(
|
|
235
|
+
self._load_from_sources("det_coord_x"), require_units=ureg.m, uncertainty_key="detector_position_jitter"
|
|
236
|
+
) # scalar length
|
|
237
|
+
det_coord_y = prepare_static_scalar(
|
|
238
|
+
self._load_from_sources("det_coord_y"), require_units=ureg.m, uncertainty_key="detector_position_jitter"
|
|
239
|
+
) # scalar length
|
|
240
|
+
|
|
241
|
+
pitch_slow = prepare_static_scalar(
|
|
242
|
+
self._load_from_sources("pixel_pitch_slow"),
|
|
243
|
+
require_units=ureg.m / ureg.pixel,
|
|
244
|
+
uncertainty_key="pixel_pitch_jitter",
|
|
245
|
+
) # scalar length/pixel
|
|
246
|
+
pitch_fast = prepare_static_scalar(
|
|
247
|
+
self._load_from_sources("pixel_pitch_fast"),
|
|
248
|
+
require_units=ureg.m / ureg.pixel,
|
|
249
|
+
uncertainty_key="pixel_pitch_jitter",
|
|
250
|
+
) # scalar length/pixel
|
|
251
|
+
|
|
252
|
+
e_fast = unit_vec3(self.configuration.get("basis_fast", (1.0, 0.0, 0.0)), name="basis_fast")
|
|
253
|
+
e_slow = unit_vec3(self.configuration.get("basis_slow", (0.0, 1.0, 0.0)), name="basis_slow")
|
|
254
|
+
e_norm = unit_vec3(self.configuration.get("basis_normal", (0.0, 0.0, 1.0)), name="basis_normal")
|
|
255
|
+
|
|
256
|
+
return CanonicalDetectorFrame(
|
|
257
|
+
det_coord_z=det_coord_z,
|
|
258
|
+
det_coord_x=det_coord_x,
|
|
259
|
+
det_coord_y=det_coord_y,
|
|
260
|
+
e_fast=e_fast,
|
|
261
|
+
e_slow=e_slow,
|
|
262
|
+
e_normal=e_norm,
|
|
263
|
+
pixel_pitch_slow=pitch_slow,
|
|
264
|
+
pixel_pitch_fast=pitch_fast,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# ----------------------------
|
|
268
|
+
# rank/shape helpers
|
|
269
|
+
# ----------------------------
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def _detector_shape(signal_bd: BaseData, RoD: int) -> Tuple[int, ...]:
|
|
273
|
+
return () if RoD <= 0 else tuple(signal_bd.signal.shape[-RoD:])
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def _require_scalar(name: str, bd: BaseData) -> None:
|
|
277
|
+
if np.size(bd.signal) != 1:
|
|
278
|
+
raise ValueError(f"{name} must be scalar (size==1). Got shape={np.shape(bd.signal)}.")
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def _unit(v: np.ndarray | Tuple[float, float, float]) -> np.ndarray:
|
|
282
|
+
v = np.asarray(v, dtype=float).reshape(3)
|
|
283
|
+
n = float(np.linalg.norm(v))
|
|
284
|
+
if n == 0.0:
|
|
285
|
+
raise ValueError("basis vector must be non-zero")
|
|
286
|
+
return v / n
|
|
287
|
+
|
|
288
|
+
# ----------------------------
|
|
289
|
+
# broadcast-friendly pixel indices (pixel-center convention)
|
|
290
|
+
# ----------------------------
|
|
291
|
+
|
|
292
|
+
@staticmethod
|
|
293
|
+
def _idx_fast_1d(n_fast: int) -> BaseData:
|
|
294
|
+
sig = np.arange(n_fast, dtype=float) + 0.5
|
|
295
|
+
return BaseData(signal=sig, units=ureg.pixel, uncertainties={"pixel_index_fast": np.full_like(sig, 0.5)})
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def _idx_slow_2d(n_slow: int) -> BaseData:
|
|
299
|
+
sig = (np.arange(n_slow, dtype=float) + 0.5)[:, None]
|
|
300
|
+
return BaseData(signal=sig, units=ureg.pixel, uncertainties={"pixel_index_slow": np.full_like(sig, 0.5)})
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def _idx_fast_2d(n_fast: int) -> BaseData:
|
|
304
|
+
sig = (np.arange(n_fast, dtype=float) + 0.5)[None, :]
|
|
305
|
+
return BaseData(signal=sig, units=ureg.pixel, uncertainties={"pixel_index_fast": np.full_like(sig, 0.5)})
|
|
306
|
+
|
|
307
|
+
# ----------------------------
|
|
308
|
+
# core compute
|
|
309
|
+
# ----------------------------
|
|
310
|
+
|
|
311
|
+
def _compute_pixel_positions(
|
|
312
|
+
self,
|
|
313
|
+
*,
|
|
314
|
+
RoD: int,
|
|
315
|
+
detector_shape: Tuple[int, ...],
|
|
316
|
+
frame: CanonicalDetectorFrame,
|
|
317
|
+
) -> Dict[str, BaseData]:
|
|
318
|
+
"""
|
|
319
|
+
here:
|
|
320
|
+
det_coord_x/y/z represent the lab-frame position of the detector “pixel grid origin” (i.e. the corner before applying the +0.5 pixel-center shift).
|
|
321
|
+
Pixel centers are at (i+0.5, j+0.5) which matches the current _idx_* methods and the ±0.5 px index uncertainty.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
# Scalars in length units (already averaged + SEM in CanonicalDetectorFrame.__attrs_post_init__)
|
|
325
|
+
ox = require_scalar("det_coord_x", frame.det_coord_x)
|
|
326
|
+
oy = require_scalar("det_coord_y", frame.det_coord_y)
|
|
327
|
+
oz = require_scalar("det_coord_z", frame.det_coord_z)
|
|
328
|
+
pitch_fast = require_scalar("pixel_pitch_fast", frame.pixel_pitch_fast)
|
|
329
|
+
pitch_slow = require_scalar("pixel_pitch_slow", frame.pixel_pitch_slow)
|
|
330
|
+
|
|
331
|
+
e_fast = unit_vec3(frame.e_fast, name="e_fast")
|
|
332
|
+
e_slow = unit_vec3(frame.e_slow, name="e_slow")
|
|
333
|
+
# e_normal kept for future tilt support
|
|
334
|
+
|
|
335
|
+
# RoD==0: no detector axes, just return the detector origin position as scalars
|
|
336
|
+
if RoD == 0:
|
|
337
|
+
return {"coord_x": ox, "coord_y": oy, "coord_z": oz}
|
|
338
|
+
|
|
339
|
+
# RoD==1: one detector axis ("fast")
|
|
340
|
+
if RoD == 1:
|
|
341
|
+
(n_fast,) = detector_shape
|
|
342
|
+
i_fast_px = self._idx_fast_1d(n_fast) # (n_fast,), centers at i+0.5
|
|
343
|
+
|
|
344
|
+
off_fast = i_fast_px * pitch_fast # length along fast axis
|
|
345
|
+
|
|
346
|
+
coord_x = ox + (off_fast * e_fast[0])
|
|
347
|
+
coord_y = oy + (off_fast * e_fast[1])
|
|
348
|
+
coord_z = oz + (off_fast * e_fast[2])
|
|
349
|
+
return {"coord_x": coord_x, "coord_y": coord_y, "coord_z": coord_z}
|
|
350
|
+
|
|
351
|
+
# RoD==2: (slow, fast)
|
|
352
|
+
if RoD != 2:
|
|
353
|
+
raise NotImplementedError(
|
|
354
|
+
f"PixelCoordinates3D: only RoD in (0, 1, 2) supported; got RoD={RoD}." # noqa: E702
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
n_slow, n_fast = detector_shape
|
|
358
|
+
j_slow_px = self._idx_slow_2d(n_slow) # (n_slow, 1)
|
|
359
|
+
i_fast_px = self._idx_fast_2d(n_fast) # (1, n_fast)
|
|
360
|
+
|
|
361
|
+
off_slow = j_slow_px * pitch_slow # (n_slow, 1) length
|
|
362
|
+
off_fast = i_fast_px * pitch_fast # (1, n_fast) length
|
|
363
|
+
|
|
364
|
+
# Broadcast to (n_slow, n_fast) automatically via BaseData arithmetic
|
|
365
|
+
coord_x = ox + (off_slow * e_slow[0]) + (off_fast * e_fast[0])
|
|
366
|
+
coord_y = oy + (off_slow * e_slow[1]) + (off_fast * e_fast[1])
|
|
367
|
+
coord_z = oz + (off_slow * e_slow[2]) + (off_fast * e_fast[2])
|
|
368
|
+
|
|
369
|
+
return {"coord_x": coord_x, "coord_y": coord_y, "coord_z": coord_z}
|
|
370
|
+
|
|
371
|
+
# ----------------------------
|
|
372
|
+
# ProcessStep lifecycle
|
|
373
|
+
# ----------------------------
|
|
374
|
+
|
|
375
|
+
def prepare_execution(self):
|
|
376
|
+
super().prepare_execution()
|
|
377
|
+
|
|
378
|
+
with_keys = self.configuration.get("with_processing_keys") or []
|
|
379
|
+
if not with_keys:
|
|
380
|
+
raise ValueError("PixelCoordinates3D: configuration.with_processing_keys is empty.")
|
|
381
|
+
|
|
382
|
+
ref_signal: BaseData = self.processing_data[with_keys[0]]["signal"]
|
|
383
|
+
|
|
384
|
+
RoD = ref_signal.rank_of_data
|
|
385
|
+
if RoD not in (0, 1, 2):
|
|
386
|
+
raise NotImplementedError(
|
|
387
|
+
f"PixelCoordinates3D: only RoD in (0, 1, 2) supported; got RoD={RoD}." # noqa: E702
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
detector_shape = self._detector_shape(ref_signal, RoD)
|
|
391
|
+
frame = self._load_canonical_frame(RoD=RoD, detector_shape=detector_shape, reference_signal=ref_signal)
|
|
392
|
+
outputs = self._compute_pixel_positions(RoD=RoD, detector_shape=detector_shape, frame=frame)
|
|
393
|
+
|
|
394
|
+
for bd in outputs.values():
|
|
395
|
+
bd.rank_of_data = min(RoD, int(np.ndim(bd.signal)))
|
|
396
|
+
|
|
397
|
+
self._prepared_data = {k: outputs[k] for k in ("coord_x", "coord_y", "coord_z")}
|
|
398
|
+
|
|
399
|
+
def calculate(self):
|
|
400
|
+
with_keys = self.configuration.get("with_processing_keys") or []
|
|
401
|
+
if not with_keys:
|
|
402
|
+
logger.warning("PixelCoordinates3D: no with_processing_keys specified; nothing to do.")
|
|
403
|
+
return {}
|
|
404
|
+
|
|
405
|
+
out: Dict[str, object] = {}
|
|
406
|
+
for key in with_keys:
|
|
407
|
+
bundle = self.processing_data.get(key)
|
|
408
|
+
if bundle is None:
|
|
409
|
+
logger.warning(
|
|
410
|
+
f"PixelCoordinates3D: processing_data has no entry for key={key!r}; skipping." # noqa: E702
|
|
411
|
+
) # noqa: E702
|
|
412
|
+
continue
|
|
413
|
+
for out_key, bd in self._prepared_data.items():
|
|
414
|
+
bundle[out_key] = bd
|
|
415
|
+
out[key] = bundle
|
|
416
|
+
|
|
417
|
+
return out
|
|
@@ -0,0 +1,63 @@
|
|
|
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", "Armin Moser"] # add names to the list as appropriate
|
|
9
|
+
__copyright__ = "Copyright 2025, The MoDaCor team"
|
|
10
|
+
__date__ = "29/10/2025"
|
|
11
|
+
__status__ = "Development" # "Development", "Production"
|
|
12
|
+
# end of header and standard imports
|
|
13
|
+
|
|
14
|
+
__all__ = ["SolidAngleCorrection"]
|
|
15
|
+
__version__ = "20251029.1"
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# from modacor import ureg
|
|
20
|
+
# from modacor.dataclasss.basedata import BaseData
|
|
21
|
+
from modacor.dataclasses.databundle import DataBundle
|
|
22
|
+
from modacor.dataclasses.process_step import ProcessStep
|
|
23
|
+
from modacor.dataclasses.process_step_describer import ProcessStepDescriber
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SolidAngleCorrection(ProcessStep):
|
|
27
|
+
"""
|
|
28
|
+
Normalize a signal by a solid angle "Omega" calculated using XSGeometry
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
documentation = ProcessStepDescriber(
|
|
32
|
+
calling_name="Solid Angle Correction",
|
|
33
|
+
calling_id="SolidAngleCorrection",
|
|
34
|
+
calling_module_path=Path(__file__),
|
|
35
|
+
calling_version=__version__,
|
|
36
|
+
required_data_keys=["signal", "Omega"],
|
|
37
|
+
modifies={"signal": ["signal", "uncertainties", "units"]},
|
|
38
|
+
arguments={
|
|
39
|
+
"with_processing_keys": {
|
|
40
|
+
"type": list,
|
|
41
|
+
"required": True,
|
|
42
|
+
"default": None,
|
|
43
|
+
"doc": "ProcessingData keys whose signal should be divided by Omega.",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
step_keywords=["divide", "normalize", "solid angle"],
|
|
47
|
+
step_doc="Divide the pixels in a signal by their solid angle coverage",
|
|
48
|
+
step_reference="DOI 10.1088/0953-8984/25/38/383201",
|
|
49
|
+
step_note="""This divides the signal by the value previously calculated
|
|
50
|
+
using the XSGeometry module""",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def calculate(self) -> dict[str, DataBundle]:
|
|
54
|
+
output: dict[str, DataBundle] = {}
|
|
55
|
+
|
|
56
|
+
# actual work happens here:
|
|
57
|
+
for key in self._normalised_processing_keys():
|
|
58
|
+
databundle = self.processing_data.get(key)
|
|
59
|
+
# divide the data
|
|
60
|
+
# Rely on BaseData.__truediv__ for units + uncertainty propagation
|
|
61
|
+
databundle["signal"] /= databundle["Omega"]
|
|
62
|
+
output[key] = databundle
|
|
63
|
+
return output
|