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,293 @@
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
+ from typing import Dict, Tuple
9
+
10
+ import numpy as np
11
+
12
+ from modacor import ureg
13
+ from modacor.dataclasses.basedata import BaseData
14
+ from modacor.dataclasses.helpers import basedata_from_sources
15
+ from modacor.dataclasses.messagehandler import MessageHandler
16
+ from modacor.dataclasses.process_step import ProcessStep
17
+ from modacor.dataclasses.process_step_describer import ProcessStepDescriber
18
+ from modacor.modules.technique_modules.scattering.geometry_helpers import (
19
+ prepare_static_scalar,
20
+ require_scalar,
21
+ unit_vec3,
22
+ )
23
+
24
+ logger = MessageHandler(name=__name__)
25
+
26
+ __version__ = "20260106.1"
27
+ __all__ = ["XSGeometryFromPixelCoordinates"]
28
+
29
+
30
+ class XSGeometryFromPixelCoordinates(ProcessStep):
31
+ """
32
+ Compute scattering geometry from precomputed lab-frame pixel coordinates.
33
+
34
+ Inputs in each databundle:
35
+ - coord_x, coord_y, coord_z (BaseData arrays, length units)
36
+
37
+ Inputs from configuration sources:
38
+ - sample_z: scalar length (sample is at (0,0,sample_z))
39
+ - wavelength: scalar length
40
+ - pixel_pitch_fast, pixel_pitch_slow: scalar length/pixel (for Omega)
41
+
42
+ Outputs:
43
+ - Q0, Q1, Q2, Q, Psi, TwoTheta, Omega
44
+ """
45
+
46
+ documentation = ProcessStepDescriber(
47
+ calling_name="Add Q, Psi, TwoTheta, Omega from pixel coordinates",
48
+ calling_id="XSGeometryFromPixelCoordinates",
49
+ calling_module_path=Path(__file__),
50
+ calling_version=__version__,
51
+ required_data_keys=["coord_x", "coord_y", "coord_z"],
52
+ arguments={
53
+ "sample_z_source": {
54
+ "type": (str, type(None)),
55
+ "required": True,
56
+ "default": None,
57
+ "doc": "IoSources key for sample z-position signal.",
58
+ },
59
+ "sample_z_units_source": {
60
+ "type": (str, type(None)),
61
+ "default": None,
62
+ "doc": "IoSources key for sample z-position units.",
63
+ },
64
+ "sample_z_uncertainties_sources": {
65
+ "type": dict,
66
+ "default": {},
67
+ "doc": "Uncertainty sources for sample z-position.",
68
+ },
69
+ "wavelength_source": {
70
+ "type": (str, type(None)),
71
+ "required": True,
72
+ "default": None,
73
+ "doc": "IoSources key for wavelength signal.",
74
+ },
75
+ "wavelength_units_source": {
76
+ "type": (str, type(None)),
77
+ "default": None,
78
+ "doc": "IoSources key for wavelength units.",
79
+ },
80
+ "wavelength_uncertainties_sources": {
81
+ "type": dict,
82
+ "default": {},
83
+ "doc": "Uncertainty sources for wavelength.",
84
+ },
85
+ "pixel_pitch_slow_source": {
86
+ "type": (str, type(None)),
87
+ "required": True,
88
+ "default": None,
89
+ "doc": "IoSources key for slow-axis pixel pitch signal.",
90
+ },
91
+ "pixel_pitch_slow_units_source": {
92
+ "type": (str, type(None)),
93
+ "default": None,
94
+ "doc": "IoSources key for slow-axis pixel pitch units.",
95
+ },
96
+ "pixel_pitch_slow_uncertainties_sources": {
97
+ "type": dict,
98
+ "default": {},
99
+ "doc": "Uncertainty sources for slow-axis pixel pitch.",
100
+ },
101
+ "pixel_pitch_fast_source": {
102
+ "type": (str, type(None)),
103
+ "required": True,
104
+ "default": None,
105
+ "doc": "IoSources key for fast-axis pixel pitch signal.",
106
+ },
107
+ "pixel_pitch_fast_units_source": {
108
+ "type": (str, type(None)),
109
+ "default": None,
110
+ "doc": "IoSources key for fast-axis pixel pitch units.",
111
+ },
112
+ "pixel_pitch_fast_uncertainties_sources": {
113
+ "type": dict,
114
+ "default": {},
115
+ "doc": "Uncertainty sources for fast-axis pixel pitch.",
116
+ },
117
+ "detector_normal": {
118
+ "type": tuple,
119
+ "default": (0.0, 0.0, 1.0),
120
+ "doc": "Detector normal unit vector in lab frame.",
121
+ },
122
+ },
123
+ modifies={
124
+ "Q0": ["signal", "uncertainties"],
125
+ "Q1": ["signal", "uncertainties"],
126
+ "Q2": ["signal", "uncertainties"],
127
+ "Q": ["signal", "uncertainties"],
128
+ "Psi": ["signal"], # computed from nominal x/y only
129
+ "TwoTheta": ["signal", "uncertainties"],
130
+ "Omega": ["signal", "uncertainties"],
131
+ },
132
+ step_keywords=["geometry", "Q", "Psi", "TwoTheta", "Solid Angle", "Omega", "scattering"],
133
+ step_doc="Compute Q-vector components and angles from lab-frame pixel coordinates.",
134
+ )
135
+
136
+ output_keys: Tuple[str, ...] = ("Q0", "Q1", "Q2", "Q", "Psi", "TwoTheta", "Omega")
137
+
138
+ # ----------------------------
139
+ # loading helpers
140
+ # ----------------------------
141
+
142
+ def _load_from_sources(self, key: str) -> BaseData:
143
+ return basedata_from_sources(
144
+ io_sources=self.io_sources,
145
+ signal_source=self.configuration.get(f"{key}_source"),
146
+ units_source=self.configuration.get(f"{key}_units_source", None),
147
+ uncertainty_sources=self.configuration.get(f"{key}_uncertainties_sources", {}),
148
+ )
149
+
150
+ # ----------------------------
151
+ # core compute
152
+ # ----------------------------
153
+
154
+ def _compute(
155
+ self,
156
+ *,
157
+ coord_x: BaseData,
158
+ coord_y: BaseData,
159
+ coord_z: BaseData,
160
+ sample_z: BaseData,
161
+ wavelength: BaseData,
162
+ pitch_slow: BaseData,
163
+ pitch_fast: BaseData,
164
+ detector_normal: np.ndarray,
165
+ ) -> Dict[str, BaseData]:
166
+ # sample position is (0,0,sample_z)
167
+ dz = coord_z - sample_z
168
+ dx = coord_x
169
+ dy = coord_y
170
+
171
+ # ray length
172
+ R = ((dx**2) + (dy**2) + (dz**2)).sqrt()
173
+
174
+ # angles
175
+ r_perp = ((dx**2) + (dy**2)).sqrt()
176
+ TwoTheta = (r_perp / dz).arctan() # radians
177
+
178
+ # k = 2π/λ
179
+ two_pi = float(2.0 * np.pi)
180
+ k = two_pi / wavelength # 1/length
181
+
182
+ # unit direction to pixel
183
+ rhat_x = dx / R
184
+ rhat_y = dy / R
185
+ rhat_z = dz / R
186
+
187
+ # q = k_out - k_in, with k_in along +z: (0,0,k)
188
+ Q0 = k * rhat_x
189
+ Q1 = k * rhat_y
190
+ Q2 = k * (rhat_z - 1.0)
191
+
192
+ Q = ((Q0**2) + (Q1**2) + (Q2**2)).sqrt()
193
+
194
+ # Psi from NOMINAL geometry only (matches your earlier approach)
195
+ psi_signal = np.arctan2(dy.signal, dx.signal)
196
+ Psi = BaseData(signal=psi_signal, units=ureg.radian)
197
+
198
+ # Solid angle per pixel:
199
+ # dΩ ≈ A * cos(alpha) / R^2, with cos(alpha)=n·rhat
200
+ # Here A from pitches (length/pixel)×(length/pixel).
201
+ n = detector_normal
202
+ cos_alpha = (rhat_x * n[0]) + (rhat_y * n[1]) + (rhat_z * n[2])
203
+ cos_alpha_clipped = cos_alpha.copy()
204
+ cos_alpha_clipped.signal = np.clip(cos_alpha.signal, 0.0, None)
205
+ cos_alpha = cos_alpha_clipped
206
+
207
+ one_px = BaseData(signal=np.array(1.0, dtype=float), units=ureg.pixel, rank_of_data=0)
208
+ area_pixel = (pitch_fast * one_px) * (pitch_slow * one_px) # -> m^2
209
+ Omega = (area_pixel * cos_alpha) / (R**2)
210
+ Omega.units = ureg.steradian # (steradian is dimensionless, but explicit is fine)
211
+
212
+ return {"Q0": Q0, "Q1": Q1, "Q2": Q2, "Q": Q, "Psi": Psi, "TwoTheta": TwoTheta, "Omega": Omega}
213
+
214
+ # ----------------------------
215
+ # ProcessStep lifecycle
216
+ # ----------------------------
217
+
218
+ def prepare_execution(self):
219
+ super().prepare_execution()
220
+
221
+ with_keys = self.configuration.get("with_processing_keys") or []
222
+ if not with_keys:
223
+ raise ValueError("XSGeometryFromPixelCoordinates: configuration.with_processing_keys is empty.")
224
+
225
+ # reference bundle
226
+ ref = self.processing_data[with_keys[0]]
227
+ coord_x: BaseData = ref["coord_x"]
228
+ coord_y: BaseData = ref["coord_y"]
229
+ coord_z: BaseData = ref["coord_z"]
230
+
231
+ RoD = int(
232
+ getattr(coord_x, "rank_of_data", ref["signal"].rank_of_data if "signal" in ref else np.ndim(coord_x.signal))
233
+ )
234
+
235
+ sample_z = prepare_static_scalar(
236
+ self._load_from_sources("sample_z"), require_units=ureg.m, uncertainty_key="sample_position_jitter"
237
+ )
238
+ wavelength = prepare_static_scalar(
239
+ self._load_from_sources("wavelength"), require_units=ureg.m, uncertainty_key="wavelength_jitter"
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
+ )
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
+ )
251
+
252
+ detector_normal = unit_vec3(self.configuration.get("detector_normal", (0.0, 0.0, 1.0)), name="detector_normal")
253
+
254
+ # (optional) enforce scalar-ness right before compute:
255
+ sample_z = require_scalar("sample_z", sample_z)
256
+ wavelength = require_scalar("wavelength", wavelength)
257
+ pitch_slow = require_scalar("pixel_pitch_slow", pitch_slow)
258
+ pitch_fast = require_scalar("pixel_pitch_fast", pitch_fast)
259
+
260
+ out = self._compute(
261
+ coord_x=coord_x,
262
+ coord_y=coord_y,
263
+ coord_z=coord_z,
264
+ sample_z=sample_z,
265
+ wavelength=wavelength,
266
+ pitch_slow=pitch_slow,
267
+ pitch_fast=pitch_fast,
268
+ detector_normal=detector_normal,
269
+ )
270
+
271
+ for bd in out.values():
272
+ bd.rank_of_data = min(RoD, int(np.ndim(bd.signal)))
273
+
274
+ self._prepared_data = {k: out[k] for k in self.output_keys}
275
+
276
+ def calculate(self):
277
+ with_keys = self.configuration.get("with_processing_keys") or []
278
+ if not with_keys:
279
+ logger.warning("XSGeometryFromPixelCoordinates: no with_processing_keys specified; nothing to do.")
280
+ return {}
281
+
282
+ out: Dict[str, object] = {}
283
+ for key in with_keys:
284
+ bundle = self.processing_data.get(key)
285
+ if bundle is None:
286
+ logger.warning(
287
+ f"XSGeometryFromPixelCoordinates: no processing_data entry for key={key!r}; skipping." # noqa: E702
288
+ )
289
+ continue
290
+ for out_key, bd in self._prepared_data.items():
291
+ bundle[out_key] = bd
292
+ out[key] = bundle
293
+ return out
File without changes