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