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,438 @@
|
|
|
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
|
+
__coding__ = "utf-8"
|
|
10
|
+
__authors__ = ["Anja Hörmann", "Brian R. Pauw"] # add names to the list as appropriate
|
|
11
|
+
__copyright__ = "Copyright 2025, The MoDaCor team"
|
|
12
|
+
__date__ = "22/11/2025"
|
|
13
|
+
__status__ = "Development" # "Development", "Production"
|
|
14
|
+
# end of header and standard imports
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
from modacor.dataclasses.process_step import ProcessStep
|
|
21
|
+
from modacor.runner.pipeline import Pipeline
|
|
22
|
+
from modacor.runner.process_step_registry import ProcessStepRegistry
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def linear_pipeline():
|
|
27
|
+
# Simple linear graph: 1 -> 2 -> 3
|
|
28
|
+
return {3: {2, 1}, 2: {1}}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DummyIoSources:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DummyProcessStepDescriber:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DummyProcessStep:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def yaml_one_step():
|
|
45
|
+
# Single-step pipeline, keyed by step_id "div"
|
|
46
|
+
return """
|
|
47
|
+
name: one_step
|
|
48
|
+
steps:
|
|
49
|
+
di:
|
|
50
|
+
module: Divide
|
|
51
|
+
requires_steps: []
|
|
52
|
+
configuration:
|
|
53
|
+
divisor_source: 3
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def yaml_linear_pipeline():
|
|
59
|
+
# Simple 3-step linear pipeline with string step_ids
|
|
60
|
+
return """
|
|
61
|
+
name: simple_pipeline
|
|
62
|
+
steps:
|
|
63
|
+
1:
|
|
64
|
+
module: PoissonUncertainties
|
|
65
|
+
requires_steps: []
|
|
66
|
+
p2:
|
|
67
|
+
module: PoissonUncertainties
|
|
68
|
+
requires_steps: [1]
|
|
69
|
+
mul:
|
|
70
|
+
module: PoissonUncertainties
|
|
71
|
+
requires_steps: [p2]
|
|
72
|
+
configuration:
|
|
73
|
+
multiplier: 3
|
|
74
|
+
signal: sample::signal
|
|
75
|
+
io_sources:
|
|
76
|
+
- sample
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_linear_pipeline(linear_pipeline):
|
|
81
|
+
"tests the sequence is expected for a linear graph"
|
|
82
|
+
pipeline = Pipeline(graph=linear_pipeline)
|
|
83
|
+
pipeline.prepare()
|
|
84
|
+
sequence = []
|
|
85
|
+
while pipeline.is_active():
|
|
86
|
+
for node in pipeline.get_ready():
|
|
87
|
+
sequence.append(node)
|
|
88
|
+
pipeline.done(node)
|
|
89
|
+
assert sequence == [1, 2, 3]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_node_addition(linear_pipeline):
|
|
93
|
+
pipeline = Pipeline.from_dict(linear_pipeline)
|
|
94
|
+
ps = DummyProcessStep()
|
|
95
|
+
pipeline.add(ps, *[1, 2, 3])
|
|
96
|
+
pipeline.prepare()
|
|
97
|
+
sequence = []
|
|
98
|
+
while pipeline.is_active():
|
|
99
|
+
for node in pipeline.get_ready():
|
|
100
|
+
sequence.append(node)
|
|
101
|
+
pipeline.done(node)
|
|
102
|
+
assert sequence == [1, 2, 3, ps]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_branch_addition(linear_pipeline, pipeline_to_add={5: {6}}, at_node=2):
|
|
106
|
+
"""
|
|
107
|
+
add a pipeline as a branch on an existing pipeline, using the inherited add method
|
|
108
|
+
"""
|
|
109
|
+
pipeline_1 = Pipeline(graph=linear_pipeline)
|
|
110
|
+
pipeline_2 = Pipeline(graph=pipeline_to_add)
|
|
111
|
+
pipeline_1.add(at_node, *pipeline_2.static_order())
|
|
112
|
+
assert [*pipeline_1.static_order()] == [1, 6, 5, 2, 3]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_branch_addition_method(linear_pipeline, branch_graph={5: {6}}, branching_node=2):
|
|
116
|
+
pipeline = Pipeline(graph=linear_pipeline)
|
|
117
|
+
branch = Pipeline(graph=branch_graph)
|
|
118
|
+
pipeline.add_incoming_branch(branch, branching_node=2)
|
|
119
|
+
assert [*pipeline.static_order()] == [1, 6, 5, 2, 3]
|
|
120
|
+
assert pipeline.graph == {3: {2, 1}, 2: {1, 5}, 5: {6}}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_diverging_branch_addition(linear_pipeline, branch_graph={5: {6}, 6: set()}, branching_node=2):
|
|
124
|
+
pipeline = Pipeline(graph=linear_pipeline)
|
|
125
|
+
branch = Pipeline(graph=branch_graph)
|
|
126
|
+
pipeline.add_outgoing_branch(branch, branching_node)
|
|
127
|
+
assert [*pipeline.static_order()] == [1, 2, 3, 6, 5]
|
|
128
|
+
assert pipeline.graph == {3: {2, 1}, 2: {1}, 5: {6}, 6: {2}}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_yaml_format(yaml_linear_pipeline):
|
|
132
|
+
yaml_obj = yaml.safe_load(yaml_linear_pipeline)
|
|
133
|
+
assert "steps" in yaml_obj
|
|
134
|
+
# now keyed by step_id, not by human-readable name
|
|
135
|
+
assert 1 in yaml_obj["steps"]
|
|
136
|
+
assert "p2" in yaml_obj["steps"]
|
|
137
|
+
assert "mul" in yaml_obj["steps"]
|
|
138
|
+
assert isinstance(yaml_obj["steps"]["mul"]["configuration"], dict)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_pipeline_from_yaml(yaml_one_step):
|
|
142
|
+
pipeline = Pipeline.from_yaml(yaml_one_step)
|
|
143
|
+
assert pipeline.name == "one_step"
|
|
144
|
+
assert isinstance(pipeline, Pipeline)
|
|
145
|
+
|
|
146
|
+
# One node with no prerequisites
|
|
147
|
+
assert len(pipeline.graph) == 1
|
|
148
|
+
((node, deps),) = pipeline.graph.items()
|
|
149
|
+
assert isinstance(node, ProcessStep)
|
|
150
|
+
assert deps == set()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_pipeline_from_yaml_with_custom_registry():
|
|
154
|
+
"""
|
|
155
|
+
Ensure Pipeline.from_yaml uses the given ProcessStepRegistry
|
|
156
|
+
and instantiates the correct ProcessStep subclass.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
class DummyStep(ProcessStep):
|
|
160
|
+
def __init__(self, *args, **kwargs):
|
|
161
|
+
super().__init__(*args, **kwargs)
|
|
162
|
+
self.executed = False
|
|
163
|
+
|
|
164
|
+
def execute(self, **kwargs):
|
|
165
|
+
self.executed = True
|
|
166
|
+
|
|
167
|
+
ps_registry = ProcessStepRegistry()
|
|
168
|
+
ps_registry.register(DummyStep)
|
|
169
|
+
|
|
170
|
+
yaml_str = """
|
|
171
|
+
name: dummy_pipeline
|
|
172
|
+
steps:
|
|
173
|
+
s1:
|
|
174
|
+
module: DummyStep
|
|
175
|
+
requires_steps: []
|
|
176
|
+
configuration:
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
pipeline = Pipeline.from_yaml(yaml_str, registry=ps_registry)
|
|
180
|
+
assert isinstance(pipeline, Pipeline)
|
|
181
|
+
assert pipeline.name == "dummy_pipeline"
|
|
182
|
+
|
|
183
|
+
# There should be exactly one node in the graph
|
|
184
|
+
assert len(pipeline.graph) == 1
|
|
185
|
+
((node, deps),) = pipeline.graph.items()
|
|
186
|
+
|
|
187
|
+
assert isinstance(node, DummyStep)
|
|
188
|
+
assert deps == set()
|
|
189
|
+
|
|
190
|
+
# Run the pipeline and check that our DummyStep.execute was called
|
|
191
|
+
pipeline.run()
|
|
192
|
+
assert node.executed is True
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_pipeline_from_yaml_unknown_module_raises():
|
|
196
|
+
"""
|
|
197
|
+
If the YAML references a module name that the registry cannot resolve,
|
|
198
|
+
Pipeline.from_yaml should raise a KeyError (propagated from the registry).
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
# Use a registry with no base_package and no registrations,
|
|
202
|
+
# so any lookup will fail with KeyError.
|
|
203
|
+
registry = ProcessStepRegistry()
|
|
204
|
+
|
|
205
|
+
yaml_str = """
|
|
206
|
+
name: bad_pipeline
|
|
207
|
+
steps:
|
|
208
|
+
s1:
|
|
209
|
+
module: NonExistentStep
|
|
210
|
+
requires_steps: []
|
|
211
|
+
configuration:
|
|
212
|
+
some_param: 42
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
with pytest.raises(KeyError):
|
|
216
|
+
Pipeline.from_yaml(yaml_str, registry=registry)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_to_spec_basic():
|
|
220
|
+
"""to_spec should expose nodes and edges consistent with the internal graph."""
|
|
221
|
+
|
|
222
|
+
class DummyDoc:
|
|
223
|
+
calling_name = "Dummy step"
|
|
224
|
+
calling_module_path = Path("dummy/path.py")
|
|
225
|
+
calling_version = "0.1.0"
|
|
226
|
+
|
|
227
|
+
class DummyNode:
|
|
228
|
+
def __init__(self, step_id, config):
|
|
229
|
+
self.step_id = step_id
|
|
230
|
+
self.configuration = config
|
|
231
|
+
self.documentation = DummyDoc
|
|
232
|
+
|
|
233
|
+
# Build a tiny graph: n1 -> n2
|
|
234
|
+
n1 = DummyNode(step_id="1", config={"foo": "bar"})
|
|
235
|
+
n2 = DummyNode(step_id="2", config={"baz": 123})
|
|
236
|
+
|
|
237
|
+
graph = {
|
|
238
|
+
n2: {n1}, # n2 depends on n1
|
|
239
|
+
n1: set(), # n1 has no prerequisites
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
pipeline = Pipeline(graph=graph, name="test_pipe")
|
|
243
|
+
|
|
244
|
+
spec = pipeline.to_spec()
|
|
245
|
+
|
|
246
|
+
# Basic structure
|
|
247
|
+
assert spec["name"] == "test_pipe"
|
|
248
|
+
assert "nodes" in spec and "edges" in spec
|
|
249
|
+
|
|
250
|
+
# Nodes: two nodes with ids "1" and "2"
|
|
251
|
+
node_ids = {n["id"] for n in spec["nodes"]}
|
|
252
|
+
assert node_ids == {"1", "2"}
|
|
253
|
+
|
|
254
|
+
# Each node should carry module name and config
|
|
255
|
+
modules = {n["module"] for n in spec["nodes"]}
|
|
256
|
+
assert modules == {"DummyNode"}
|
|
257
|
+
|
|
258
|
+
# Find n1 and n2 entries
|
|
259
|
+
node_map = {n["id"]: n for n in spec["nodes"]}
|
|
260
|
+
assert node_map["1"]["config"] == {"foo": "bar"}
|
|
261
|
+
assert node_map["2"]["config"] == {"baz": 123}
|
|
262
|
+
|
|
263
|
+
# Edges: exactly one edge 1 -> 2
|
|
264
|
+
edges = {(e["from"], e["to"]) for e in spec["edges"]}
|
|
265
|
+
assert edges == {("1", "2")}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_to_dot_matches_spec():
|
|
269
|
+
"""to_dot should reflect the nodes and edges from to_spec in DOT format."""
|
|
270
|
+
|
|
271
|
+
class DummyDoc:
|
|
272
|
+
calling_name = "Dummy step"
|
|
273
|
+
calling_module_path = Path("dummy/path.py")
|
|
274
|
+
calling_version = "0.1.0"
|
|
275
|
+
|
|
276
|
+
class DummyNode:
|
|
277
|
+
def __init__(self, step_id):
|
|
278
|
+
self.step_id = step_id
|
|
279
|
+
self.configuration = {}
|
|
280
|
+
self.documentation = DummyDoc
|
|
281
|
+
|
|
282
|
+
n1 = DummyNode(step_id="1")
|
|
283
|
+
n2 = DummyNode(step_id="2")
|
|
284
|
+
graph = {n2: {n1}, n1: set()}
|
|
285
|
+
|
|
286
|
+
pipeline = Pipeline(graph=graph, name="dot_test")
|
|
287
|
+
|
|
288
|
+
dot_src = pipeline.to_dot()
|
|
289
|
+
|
|
290
|
+
# Basic header
|
|
291
|
+
assert 'digraph "dot_test"' in dot_src
|
|
292
|
+
|
|
293
|
+
# Node labels should include "<id>: <module name>"
|
|
294
|
+
assert '"1" [label="1: DummyNode"];' in dot_src
|
|
295
|
+
assert '"2" [label="2: DummyNode"];' in dot_src
|
|
296
|
+
|
|
297
|
+
# Edge representation
|
|
298
|
+
assert '"1" -> "2";' in dot_src
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_to_mermaid_flowchart():
|
|
302
|
+
"""to_mermaid should emit a valid-looking Mermaid flowchart syntax."""
|
|
303
|
+
|
|
304
|
+
class DummyDoc:
|
|
305
|
+
calling_name = "Dummy step"
|
|
306
|
+
calling_module_path = Path("dummy/path.py")
|
|
307
|
+
calling_version = "0.1.0"
|
|
308
|
+
|
|
309
|
+
class DummyNode:
|
|
310
|
+
def __init__(self, step_id):
|
|
311
|
+
self.step_id = step_id
|
|
312
|
+
self.configuration = {}
|
|
313
|
+
self.documentation = DummyDoc
|
|
314
|
+
|
|
315
|
+
n1 = DummyNode(step_id="1")
|
|
316
|
+
n2 = DummyNode(step_id="2")
|
|
317
|
+
n2.short_title = "custom purpose"
|
|
318
|
+
graph = {n2: {n1}, n1: set()}
|
|
319
|
+
|
|
320
|
+
pipeline = Pipeline(graph=graph, name="mermaid_test")
|
|
321
|
+
|
|
322
|
+
mermaid_src = pipeline.to_mermaid(direction="TB")
|
|
323
|
+
|
|
324
|
+
# Header
|
|
325
|
+
assert mermaid_src.splitlines()[0] == "flowchart TB"
|
|
326
|
+
|
|
327
|
+
# Nodes: 1 and 2 with labels "1: DummyNode" etc.
|
|
328
|
+
assert '1["1: DummyNode"]' in mermaid_src
|
|
329
|
+
assert '2["2: DummyNode<br/>custom purpose"]' in mermaid_src
|
|
330
|
+
|
|
331
|
+
# Edge: 1 --> 2
|
|
332
|
+
assert "1 --> 2" in mermaid_src
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_yaml_spec_roundtrip_with_edit():
|
|
336
|
+
"""
|
|
337
|
+
End-to-end style test:
|
|
338
|
+
|
|
339
|
+
1. Load a pipeline from YAML (using a dummy ProcessStep via a custom registry).
|
|
340
|
+
2. Export to a spec for a web-based graphing tool.
|
|
341
|
+
3. Modify the spec (add a node, tweak a config).
|
|
342
|
+
4. Rebuild the pipeline from the modified spec.
|
|
343
|
+
5. Export back to YAML and assert the structure reflects the edits.
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
class DummyStep(ProcessStep):
|
|
347
|
+
# Extend CONFIG_KEYS so that "a" is a valid configuration key
|
|
348
|
+
CONFIG_KEYS = {
|
|
349
|
+
**ProcessStep.CONFIG_KEYS,
|
|
350
|
+
"a": {
|
|
351
|
+
"type": int,
|
|
352
|
+
"allow_iterable": False,
|
|
353
|
+
"allow_none": False,
|
|
354
|
+
"default": 0,
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
def calculate(self):
|
|
359
|
+
# For this test we don't care about actual computation
|
|
360
|
+
return {}
|
|
361
|
+
|
|
362
|
+
# Registry that only knows about DummyStep (no lazy imports)
|
|
363
|
+
registry = ProcessStepRegistry()
|
|
364
|
+
registry.register(DummyStep)
|
|
365
|
+
|
|
366
|
+
# 1. Original YAML: simple 2-step linear pipeline
|
|
367
|
+
original_yaml = """
|
|
368
|
+
name: roundtrip_pipeline
|
|
369
|
+
steps:
|
|
370
|
+
1:
|
|
371
|
+
module: DummyStep
|
|
372
|
+
requires_steps: []
|
|
373
|
+
configuration:
|
|
374
|
+
a: 1
|
|
375
|
+
2:
|
|
376
|
+
module: DummyStep
|
|
377
|
+
requires_steps: [1]
|
|
378
|
+
configuration:
|
|
379
|
+
a: 2
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
pipeline = Pipeline.from_yaml(original_yaml, registry=registry)
|
|
383
|
+
|
|
384
|
+
# Sanity: initial topology is 1 -> 2
|
|
385
|
+
initial_order = [node.step_id for node in pipeline.static_order()]
|
|
386
|
+
assert initial_order == ["1", "2"]
|
|
387
|
+
|
|
388
|
+
# 2. Export to spec (what the web editor would see)
|
|
389
|
+
spec = pipeline.to_spec()
|
|
390
|
+
|
|
391
|
+
# 3. Modify the spec as if the user edited the graph in a UI:
|
|
392
|
+
# - Add a new node "3" depending on "2"
|
|
393
|
+
# - Change the config of node "2"
|
|
394
|
+
spec["nodes"].append(
|
|
395
|
+
{
|
|
396
|
+
"id": "3",
|
|
397
|
+
"label": "Third step",
|
|
398
|
+
"module": "DummyStep",
|
|
399
|
+
"config": {"a": 3},
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
spec["edges"].append({"from": "2", "to": "3"})
|
|
403
|
+
|
|
404
|
+
for node in spec["nodes"]:
|
|
405
|
+
if node["id"] == "2":
|
|
406
|
+
node["config"]["a"] = 42
|
|
407
|
+
|
|
408
|
+
# 4. Rebuild a pipeline from the modified spec
|
|
409
|
+
modified_pipeline = Pipeline.from_spec(spec, registry=registry)
|
|
410
|
+
modified_order = [node.step_id for node in modified_pipeline.static_order()]
|
|
411
|
+
assert modified_order == ["1", "2", "3"]
|
|
412
|
+
|
|
413
|
+
# 5. Export back to YAML
|
|
414
|
+
modified_yaml = modified_pipeline.to_yaml()
|
|
415
|
+
yaml_obj = yaml.safe_load(modified_yaml)
|
|
416
|
+
|
|
417
|
+
# Basic structure
|
|
418
|
+
assert yaml_obj["name"] == "roundtrip_pipeline"
|
|
419
|
+
assert "steps" in yaml_obj
|
|
420
|
+
|
|
421
|
+
steps = yaml_obj["steps"]
|
|
422
|
+
# Keys are step_ids as strings
|
|
423
|
+
assert set(steps.keys()) == {"1", "2", "3"}
|
|
424
|
+
|
|
425
|
+
# All modules should be DummyStep
|
|
426
|
+
assert steps["1"]["module"] == "DummyStep"
|
|
427
|
+
assert steps["2"]["module"] == "DummyStep"
|
|
428
|
+
assert steps["3"]["module"] == "DummyStep"
|
|
429
|
+
|
|
430
|
+
# Requires_steps reflect the edited graph: 1 -> 2 -> 3
|
|
431
|
+
assert "requires_steps" not in steps["1"] or steps["1"]["requires_steps"] == []
|
|
432
|
+
assert steps["2"]["requires_steps"] == ["1"]
|
|
433
|
+
assert steps["3"]["requires_steps"] == ["2"]
|
|
434
|
+
|
|
435
|
+
# Config values reflect original + edits
|
|
436
|
+
assert steps["1"]["configuration"]["a"] == 1
|
|
437
|
+
assert steps["2"]["configuration"]["a"] == 42 # edited
|
|
438
|
+
assert steps["3"]["configuration"]["a"] == 3
|
|
@@ -0,0 +1,65 @@
|
|
|
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__ = "16/11/2025"
|
|
11
|
+
__status__ = "Development" # "Development", "Production"
|
|
12
|
+
# end of header and standard imports
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import types
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from modacor.dataclasses.process_step import ProcessStep
|
|
20
|
+
from modacor.runner.process_step_registry import ProcessStepRegistry
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_register_and_get_process_step():
|
|
24
|
+
registry = ProcessStepRegistry()
|
|
25
|
+
|
|
26
|
+
class DummyStep(ProcessStep):
|
|
27
|
+
def execute(self, **kwargs):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
registry.register(DummyStep)
|
|
31
|
+
cls = registry.get("DummyStep")
|
|
32
|
+
|
|
33
|
+
assert cls is DummyStep
|
|
34
|
+
assert "DummyStep" in registry
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_register_with_custom_name():
|
|
38
|
+
registry = ProcessStepRegistry()
|
|
39
|
+
|
|
40
|
+
class DummyStep(ProcessStep):
|
|
41
|
+
def execute(self, **kwargs):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
registry.register(DummyStep, name="custom_name")
|
|
45
|
+
cls = registry.get("custom_name")
|
|
46
|
+
|
|
47
|
+
assert cls is DummyStep
|
|
48
|
+
assert "custom_name" in registry
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_register_non_process_step_raises():
|
|
52
|
+
registry = ProcessStepRegistry()
|
|
53
|
+
|
|
54
|
+
class NotAStep:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
with pytest.raises(TypeError):
|
|
58
|
+
registry.register(NotAStep)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_get_unknown_without_base_package_raises():
|
|
62
|
+
registry = ProcessStepRegistry()
|
|
63
|
+
|
|
64
|
+
with pytest.raises(KeyError):
|
|
65
|
+
registry.get("DoesNotExistStep")
|
|
@@ -0,0 +1,43 @@
|
|
|
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__ = ["Jérome Kieffer"] # 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
|
+
|
|
15
|
+
"""try to import all sub-modules from the project"""
|
|
16
|
+
|
|
17
|
+
import importlib
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
dirname = os.path.dirname
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_import_all():
|
|
24
|
+
project_dir = dirname(dirname(os.path.abspath(__file__)))
|
|
25
|
+
start = len(project_dir) - len("modacor")
|
|
26
|
+
modules = []
|
|
27
|
+
for path, dirs, files in os.walk(project_dir):
|
|
28
|
+
for f in files:
|
|
29
|
+
if f.endswith(".py") and not f.startswith("__"):
|
|
30
|
+
modules.append(os.path.join(path[start:], f[:-3]))
|
|
31
|
+
cnt = 0
|
|
32
|
+
for i in modules:
|
|
33
|
+
j = i.replace(os.sep, ".")
|
|
34
|
+
try:
|
|
35
|
+
_ = importlib.import_module(j)
|
|
36
|
+
except Exception as err:
|
|
37
|
+
print(f"{type(err).__name__} in {j}: {err}.")
|
|
38
|
+
cnt += 1
|
|
39
|
+
assert cnt == 0, f"{cnt} submodules could not import properly"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
test_import_all()
|
|
@@ -0,0 +1,17 @@
|
|
|
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__ = ["Ingo Breßler"] # 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
|
+
|
|
15
|
+
# dummy test to verify the setup
|
|
16
|
+
def test_main():
|
|
17
|
+
pass
|
|
@@ -0,0 +1,79 @@
|
|
|
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__ = "22/11/2025"
|
|
11
|
+
__status__ = "Development" # "Development", "Production"
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from pint import UnitRegistry
|
|
15
|
+
from pint.errors import DimensionalityError
|
|
16
|
+
|
|
17
|
+
from modacor.units import configure_detector_pixel_units
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_pixel_dimension_is_detector_element_after_configure() -> None:
|
|
21
|
+
ureg = UnitRegistry()
|
|
22
|
+
|
|
23
|
+
# Pint usually defines pixel as a printing/display unit by default.
|
|
24
|
+
# We only assert the post-condition to keep this robust across Pint variants.
|
|
25
|
+
configure_detector_pixel_units(ureg)
|
|
26
|
+
|
|
27
|
+
dim_pixel = (1 * ureg.pixel).dimensionality
|
|
28
|
+
assert "[detector_pixel]" in dim_pixel
|
|
29
|
+
assert "[printing_unit]" not in dim_pixel
|
|
30
|
+
|
|
31
|
+
# Aliases must all resolve to the same dimensionality
|
|
32
|
+
assert (1 * ureg.px).dimensionality == dim_pixel
|
|
33
|
+
assert (1 * ureg.pixels).dimensionality == dim_pixel
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_parsing_common_metadata_spellings() -> None:
|
|
37
|
+
ureg = UnitRegistry()
|
|
38
|
+
configure_detector_pixel_units(ureg)
|
|
39
|
+
|
|
40
|
+
q1 = ureg.Quantity("1 pixel")
|
|
41
|
+
q2 = ureg.Quantity("1 pixels")
|
|
42
|
+
q3 = ureg.Quantity("1 px")
|
|
43
|
+
|
|
44
|
+
assert q1.dimensionality == q2.dimensionality == q3.dimensionality
|
|
45
|
+
assert q1.to("pixel").magnitude == pytest.approx(1.0)
|
|
46
|
+
assert q2.to("pixel").magnitude == pytest.approx(1.0)
|
|
47
|
+
assert q3.to("pixel").magnitude == pytest.approx(1.0)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_mm_per_pixel_is_not_convertible_to_mm() -> None:
|
|
51
|
+
ureg = UnitRegistry()
|
|
52
|
+
configure_detector_pixel_units(ureg)
|
|
53
|
+
|
|
54
|
+
q = 0.172 * ureg.mm / ureg.pixel
|
|
55
|
+
with pytest.raises(DimensionalityError):
|
|
56
|
+
_ = q.to("mm")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_pixel_cancels_when_multiplying_by_pixel_count() -> None:
|
|
60
|
+
ureg = UnitRegistry()
|
|
61
|
+
configure_detector_pixel_units(ureg)
|
|
62
|
+
|
|
63
|
+
pixel_size = 0.172 * ureg.mm / ureg.pixel
|
|
64
|
+
length = pixel_size * (100 * ureg.pixel)
|
|
65
|
+
|
|
66
|
+
assert length.to("mm").magnitude == pytest.approx(17.2)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_area_and_volume_per_pixel_behave_sensibly() -> None:
|
|
70
|
+
ureg = UnitRegistry()
|
|
71
|
+
configure_detector_pixel_units(ureg)
|
|
72
|
+
|
|
73
|
+
area_per_pixel = 0.0296 * ureg.mm**2 / ureg.pixel
|
|
74
|
+
area = area_per_pixel * (10 * ureg.pixel)
|
|
75
|
+
assert area.to("mm^2").magnitude == pytest.approx(0.296)
|
|
76
|
+
|
|
77
|
+
vol_per_pixel = 0.005 * ureg.mm**3 / ureg.pixel
|
|
78
|
+
vol = vol_per_pixel * (12 * ureg.px) # alias
|
|
79
|
+
assert vol.to("mm^3").magnitude == pytest.approx(0.06)
|