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,120 @@
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 2026, The MoDaCor team"
10
+ __date__ = "20/01/2026"
11
+ __status__ = "Development" # "Development", "Production"
12
+ # end of header and standard imports
13
+
14
+ import numpy as np
15
+ import pytest
16
+
17
+ from modacor.io.tiled.tiled_source import TiledSource
18
+
19
+
20
+ class _DummyStructure:
21
+ def __init__(self, shape, dtype):
22
+ self.shape = tuple(shape)
23
+ self.dtype = dtype
24
+
25
+
26
+ class _DummyLeaf:
27
+ def __init__(self, data: np.ndarray, metadata: dict[str, object] | None = None):
28
+ self._data = np.asarray(data)
29
+ self.metadata = metadata or {}
30
+
31
+ def read(self, slice=None):
32
+ if slice is None:
33
+ return self._data
34
+ return self._data[slice]
35
+
36
+ @property
37
+ def shape(self):
38
+ return self._data.shape
39
+
40
+ @property
41
+ def dtype(self):
42
+ return self._data.dtype
43
+
44
+ def structure(self):
45
+ return _DummyStructure(self._data.shape, self._data.dtype)
46
+
47
+ @property
48
+ def attrs(self):
49
+ return self.metadata.get("attrs", {})
50
+
51
+
52
+ class _DummyNode:
53
+ def __init__(self, children: dict[str, object], metadata: dict[str, object] | None = None):
54
+ self._children = children
55
+ self.metadata = metadata or {}
56
+
57
+ def __getitem__(self, item: str):
58
+ return self._children[item]
59
+
60
+ @property
61
+ def attrs(self):
62
+ return self.metadata.get("attrs", {})
63
+
64
+
65
+ @pytest.fixture
66
+ def dummy_source() -> TiledSource:
67
+ root = _DummyNode(
68
+ {
69
+ "entry": _DummyNode(
70
+ {
71
+ "data": _DummyLeaf(
72
+ data=np.arange(6).reshape(2, 3),
73
+ metadata={"attrs": {"units": "counts"}},
74
+ ),
75
+ "scalar": _DummyLeaf(np.array(42)),
76
+ },
77
+ metadata={"attrs": {"title": "Example"}},
78
+ )
79
+ }
80
+ )
81
+
82
+ return TiledSource(
83
+ source_reference="dummy",
84
+ root_node=root,
85
+ iosource_method_kwargs={"base_item_path": "entry"},
86
+ )
87
+
88
+
89
+ def test_tiled_source_reads_array(dummy_source: TiledSource):
90
+ data = dummy_source.get_data("data")
91
+ np.testing.assert_array_equal(data, np.arange(6).reshape(2, 3))
92
+
93
+ cached = dummy_source.get_data("data")
94
+ np.testing.assert_array_equal(cached, data)
95
+
96
+
97
+ def test_tiled_source_slicing(dummy_source: TiledSource):
98
+ sliced = dummy_source.get_data("data", load_slice=np.s_[1, :])
99
+ np.testing.assert_array_equal(sliced, np.array([3, 4, 5]))
100
+
101
+
102
+ def test_tiled_source_shape_dtype(dummy_source: TiledSource):
103
+ assert dummy_source.get_data_shape("data") == (2, 3)
104
+ assert dummy_source.get_data_dtype("data") == np.dtype(int)
105
+
106
+
107
+ def test_tiled_source_attributes(dummy_source: TiledSource):
108
+ attrs = dummy_source.get_data_attributes("data")
109
+ assert attrs == {"units": "counts"}
110
+
111
+ assert dummy_source.get_static_metadata("data@units") == "counts"
112
+ # For metadata without explicit attribute name, the full metadata mapping is returned
113
+ metadata = dummy_source.get_static_metadata("data")
114
+ assert metadata == {"attrs": {"units": "counts"}}
115
+
116
+
117
+ def test_tiled_source_resolves_base_metadata(dummy_source: TiledSource):
118
+ assert dummy_source.get_static_metadata("") == {}
119
+ assert dummy_source.get_static_metadata("scalar") == {}
120
+ np.testing.assert_array_equal(dummy_source.get_data("scalar"), np.array(42))
@@ -0,0 +1,27 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+ # Copyright 2025 MoDaCor Authors
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without modification,
5
+ # are permitted provided that the following conditions are met:
6
+ # 1. Redistributions of source code must retain the above copyright notice, this
7
+ # list of conditions and the following disclaimer.
8
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # 3. Neither the name of the copyright holder nor the names of its contributors
12
+ # may be used to endorse or promote products derived from this software without
13
+ # specific prior written permission.
14
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
15
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
18
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21
+ # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
+
25
+ __license__ = "BSD-3-Clause"
26
+ __copyright__ = "Copyright 2025 MoDaCor Authors"
27
+ __status__ = "Alpha"
@@ -0,0 +1,26 @@
1
+ --- # Datafile with examples of static metadata for MoDaCor's static IoSource
2
+ instrument:
3
+ name: "MOUSE"
4
+ type: "X-ray scattering"
5
+ manufacturer: "In-house modified Xenocs"
6
+ model: "Xeuss 2.0"
7
+ serial_number: "1"
8
+ probe_properties:
9
+ wavelength:
10
+ value: 0.1542
11
+ unit: "nm"
12
+ uncertainty: 0.0002
13
+ geometry:
14
+ sample_position:
15
+ x:
16
+ value: 0.0
17
+ unit: "m"
18
+ uncertainty: 0.000
19
+ y:
20
+ value: 0.0
21
+ unit: "m"
22
+ uncertainty: 0.000
23
+ z:
24
+ value: 0.0
25
+ unit: "m"
26
+ uncertainty: 0.000
@@ -0,0 +1,47 @@
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"]
9
+ __copyright__ = "Copyright 2025, The MoDaCor team"
10
+ __date__ = "06/06/2025"
11
+ __status__ = "Development" # "Development", "Production"
12
+ # end of header and standard imports
13
+
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+ import pytest
18
+
19
+ from ....io.yaml.yaml_source import YAMLSource
20
+
21
+ filepath = Path(__file__).parent / "static_data_example.yaml"
22
+
23
+
24
+ def test_yaml_source_initialization():
25
+ """
26
+ Test the initialization of the YAMLSource class.
27
+ """
28
+ source = YAMLSource(source_reference="defaults", resource_location=filepath)
29
+ source._preload()
30
+ assert isinstance(source._yaml_data, dict)
31
+ assert isinstance(source._data_cache, dict)
32
+
33
+
34
+ def test_yaml_source_get_value():
35
+ source = YAMLSource(source_reference="defaults", resource_location=filepath)
36
+ source._preload()
37
+ # at this point, data_cache should be empty:
38
+ assert source._data_cache == {}
39
+ v = source.get_data("probe_properties/wavelength/value")
40
+ assert isinstance(v, np.ndarray)
41
+ # and now we should have the value in the cache:
42
+ assert "probe_properties/wavelength/value" in source._data_cache
43
+ # this should raise a valueerror as the string cannot be converted to a float array:
44
+ with pytest.raises(ValueError):
45
+ source.get_data("probe_properties/wavelength/unit")
46
+ # but this works:
47
+ assert source.get_static_metadata("probe_properties/wavelength/unit") == "nm"
@@ -0,0 +1,27 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+ # Copyright 2025 MoDaCor Authors
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without modification,
5
+ # are permitted provided that the following conditions are met:
6
+ # 1. Redistributions of source code must retain the above copyright notice, this
7
+ # list of conditions and the following disclaimer.
8
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # 3. Neither the name of the copyright holder nor the names of its contributors
12
+ # may be used to endorse or promote products derived from this software without
13
+ # specific prior written permission.
14
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
15
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
18
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21
+ # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
+
25
+ __license__ = "BSD-3-Clause"
26
+ __copyright__ = "Copyright 2025 MoDaCor Authors"
27
+ __status__ = "Alpha"
@@ -0,0 +1,27 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+ # Copyright 2025 MoDaCor Authors
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without modification,
5
+ # are permitted provided that the following conditions are met:
6
+ # 1. Redistributions of source code must retain the above copyright notice, this
7
+ # list of conditions and the following disclaimer.
8
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # 3. Neither the name of the copyright holder nor the names of its contributors
12
+ # may be used to endorse or promote products derived from this software without
13
+ # specific prior written permission.
14
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
15
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
18
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21
+ # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
+
25
+ __license__ = "BSD-3-Clause"
26
+ __copyright__ = "Copyright 2025 MoDaCor Authors"
27
+ __status__ = "Alpha"
@@ -0,0 +1,219 @@
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__ = "03/12/2025"
11
+ __status__ = "Development" # "Development", "Production"
12
+ # end of header and standard imports
13
+
14
+ from typing import Any
15
+
16
+ import numpy as np
17
+ import pytest
18
+ from attrs import define, field
19
+
20
+ from modacor import ureg
21
+ from modacor.dataclasses.basedata import BaseData
22
+ from modacor.dataclasses.databundle import DataBundle
23
+ from modacor.dataclasses.processing_data import ProcessingData
24
+ from modacor.io.io_source import IoSource
25
+ from modacor.io.io_sources import IoSources
26
+ from modacor.modules.base_modules.append_processing_data import AppendProcessingData
27
+
28
+
29
+ @define(kw_only=True)
30
+ class MemoryIoSource(IoSource):
31
+ """
32
+ Minimal in-memory IoSource for tests.
33
+
34
+ - `data_key` is the part AFTER '<source_ref>::' (IoSources does that split).
35
+ - get_data supports slicing via numpy indexing.
36
+ - get_static_metadata returns whatever was stored.
37
+ """
38
+
39
+ data: dict[str, np.ndarray] = field(factory=dict)
40
+ metadata: dict[str, Any] = field(factory=dict)
41
+
42
+ def get_data(self, data_key: str, load_slice=...) -> np.ndarray:
43
+ arr = self.data[data_key]
44
+ return arr[load_slice]
45
+
46
+ def get_data_shape(self, data_key: str) -> tuple[int, ...]:
47
+ return tuple(self.data[data_key].shape) if data_key in self.data else ()
48
+
49
+ def get_data_dtype(self, data_key: str):
50
+ return self.data[data_key].dtype if data_key in self.data else None
51
+
52
+ def get_data_attributes(self, data_key: str) -> dict[str, Any]:
53
+ # Not needed in these tests
54
+ return {}
55
+
56
+ def get_static_metadata(self, data_key: str) -> Any:
57
+ return self.metadata[data_key]
58
+
59
+
60
+ @pytest.fixture
61
+ def signal_array():
62
+ return np.arange(6, dtype=float).reshape(2, 3)
63
+
64
+
65
+ @pytest.fixture
66
+ def io_sources(signal_array) -> IoSources:
67
+ """
68
+ Build a real IoSources with one registered MemoryIoSource under source_ref='sample'.
69
+ """
70
+ sources = IoSources()
71
+
72
+ src = MemoryIoSource(
73
+ source_reference="sample",
74
+ data={
75
+ "entry/instrument/detector/data": signal_array,
76
+ "entry/instrument/detector/sigma": np.ones_like(signal_array),
77
+ },
78
+ metadata={
79
+ "config/rank": 1,
80
+ "entry/instrument/detector/data@units": "dimensionless",
81
+ },
82
+ )
83
+
84
+ sources.register_source(src)
85
+ return sources
86
+
87
+
88
+ def _make_step(io_sources: IoSources, processing_data: ProcessingData | None = None) -> AppendProcessingData:
89
+ if processing_data is None:
90
+ processing_data = ProcessingData()
91
+ step = AppendProcessingData(io_sources=io_sources, processing_data=processing_data)
92
+ return step
93
+
94
+
95
+ # --------------------------------------------------------------------------- #
96
+ # 1. Basic creation: new DataBundle with default output key "signal"
97
+ # --------------------------------------------------------------------------- #
98
+ def test_append_processing_data_creates_new_bundle(io_sources, signal_array):
99
+ step = _make_step(io_sources)
100
+
101
+ step.modify_config_by_dict(
102
+ {
103
+ "processing_key": "sample",
104
+ "signal_location": "sample::entry/instrument/detector/data",
105
+ "rank_of_data": 2,
106
+ "units_location": None,
107
+ "units_override": None,
108
+ "uncertainties_sources": {},
109
+ }
110
+ )
111
+
112
+ output = step.calculate()
113
+
114
+ assert list(output.keys()) == ["sample"]
115
+ bundle = output["sample"]
116
+ assert isinstance(bundle, DataBundle)
117
+
118
+ assert "sample" in step.processing_data
119
+ assert step.processing_data["sample"] is bundle
120
+
121
+ assert "signal" in bundle
122
+ bd = bundle["signal"]
123
+ assert isinstance(bd, BaseData)
124
+ np.testing.assert_array_equal(bd.signal, signal_array)
125
+
126
+ assert bd.units == ureg.dimensionless
127
+ assert bd.rank_of_data == 2
128
+ assert bundle.default_plot == "signal"
129
+
130
+
131
+ # --------------------------------------------------------------------------- #
132
+ # 2. Updating an existing DataBundle (reusing processing_key)
133
+ # --------------------------------------------------------------------------- #
134
+ def test_append_processing_data_updates_existing_bundle(io_sources, signal_array):
135
+ processing_data = ProcessingData()
136
+
137
+ existing_bundle = DataBundle()
138
+ existing_bd = BaseData(signal=np.ones_like(signal_array), units=ureg.dimensionless)
139
+ existing_bundle["existing"] = existing_bd
140
+ existing_bundle.default_plot = "existing"
141
+ processing_data["sample"] = existing_bundle
142
+
143
+ step = _make_step(io_sources, processing_data=processing_data)
144
+
145
+ step.modify_config_by_dict(
146
+ {
147
+ "processing_key": "sample",
148
+ "signal_location": "sample::entry/instrument/detector/data",
149
+ "rank_of_data": 1,
150
+ "databundle_output_key": "I",
151
+ "units_location": None,
152
+ "units_override": None,
153
+ "uncertainties_sources": {},
154
+ }
155
+ )
156
+
157
+ output = step.calculate()
158
+
159
+ assert list(output.keys()) == ["sample"]
160
+ bundle = output["sample"]
161
+ assert bundle is existing_bundle
162
+
163
+ assert "existing" in bundle
164
+ assert bundle["existing"] is existing_bd
165
+
166
+ assert "I" in bundle
167
+ bd_I = bundle["I"]
168
+ np.testing.assert_array_equal(bd_I.signal, signal_array)
169
+ assert bd_I.rank_of_data == 1
170
+
171
+ assert bundle.default_plot == "existing"
172
+
173
+
174
+ # --------------------------------------------------------------------------- #
175
+ # 3. rank_of_data resolved from IoSources metadata (string reference)
176
+ # --------------------------------------------------------------------------- #
177
+ def test_append_processing_data_rank_from_metadata(io_sources):
178
+ step = _make_step(io_sources)
179
+
180
+ step.modify_config_by_dict(
181
+ {
182
+ "processing_key": "sample",
183
+ "signal_location": "sample::entry/instrument/detector/data",
184
+ "rank_of_data": "sample::config/rank",
185
+ "units_location": None,
186
+ "units_override": None,
187
+ "uncertainties_sources": {},
188
+ }
189
+ )
190
+
191
+ output = step.calculate()
192
+ bd = output["sample"]["signal"]
193
+ assert bd.rank_of_data == 1
194
+
195
+
196
+ # --------------------------------------------------------------------------- #
197
+ # 4. uncertainties_sources wiring
198
+ # --------------------------------------------------------------------------- #
199
+ def test_append_processing_data_adds_uncertainties(io_sources, signal_array):
200
+ step = _make_step(io_sources)
201
+
202
+ step.modify_config_by_dict(
203
+ {
204
+ "processing_key": "sample",
205
+ "signal_location": "sample::entry/instrument/detector/data",
206
+ "rank_of_data": 2,
207
+ "units_location": None,
208
+ "units_override": None,
209
+ "uncertainties_sources": {
210
+ "sigma": "sample::entry/instrument/detector/sigma",
211
+ },
212
+ }
213
+ )
214
+
215
+ output = step.calculate()
216
+ bd = output["sample"]["signal"]
217
+
218
+ assert "sigma" in bd.uncertainties
219
+ np.testing.assert_array_equal(bd.uncertainties["sigma"], np.ones_like(signal_array))
@@ -0,0 +1,76 @@
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"]
9
+ __copyright__ = "Copyright 2026, The MoDaCor team"
10
+ __date__ = "09/01/2026"
11
+ __status__ = "Development" # "Development", "Production"
12
+ # end of header and standard imports
13
+
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+
18
+ from modacor.io.csv.csv_sink import CSVSink
19
+ from modacor.io.io_sinks import IoSinks
20
+ from modacor.modules.base_modules.append_sink import AppendSink
21
+
22
+
23
+ @pytest.fixture
24
+ def io_sinks():
25
+ return IoSinks()
26
+
27
+
28
+ def _make_step(io_sinks: IoSinks) -> AppendSink:
29
+ step = AppendSink()
30
+ step.io_sinks = io_sinks
31
+ return step
32
+
33
+
34
+ def test_append_sink_registers_csvsink(tmp_path: Path, io_sinks: IoSinks):
35
+ out_file = tmp_path / "export.csv"
36
+
37
+ step = _make_step(io_sinks)
38
+ step.modify_config_by_dict(
39
+ {
40
+ "sink_identifier": ["export_csv"],
41
+ "sink_location": [str(out_file)],
42
+ "iosink_module": "modacor.io.csv.csv_sink.CSVSink",
43
+ "iosink_method_kwargs": {"delimiter": ","},
44
+ }
45
+ )
46
+
47
+ output = step.calculate()
48
+ assert output == {}
49
+
50
+ assert "export_csv" in io_sinks.defined_sinks
51
+ sink = io_sinks.defined_sinks["export_csv"]
52
+ assert isinstance(sink, CSVSink)
53
+ assert sink.resource_location == out_file
54
+
55
+
56
+ def test_append_sink_does_not_overwrite_existing_sink(tmp_path: Path, io_sinks: IoSinks):
57
+ out_file = tmp_path / "export.csv"
58
+
59
+ step = _make_step(io_sinks)
60
+ step.modify_config_by_dict(
61
+ {
62
+ "sink_identifier": ["export_csv"],
63
+ "sink_location": [str(out_file)],
64
+ "iosink_module": "modacor.io.csv.csv_sink.CSVSink",
65
+ "iosink_method_kwargs": {"delimiter": ","},
66
+ }
67
+ )
68
+
69
+ step.calculate()
70
+ first = io_sinks.defined_sinks["export_csv"]
71
+
72
+ # Run again: AppendSink should skip registering if id already exists
73
+ step.calculate()
74
+ second = io_sinks.defined_sinks["export_csv"]
75
+
76
+ assert first is second