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,282 @@
|
|
|
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__ = ["Malte Storm", "Jérome Kieffer", "Anja Hörmann", "Brian R. Pauw"] # add names to the list as appropriate
|
|
9
|
+
__copyright__ = "Copyright 2025, The MoDaCor team"
|
|
10
|
+
__date__ = "16/11/2025"
|
|
11
|
+
__status__ = "Development" # "Development", "Production"
|
|
12
|
+
# end of header and standard imports
|
|
13
|
+
|
|
14
|
+
from typing import Iterable
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from modacor import ureg
|
|
20
|
+
from modacor.dataclasses.basedata import BaseData
|
|
21
|
+
from modacor.dataclasses.databundle import DataBundle
|
|
22
|
+
from modacor.dataclasses.process_step import ProcessStep
|
|
23
|
+
from modacor.dataclasses.processing_data import ProcessingData
|
|
24
|
+
from modacor.io import IoSources
|
|
25
|
+
|
|
26
|
+
TEST_IO_SOURCES = IoSources()
|
|
27
|
+
|
|
28
|
+
_TEST_KEYS = {
|
|
29
|
+
"test_str": {
|
|
30
|
+
"type": str,
|
|
31
|
+
"allow_iterable": False,
|
|
32
|
+
"allow_none": False,
|
|
33
|
+
"default": "",
|
|
34
|
+
},
|
|
35
|
+
"test_str_allow_list": {
|
|
36
|
+
"type": str,
|
|
37
|
+
"allow_iterable": True,
|
|
38
|
+
"allow_none": False,
|
|
39
|
+
"default": "",
|
|
40
|
+
},
|
|
41
|
+
"test_str_allow_none": {
|
|
42
|
+
"type": str,
|
|
43
|
+
"allow_iterable": False,
|
|
44
|
+
"allow_none": True,
|
|
45
|
+
"default": None,
|
|
46
|
+
},
|
|
47
|
+
"test_str_allow_list_none": {
|
|
48
|
+
"type": str,
|
|
49
|
+
"allow_iterable": True,
|
|
50
|
+
"allow_none": True,
|
|
51
|
+
"default": None,
|
|
52
|
+
},
|
|
53
|
+
"test_int": {
|
|
54
|
+
"type": int,
|
|
55
|
+
"allow_iterable": False,
|
|
56
|
+
"allow_none": False,
|
|
57
|
+
"default": 0,
|
|
58
|
+
},
|
|
59
|
+
"test_int_allow_list": {
|
|
60
|
+
"type": int,
|
|
61
|
+
"allow_iterable": True,
|
|
62
|
+
"allow_none": False,
|
|
63
|
+
"default": 0,
|
|
64
|
+
},
|
|
65
|
+
"test_int_allow_none": {
|
|
66
|
+
"type": int,
|
|
67
|
+
"allow_iterable": False,
|
|
68
|
+
"allow_none": True,
|
|
69
|
+
"default": None,
|
|
70
|
+
},
|
|
71
|
+
"test_int_allow_list_none": {
|
|
72
|
+
"type": int,
|
|
73
|
+
"allow_iterable": True,
|
|
74
|
+
"allow_none": True,
|
|
75
|
+
"default": None,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_TEST_VALUES = ["", "test", 1, 42, None]
|
|
80
|
+
_TEST_LISTS = [["", "2"], ["", 12], [12, 42], [12, None], ["b", None]]
|
|
81
|
+
_TEST_TUPLES = [("12", "b"), ("", 12), (12, 42), ("a", None), (12, None)]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TESTProcessingStep(ProcessStep):
|
|
85
|
+
CONFIG_KEYS = {_k: _v for _k, _v in _TEST_KEYS.items()}
|
|
86
|
+
|
|
87
|
+
def calculate(self) -> dict[str, DataBundle]:
|
|
88
|
+
_data = self.processing_data.get("dummy_key", DataBundle())
|
|
89
|
+
_data["new_key"] = BaseData(signal=np.arange(100).reshape(10, 10), uncertainties={"sem": 0.0}, units=ureg.meter)
|
|
90
|
+
_data2 = self.processing_data.get("bundle2", DataBundle())
|
|
91
|
+
_data2["new_key"] = BaseData(signal=np.zeros(20), uncertainties={"sem": 0.0}, units=ureg.meter)
|
|
92
|
+
return {"dummy_key": _data, "bundle2": _data2}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.fixture
|
|
96
|
+
def class_with_config_keys(request):
|
|
97
|
+
_keys = request.param
|
|
98
|
+
|
|
99
|
+
class TestClass(TESTProcessingStep):
|
|
100
|
+
CONFIG_KEYS = {_key: _TEST_KEYS[_key] for _key in _keys}
|
|
101
|
+
|
|
102
|
+
return _keys, TestClass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.fixture
|
|
106
|
+
def processing_data():
|
|
107
|
+
data = ProcessingData()
|
|
108
|
+
data["bundle1"] = DataBundle()
|
|
109
|
+
data["bundle2"] = DataBundle()
|
|
110
|
+
data["bundle1"]["key1"] = BaseData(signal=np.arange(50), uncertainties={"sem": 0.0}, units=ureg.meter)
|
|
111
|
+
data["bundle2"]["key2"] = BaseData(signal=np.ones((10, 10)), uncertainties={"sem": 0.0}, units=ureg.meter)
|
|
112
|
+
return data
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_process_step_default_config__generic():
|
|
116
|
+
_defaults = ProcessStep.default_config()
|
|
117
|
+
assert isinstance(_defaults, dict)
|
|
118
|
+
assert all(key in _defaults for key in ProcessStep.CONFIG_KEYS.keys())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.parametrize(
|
|
122
|
+
"class_with_config_keys", [["test_str"], ["test_int"], ["test_str", "test_int"]], indirect=True
|
|
123
|
+
)
|
|
124
|
+
def test_process_step_default_config__specific(class_with_config_keys):
|
|
125
|
+
_keys, _class = class_with_config_keys
|
|
126
|
+
_defaults = _class.default_config()
|
|
127
|
+
assert isinstance(_defaults, dict)
|
|
128
|
+
assert all(key in _defaults for key in _keys)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.mark.parametrize("item", _TEST_VALUES + _TEST_LISTS + _TEST_TUPLES)
|
|
132
|
+
@pytest.mark.parametrize("class_with_config_keys", [[_k] for _k in _TEST_KEYS.keys()], indirect=True)
|
|
133
|
+
def test_is_process_step_dict__w_correct_key(class_with_config_keys, item):
|
|
134
|
+
_keys, _class = class_with_config_keys
|
|
135
|
+
_config = _class.CONFIG_KEYS[_keys[0]]
|
|
136
|
+
_test_dict = {_keys[0]: item}
|
|
137
|
+
if item is None:
|
|
138
|
+
assert _class.is_process_step_dict(None, None, _test_dict) == _config["allow_none"]
|
|
139
|
+
elif not _config["allow_iterable"]:
|
|
140
|
+
assert _class.is_process_step_dict(None, None, _test_dict) == isinstance(item, _config["type"])
|
|
141
|
+
elif _config["allow_iterable"]:
|
|
142
|
+
assert _class.is_process_step_dict(None, None, _test_dict) == (
|
|
143
|
+
(isinstance(item, Iterable) and not isinstance(item, str))
|
|
144
|
+
and all(isinstance(i, _config["type"]) for i in item)
|
|
145
|
+
or isinstance(item, _config["type"])
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
assert False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_is_process_step_dict__w_wrong_key():
|
|
152
|
+
test_dict = ProcessStep.default_config() | {"wrong_key": "value"}
|
|
153
|
+
assert not ProcessStep.is_process_step_dict(None, None, test_dict)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_minimal_instantiation():
|
|
157
|
+
ps = ProcessStep(io_sources=TEST_IO_SOURCES)
|
|
158
|
+
assert isinstance(ps, ProcessStep)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_instantiation_of_subclass():
|
|
162
|
+
instance = TESTProcessingStep(io_sources=TEST_IO_SOURCES)
|
|
163
|
+
assert all(k in instance.configuration for k in TESTProcessingStep.CONFIG_KEYS)
|
|
164
|
+
assert isinstance(instance, TESTProcessingStep)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_process_step__reset():
|
|
168
|
+
ps = ProcessStep(io_sources=TEST_IO_SOURCES)
|
|
169
|
+
ps.produced_outputs = {"a": 1}
|
|
170
|
+
ps._ProcessStep__prepared = True
|
|
171
|
+
ps.executed = True
|
|
172
|
+
ps.reset()
|
|
173
|
+
assert ps.produced_outputs == {}
|
|
174
|
+
assert ps._ProcessStep__prepared is False
|
|
175
|
+
assert ps.executed is False
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_normalised_processing_keys_with_single_entry():
|
|
179
|
+
data = ProcessingData()
|
|
180
|
+
data["only"] = DataBundle()
|
|
181
|
+
ps = ProcessStep(io_sources=TEST_IO_SOURCES, processing_data=data)
|
|
182
|
+
ps.configuration["with_processing_keys"] = None
|
|
183
|
+
assert ps._normalised_processing_keys() == ["only"]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_normalised_processing_keys_with_multiple_entries_requires_explicit_keys():
|
|
187
|
+
data = ProcessingData()
|
|
188
|
+
data["a"] = DataBundle()
|
|
189
|
+
data["b"] = DataBundle()
|
|
190
|
+
ps = ProcessStep(io_sources=TEST_IO_SOURCES, processing_data=data)
|
|
191
|
+
ps.configuration["with_processing_keys"] = None
|
|
192
|
+
with pytest.raises(ValueError):
|
|
193
|
+
ps._normalised_processing_keys()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_normalised_processing_keys_accepts_string_or_list():
|
|
197
|
+
data = ProcessingData()
|
|
198
|
+
data["a"] = DataBundle()
|
|
199
|
+
ps = ProcessStep(io_sources=TEST_IO_SOURCES, processing_data=data)
|
|
200
|
+
ps.configuration["with_processing_keys"] = "a"
|
|
201
|
+
assert ps._normalised_processing_keys() == ["a"]
|
|
202
|
+
|
|
203
|
+
ps.configuration["with_processing_keys"] = ["a", "b"]
|
|
204
|
+
assert ps._normalised_processing_keys() == ["a", "b"]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_normalised_processing_keys_rejects_empty_list():
|
|
208
|
+
data = ProcessingData()
|
|
209
|
+
data["a"] = DataBundle()
|
|
210
|
+
ps = ProcessStep(io_sources=TEST_IO_SOURCES, processing_data=data)
|
|
211
|
+
ps.configuration["with_processing_keys"] = []
|
|
212
|
+
with pytest.raises(ValueError):
|
|
213
|
+
ps._normalised_processing_keys()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_normalised_processing_keys_requires_processing_data():
|
|
217
|
+
ps = ProcessStep(io_sources=TEST_IO_SOURCES, processing_data=None)
|
|
218
|
+
ps.configuration["with_processing_keys"] = ["a"]
|
|
219
|
+
with pytest.raises(RuntimeError):
|
|
220
|
+
ps._normalised_processing_keys()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@pytest.mark.parametrize("class_with_config_keys", [["test_str"]], indirect=True)
|
|
224
|
+
def test_modify_config__valid_key(class_with_config_keys):
|
|
225
|
+
instance = class_with_config_keys[1](io_sources=TEST_IO_SOURCES)
|
|
226
|
+
instance.modify_config_by_kwargs(test_str="new_value")
|
|
227
|
+
assert instance.configuration["test_str"] == "new_value"
|
|
228
|
+
assert not instance._ProcessStep__prepared
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@pytest.mark.parametrize("class_with_config_keys", [["test_str"]], indirect=True)
|
|
232
|
+
def test_modify_config__by_dict(class_with_config_keys):
|
|
233
|
+
instance = class_with_config_keys[1](io_sources=TEST_IO_SOURCES)
|
|
234
|
+
instance.modify_config_by_dict({"test_str": "new_value"})
|
|
235
|
+
assert instance.configuration["test_str"] == "new_value"
|
|
236
|
+
assert not instance._ProcessStep__prepared
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@pytest.mark.parametrize("class_with_config_keys", [["test_str"]], indirect=True)
|
|
240
|
+
def test_modify_config__invalid_key(class_with_config_keys):
|
|
241
|
+
instance = class_with_config_keys[1](io_sources=TEST_IO_SOURCES)
|
|
242
|
+
with pytest.raises(KeyError):
|
|
243
|
+
instance.modify_config_by_kwargs(silly_key="new_value")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_calculate():
|
|
247
|
+
ps = TESTProcessingStep(io_sources=TEST_IO_SOURCES)
|
|
248
|
+
ps.processing_data = ProcessingData()
|
|
249
|
+
_return = ps.calculate()
|
|
250
|
+
assert isinstance(_return, dict)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_calculate__abstract():
|
|
254
|
+
ps = ProcessStep(io_sources=TEST_IO_SOURCES)
|
|
255
|
+
with pytest.raises(NotImplementedError):
|
|
256
|
+
ps.calculate()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_execute(processing_data):
|
|
260
|
+
ps = TESTProcessingStep(io_sources=TEST_IO_SOURCES)
|
|
261
|
+
ps.execute(processing_data)
|
|
262
|
+
assert ps.executed is True
|
|
263
|
+
assert ps._ProcessStep__prepared is True
|
|
264
|
+
assert isinstance(ps.produced_outputs, dict)
|
|
265
|
+
assert isinstance(ps.produced_outputs["dummy_key"], DataBundle)
|
|
266
|
+
assert isinstance(ps.produced_outputs["bundle2"], DataBundle)
|
|
267
|
+
assert "dummy_key" in processing_data
|
|
268
|
+
assert isinstance(processing_data["bundle2"]["key2"], BaseData)
|
|
269
|
+
assert isinstance(processing_data["bundle2"]["new_key"], BaseData)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_call(processing_data):
|
|
273
|
+
ps = TESTProcessingStep(io_sources=TEST_IO_SOURCES)
|
|
274
|
+
ps(processing_data)
|
|
275
|
+
assert ps.executed is True
|
|
276
|
+
assert ps._ProcessStep__prepared is True
|
|
277
|
+
assert isinstance(ps.produced_outputs, dict)
|
|
278
|
+
assert isinstance(ps.produced_outputs["dummy_key"], DataBundle)
|
|
279
|
+
assert isinstance(ps.produced_outputs["bundle2"], DataBundle)
|
|
280
|
+
assert "dummy_key" in processing_data
|
|
281
|
+
assert isinstance(processing_data["bundle2"]["key2"], BaseData)
|
|
282
|
+
assert isinstance(processing_data["bundle2"]["new_key"], BaseData)
|
|
@@ -0,0 +1,188 @@
|
|
|
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__ = "13/12/2025"
|
|
11
|
+
__status__ = "Development" # "Development", "Production"
|
|
12
|
+
# end of header and standard imports
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from modacor.dataclasses.process_step import ProcessStep
|
|
17
|
+
from modacor.dataclasses.process_step_describer import ProcessStepDescriber
|
|
18
|
+
from modacor.dataclasses.processing_data import ProcessingData
|
|
19
|
+
from modacor.debug.pipeline_tracer import PipelineTracer
|
|
20
|
+
from modacor.runner.pipeline import Pipeline
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DummyStep(ProcessStep):
|
|
24
|
+
documentation = ProcessStepDescriber(
|
|
25
|
+
calling_name="Dummy",
|
|
26
|
+
calling_id="dummy.step",
|
|
27
|
+
calling_module_path=Path(__file__),
|
|
28
|
+
calling_version="0",
|
|
29
|
+
required_data_keys=[],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def calculate(self):
|
|
33
|
+
return {} # does not modify ProcessingData
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_tracer_emits_nothing_when_no_changes_and_no_empty_events():
|
|
37
|
+
tracer = PipelineTracer(
|
|
38
|
+
watch={"sample": ["signal"]},
|
|
39
|
+
record_only_on_change=True,
|
|
40
|
+
record_empty_step_events=False, # requires the optional field; if you did not add it, remove this line
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
step = DummyStep(io_sources=None, step_id="A")
|
|
44
|
+
data = ProcessingData()
|
|
45
|
+
|
|
46
|
+
tracer.after_step(step, data)
|
|
47
|
+
assert tracer.events == []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_tracer_can_emit_empty_step_events_when_enabled():
|
|
51
|
+
tracer = PipelineTracer(
|
|
52
|
+
watch={"sample": ["signal"]},
|
|
53
|
+
record_only_on_change=True,
|
|
54
|
+
record_empty_step_events=True, # requires the optional field
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
step = DummyStep(io_sources=None, step_id="A")
|
|
58
|
+
data = ProcessingData()
|
|
59
|
+
|
|
60
|
+
tracer.after_step(step, data)
|
|
61
|
+
assert len(tracer.events) == 1
|
|
62
|
+
assert tracer.events[0]["step_id"] == "A"
|
|
63
|
+
assert tracer.events[0]["changed"] == {}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_pipeline_attach_tracer_event_always_attaches_event_even_without_tracer_events():
|
|
67
|
+
step = DummyStep(io_sources=None, step_id="A")
|
|
68
|
+
pipeline = Pipeline.from_dict({step: []}, name="t")
|
|
69
|
+
|
|
70
|
+
tracer = PipelineTracer(watch={"sample": ["signal"]})
|
|
71
|
+
# No tracer.after_step() call -> tracer.events empty
|
|
72
|
+
|
|
73
|
+
ev = pipeline.attach_tracer_event(step, tracer)
|
|
74
|
+
|
|
75
|
+
assert ev.step_id == "A"
|
|
76
|
+
assert ev.module == "DummyStep"
|
|
77
|
+
assert ev.datasets == {} # no tracer payload
|
|
78
|
+
assert "A" in pipeline.trace_events
|
|
79
|
+
assert pipeline.trace_events["A"][-1] is ev
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_pipeline_attach_tracer_event_includes_datasets_when_tracer_has_matching_event():
|
|
83
|
+
step = DummyStep(io_sources=None, step_id="A")
|
|
84
|
+
pipeline = Pipeline.from_dict({step: []}, name="t")
|
|
85
|
+
|
|
86
|
+
tracer = PipelineTracer(
|
|
87
|
+
watch={"sample": ["signal"]},
|
|
88
|
+
record_only_on_change=True,
|
|
89
|
+
record_empty_step_events=True, # ensure tracer produces an event even if empty
|
|
90
|
+
)
|
|
91
|
+
data = ProcessingData()
|
|
92
|
+
|
|
93
|
+
tracer.after_step(step, data)
|
|
94
|
+
ev = pipeline.attach_tracer_event(step, tracer)
|
|
95
|
+
|
|
96
|
+
# Event exists; datasets may still be empty because watched target doesn't exist in ProcessingData
|
|
97
|
+
assert ev.step_id == "A"
|
|
98
|
+
assert isinstance(ev.datasets, dict)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_pipeline_attach_tracer_event_can_embed_rendered_trace_block():
|
|
102
|
+
step = DummyStep(io_sources=None, step_id="A")
|
|
103
|
+
pipeline = Pipeline.from_dict({step: []}, name="t")
|
|
104
|
+
|
|
105
|
+
tracer = PipelineTracer(
|
|
106
|
+
watch={"sample": ["signal"]},
|
|
107
|
+
record_only_on_change=False,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Ensure the tracer has at least one event to render:
|
|
111
|
+
tracer.after_step(step, ProcessingData())
|
|
112
|
+
assert tracer.events # sanity
|
|
113
|
+
|
|
114
|
+
ev = pipeline.attach_tracer_event(
|
|
115
|
+
step,
|
|
116
|
+
tracer,
|
|
117
|
+
include_rendered_trace=True,
|
|
118
|
+
include_rendered_config=True,
|
|
119
|
+
rendered_format="text/plain",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
assert isinstance(ev.messages, list)
|
|
123
|
+
trace_msgs = [m for m in ev.messages if m.get("kind") in {"rendered_trace", "rendered_trace_error"}]
|
|
124
|
+
assert trace_msgs, "Expected a rendered_trace (or error) block"
|
|
125
|
+
m = trace_msgs[0]
|
|
126
|
+
assert "format" in m and "content" in m
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_attach_tracer_event_renders_step_local_block():
|
|
130
|
+
step = DummyStep(io_sources=None, step_id="A")
|
|
131
|
+
pipeline = Pipeline.from_dict({step: []}, name="t")
|
|
132
|
+
|
|
133
|
+
tracer = PipelineTracer(
|
|
134
|
+
watch={"sample": ["signal"]},
|
|
135
|
+
record_only_on_change=False, # ensure an event is recorded even if nothing changes
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
tracer.after_step(step, ProcessingData())
|
|
139
|
+
ev = pipeline.attach_tracer_event(
|
|
140
|
+
step,
|
|
141
|
+
tracer,
|
|
142
|
+
include_rendered_trace=True,
|
|
143
|
+
include_rendered_config=True,
|
|
144
|
+
rendered_format="text/plain",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
trace_msgs = [m for m in ev.messages if m.get("kind") in {"rendered_trace", "rendered_trace_error"}]
|
|
148
|
+
assert trace_msgs, "Expected a rendered_trace (or error) block"
|
|
149
|
+
m = trace_msgs[0]
|
|
150
|
+
|
|
151
|
+
if m.get("kind") == "rendered_trace":
|
|
152
|
+
assert "Step A" in m.get("content", "")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_attach_tracer_event_embeds_rendered_trace_and_config():
|
|
156
|
+
step = DummyStep(io_sources=None, step_id="A")
|
|
157
|
+
pipeline = Pipeline.from_dict({step: []}, name="t")
|
|
158
|
+
|
|
159
|
+
tracer = PipelineTracer(watch={"sample": ["signal"]}, record_only_on_change=False)
|
|
160
|
+
tracer.after_step(step, ProcessingData())
|
|
161
|
+
|
|
162
|
+
ev = pipeline.attach_tracer_event(
|
|
163
|
+
step,
|
|
164
|
+
tracer,
|
|
165
|
+
include_rendered_trace=True,
|
|
166
|
+
include_rendered_config=True,
|
|
167
|
+
rendered_format="text/plain",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
kinds = {m.get("kind") for m in ev.messages}
|
|
171
|
+
assert "rendered_config" in kinds
|
|
172
|
+
assert "rendered_trace" in kinds # since record_only_on_change=False guarantees a step event exists
|
|
173
|
+
|
|
174
|
+
# step-local sanity: rendered trace header contains step id
|
|
175
|
+
trace_blocks = [m for m in ev.messages if m.get("kind") == "rendered_trace"]
|
|
176
|
+
if trace_blocks:
|
|
177
|
+
assert "Step A" in trace_blocks[0].get("content", "")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_attach_tracer_event_copies_duration():
|
|
181
|
+
step = DummyStep(io_sources=None, step_id="A")
|
|
182
|
+
pipeline = Pipeline.from_dict({step: []}, name="t")
|
|
183
|
+
|
|
184
|
+
tracer = PipelineTracer(watch={"sample": ["signal"]}, record_only_on_change=False)
|
|
185
|
+
tracer.after_step(step, ProcessingData(), duration_s=0.0123)
|
|
186
|
+
|
|
187
|
+
ev = pipeline.attach_tracer_event(step, tracer)
|
|
188
|
+
assert ev.duration_s == 0.0123
|
|
File without changes
|