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,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)