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,238 @@
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__ = ["Anja Hörmann", "Brian R. Pauw"] # add names to the list as appropriate
9
+ __copyright__ = "Copyright 2025, The MoDaCor team"
10
+ __date__ = "18/06/2025"
11
+ __status__ = "Development" # "Development", "Production"
12
+ # end of header and standard imports
13
+
14
+ import tempfile
15
+ import unittest
16
+ from os import unlink
17
+
18
+ import h5py
19
+ import numpy as np
20
+ import pytest
21
+
22
+ from modacor import ureg
23
+
24
+ from ...dataclasses.basedata import BaseData
25
+ from ...dataclasses.databundle import DataBundle
26
+ from ...dataclasses.process_step import ProcessStep
27
+ from ...dataclasses.processing_data import ProcessingData
28
+ from ...io.hdf.hdf_source import HDFSource
29
+ from ...io.io_sources import IoSources
30
+ from ...io.yaml.yaml_source import YAMLSource
31
+ from ...modules.base_modules.poisson_uncertainties import PoissonUncertainties
32
+ from ...runner.pipeline import Pipeline
33
+
34
+ TEST_IO_SOURCES = IoSources()
35
+ TEST_DATA = ProcessingData()
36
+ TEST_DATA["data"] = DataBundle()
37
+
38
+
39
+ @pytest.fixture
40
+ def flat_data():
41
+ data = ProcessingData()
42
+ data["bundle1"] = DataBundle()
43
+ data["bundle2"] = DataBundle()
44
+ data["bundle1"]["signal"] = BaseData(signal=np.arange(50), units=ureg.Unit("count"))
45
+ data["bundle2"]["signal"] = BaseData(signal=np.ones((10, 10)), units=ureg.Unit("count"))
46
+ return data
47
+
48
+
49
+ class DummyProcessStep(ProcessStep):
50
+ def calculate(self):
51
+ return {"test": DataBundle()}
52
+
53
+
54
+ def test_processstep_pipeline(flat_data):
55
+ "tests execution of a linear processstep pipeline (not actually doing anything)"
56
+ steps = [DummyProcessStep(io_sources=TEST_IO_SOURCES, step_id=i) for i in range(3)]
57
+ graph = {steps[i]: {steps[i + 1]} for i in range(len(steps) - 1)}
58
+
59
+ pipeline = Pipeline(graph=graph)
60
+ pipeline.prepare()
61
+ sequence = []
62
+ while pipeline.is_active():
63
+ for node in pipeline.get_ready():
64
+ node.processing_data = flat_data
65
+ sequence.append(node)
66
+ node.execute(flat_data)
67
+ pipeline.done(node)
68
+ assert pipeline._nfinished == len(steps)
69
+
70
+
71
+ def test_actual_processstep(flat_data):
72
+ "test running the PoissonUncertainties Process step"
73
+ step = PoissonUncertainties(io_sources=TEST_IO_SOURCES)
74
+ # we need to supply a list of values here
75
+ step.modify_config_by_kwargs(with_processing_keys=["bundle2"])
76
+ graph = {step: {}}
77
+
78
+ pipeline = Pipeline(graph=graph)
79
+ pipeline.prepare()
80
+ while pipeline.is_active():
81
+ for node in pipeline.get_ready():
82
+ node.processing_data = flat_data
83
+ node.execute(flat_data)
84
+ pipeline.done(node)
85
+ assert node.produced_outputs["bundle2"]["signal"].variances["Poisson"].mean().astype(int) == 1
86
+
87
+
88
+ class TestRealisticPipeline(unittest.TestCase):
89
+ """Tests using yaml files as input for both data and pipeline.
90
+
91
+ the yaml loader needs an actual path to load, can't take a string.
92
+
93
+ """
94
+
95
+ def setUp(self):
96
+ # setup yaml static input file
97
+ self.temp_file_handle = tempfile.NamedTemporaryFile(delete=False, delete_on_close=False)
98
+ self.temp_file_path = self.temp_file_handle.name
99
+ self.temp_file_handle.close()
100
+ with open(self.temp_file_path, "w") as yaml_file:
101
+ yaml_file.write(
102
+ """
103
+ instrument:
104
+ name: "SAXSess I"
105
+ type: "X-ray scattering"
106
+ manufacturer: "Anton Paar"
107
+ model: "SAXSess"
108
+ wavelength:
109
+ value: 0.1542
110
+ units: "nm"
111
+ uncertainty: 0.0005
112
+ detector:
113
+ name: "Mythen2"
114
+ type: "1D strip detector"
115
+ manufacturer: "Dectris"
116
+ n_pixels: 1280
117
+ darkcurrent:
118
+ value: 1e-5
119
+ units: "counts/second"
120
+ uncertainty: 0.1e-5
121
+ """
122
+ )
123
+ # setup two small hdf5 files for sample and background
124
+ self.temp_dataset_shape = (10, 2)
125
+ self.temp_time_handle = "frame_exposure_time"
126
+ self.temp_hdf_file_sample = tempfile.NamedTemporaryFile(delete=False, delete_on_close=False)
127
+ self.temp_hdf_path_sample = self.temp_hdf_file_sample.name
128
+ self.temp_hdf_file_sample.close()
129
+ self.temp_hdf_file_background = tempfile.NamedTemporaryFile(delete=False, delete_on_close=False)
130
+ self.temp_hdf_path_background = self.temp_hdf_file_background.name
131
+ self.temp_hdf_file_background.close()
132
+
133
+ self.temp_data = {
134
+ "sample": 10 * np.ones(self.temp_dataset_shape),
135
+ "sample_background": np.ones(self.temp_dataset_shape),
136
+ }
137
+ self.temp_hdf_paths = {"sample": self.temp_hdf_path_sample, "sample_background": self.temp_hdf_path_background}
138
+ for key, path in self.temp_hdf_paths.items():
139
+ with h5py.File(path, "w") as hdf_file:
140
+ data = hdf_file.create_dataset("data", data=self.temp_data[key], dtype="float64", compression="gzip")
141
+ data.attrs["units"] = "counts"
142
+ detector = hdf_file.create_group("detector")
143
+ time = detector.create_dataset(self.temp_time_handle, data=10.0)
144
+ time.attrs["units"] = "s"
145
+ time.attrs["uncertainties"] = f"{self.temp_time_handle}_uncertainty"
146
+ time_uncertainty = detector.create_dataset(f"{self.temp_time_handle}_uncertainty", data=0.1)
147
+ time_uncertainty.attrs["units"] = "s"
148
+
149
+ self.yaml_semirealistic_linear_pipeline = """
150
+ name: freestanding_solid
151
+ steps:
152
+ 1:
153
+ name: poisson
154
+ module: PoissonUncertainties
155
+ requires_steps: []
156
+ configuration:
157
+ with_processing_keys:
158
+ - sample
159
+ - sample_background
160
+ 2:
161
+ name: normalize_by_time
162
+ module: Divide
163
+ requires_steps: [1]
164
+ configuration:
165
+ divisor_source: sample::detector/frame_exposure_time
166
+ divisor_uncertainties_sources:
167
+ propagate_to_all: sample::detector/frame_exposure_time_uncertainty
168
+ divisor_units_source: sample::detector/frame_exposure_time@units
169
+ with_processing_keys:
170
+ - sample
171
+ - sample_background
172
+ 3:
173
+ name: subtract_dark_current
174
+ module: Subtract
175
+ requires_steps: [2]
176
+ configuration:
177
+ subtrahend_source: yaml::detector/darkcurrent/value
178
+ subtrahend_uncertainties_sources:
179
+ propagate_to_all: yaml::detector/darkcurrent/uncertainty
180
+ subtrahend_units_source: yaml::detector/darkcurrent/units
181
+ with_processing_keys:
182
+ - sample
183
+ - sample_background
184
+ bg:
185
+ name: subtract_background
186
+ module: SubtractDatabundles
187
+ requires_steps: [3]
188
+ configuration:
189
+ with_processing_keys:
190
+ - sample
191
+ - sample_background
192
+ """
193
+
194
+ def tearDown(self):
195
+ unlink(self.temp_file_path)
196
+ unlink(self.temp_hdf_path_sample)
197
+ unlink(self.temp_hdf_path_background)
198
+
199
+ def test_semirealistic_pipeline(self):
200
+ metadata_source = YAMLSource(source_reference="yaml", resource_location=self.temp_file_path)
201
+ sources = IoSources()
202
+ sources.register_source(source=metadata_source)
203
+ sources.register_source(HDFSource(source_reference="sample", resource_location=self.temp_hdf_path_sample))
204
+ sources.register_source(
205
+ HDFSource(source_reference="sample_background", resource_location=self.temp_hdf_path_background)
206
+ )
207
+
208
+ source = "sample"
209
+ _ = [
210
+ print(f"{source}::{key} with shape {val}") # noqa: E231
211
+ for key, val in sources.get_source(source)._file_datasets_shapes.items()
212
+ ]
213
+
214
+ processingdata = ProcessingData()
215
+ for key in ["sample", "sample_background"]:
216
+ processingdata[key] = DataBundle()
217
+ processingdata[key]["signal"] = BaseData(
218
+ sources.get_data(f"{key}::data"), # noqa: E231
219
+ units=sources.get_data_attributes(f"{key}::data")["units"], # noqa: E231
220
+ )
221
+
222
+ pipeline = Pipeline.from_yaml(self.yaml_semirealistic_linear_pipeline)
223
+ pipeline.prepare()
224
+ sequence = []
225
+ while pipeline.is_active():
226
+ for node in pipeline.get_ready():
227
+ node.processing_data = processingdata
228
+ node.io_sources = sources
229
+ sequence.append(node)
230
+ node.execute(processingdata)
231
+ pipeline.done(node)
232
+
233
+ assert np.isclose(np.mean(processingdata["sample"]["signal"].signal), 0.9)
234
+ # rough estimate for poisson error
235
+ expected_error = np.sqrt((np.sqrt(10) / 10.0) ** 2 + (np.sqrt(1) / 10.0) ** 2)
236
+ assert np.isclose(
237
+ np.mean(processingdata["sample"]["signal"].uncertainties["Poisson"]), expected_error, atol=2e-4
238
+ )
@@ -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"
File without changes
@@ -0,0 +1,156 @@
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__ = "12/12/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
+ # Adjust this import to match your actual module layout:
20
+ # here we assume: modacor/io/csv/csv_source.py
21
+ from modacor.io.csv.csv_source import CSVSource
22
+
23
+
24
+ def write_text_file(path: Path, content: str) -> None:
25
+ path.write_text(content, encoding="utf-8")
26
+
27
+
28
+ def test_csvsource_genfromtxt_names_true(tmp_path):
29
+ """
30
+ CSVSource should work with np.genfromtxt(names=True) where the first row
31
+ contains column names.
32
+ """
33
+ csv_content = "q,I,I_sigma\n1.0,2.0,0.1\n3.0,4.0,0.2\n"
34
+ file_path = tmp_path / "data_names_true.csv"
35
+ write_text_file(file_path, csv_content)
36
+
37
+ src = CSVSource(
38
+ source_reference="test_genfromtxt_names_true",
39
+ resource_location=file_path,
40
+ iosource_method_kwargs={"delimiter": ",", "names": True},
41
+ method=np.genfromtxt,
42
+ )
43
+
44
+ # dtype.names should be populated
45
+ assert src._data_cache.dtype.names == ("q", "I", "I_sigma")
46
+
47
+ # Data access
48
+ q = src.get_data("q")
49
+ I = src.get_data("I") # noqa: E741
50
+ I_sigma = src.get_data("I_sigma")
51
+
52
+ assert np.allclose(q, [1.0, 3.0])
53
+ assert np.allclose(I, [2.0, 4.0])
54
+ assert np.allclose(I_sigma, [0.1, 0.2])
55
+
56
+ # Shape / dtype helpers
57
+ assert src.get_data_shape("q") == (2,)
58
+ assert src.get_data_dtype("q") == src._data_cache["q"].dtype
59
+
60
+
61
+ def test_csvsource_genfromtxt_explicit_names(tmp_path):
62
+ """
63
+ CSVSource should work with np.genfromtxt(names=[...]) on data without a header row.
64
+ """
65
+ csv_content = "1.0,2.0,0.1\n3.0,4.0,0.2\n"
66
+ file_path = tmp_path / "data_explicit_names.csv"
67
+ write_text_file(file_path, csv_content)
68
+
69
+ src = CSVSource(
70
+ source_reference="test_genfromtxt_explicit_names",
71
+ resource_location=file_path,
72
+ iosource_method_kwargs={
73
+ "delimiter": ",",
74
+ "names": ["q", "I", "I_sigma"],
75
+ },
76
+ method=np.genfromtxt,
77
+ )
78
+
79
+ assert src._data_cache.dtype.names == ("q", "I", "I_sigma")
80
+
81
+ q = src.get_data("q")
82
+ I = src.get_data("I") # noqa: E741
83
+ I_sigma = src.get_data("I_sigma")
84
+
85
+ assert np.allclose(q, [1.0, 3.0])
86
+ assert np.allclose(I, [2.0, 4.0])
87
+ assert np.allclose(I_sigma, [0.1, 0.2])
88
+
89
+
90
+ def test_csvsource_loadtxt_structured_dtype(tmp_path):
91
+ """
92
+ CSVSource should support np.loadtxt when a structured dtype with field names
93
+ is provided.
94
+ """
95
+ csv_content = "1.0 2.0 0.1\n3.0 4.0 0.2\n"
96
+ file_path = tmp_path / "data_loadtxt_structured.csv"
97
+ write_text_file(file_path, csv_content)
98
+
99
+ structured_dtype = [("q", float), ("I", float), ("I_sigma", float)]
100
+
101
+ src = CSVSource(
102
+ source_reference="test_loadtxt_structured",
103
+ resource_location=file_path,
104
+ iosource_method_kwargs={
105
+ "delimiter": " ",
106
+ "dtype": structured_dtype,
107
+ },
108
+ method=np.loadtxt,
109
+ )
110
+
111
+ assert src._data_cache.dtype.names == ("q", "I", "I_sigma")
112
+
113
+ q = src.get_data("q")
114
+ I = src.get_data("I") # noqa: E741
115
+ I_sigma = src.get_data("I_sigma")
116
+
117
+ assert np.allclose(q, [1.0, 3.0])
118
+ assert np.allclose(I, [2.0, 4.0])
119
+ assert np.allclose(I_sigma, [0.1, 0.2])
120
+
121
+
122
+ def test_csvsource_raises_if_no_dtype_names(tmp_path):
123
+ """
124
+ CSVSource should raise a ValueError if the resulting array has no dtype.names,
125
+ e.g. when using np.loadtxt with a plain dtype.
126
+ """
127
+ csv_content = "1.0,2.0,3.0\n4.0,5.0,6.0\n"
128
+ file_path = tmp_path / "data_no_names.csv"
129
+ write_text_file(file_path, csv_content)
130
+
131
+ with pytest.raises(ValueError, match="dtype.names is None"):
132
+ CSVSource(
133
+ source_reference="test_no_names",
134
+ resource_location=file_path,
135
+ iosource_method_kwargs={"delimiter": ","}, # no structured dtype, no names
136
+ method=np.loadtxt,
137
+ )
138
+
139
+
140
+ def test_csvsource_get_data_invalid_key_raises(tmp_path):
141
+ """
142
+ Requesting a non-existent data_key should raise a KeyError with a helpful message.
143
+ """
144
+ csv_content = "q,I\n1.0,2.0\n3.0,4.0\n"
145
+ file_path = tmp_path / "data_invalid_key.csv"
146
+ write_text_file(file_path, csv_content)
147
+
148
+ src = CSVSource(
149
+ source_reference="test_invalid_key",
150
+ resource_location=file_path,
151
+ iosource_method_kwargs={"delimiter": ",", "names": True},
152
+ method=np.genfromtxt,
153
+ )
154
+
155
+ with pytest.raises(KeyError, match="Data key 'nonexistent' not found"):
156
+ src.get_data("nonexistent")
@@ -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,92 @@
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
+ import tempfile
15
+ import unittest
16
+ from os import unlink
17
+ from pathlib import Path
18
+
19
+ import h5py
20
+ import numpy as np
21
+
22
+ from ....io.hdf.hdf_source import HDFSource
23
+
24
+
25
+ class TestHDFSource(unittest.TestCase):
26
+ """Testing class for modacor/io/hdf/hdf_source.py"""
27
+
28
+ def setUp(self):
29
+ self.temp_file_handle = tempfile.NamedTemporaryFile(delete=False, delete_on_close=False)
30
+ self.temp_file_path = self.temp_file_handle.name
31
+ self.temp_file_handle.close()
32
+ self.temp_dataset_name = "dataset"
33
+ self.temp_dataset_shape = (10, 2)
34
+ with h5py.File(self.temp_file_path, "w") as hdf_file:
35
+ hdf_file.create_dataset(
36
+ self.temp_dataset_name, data=np.zeros(self.temp_dataset_shape), dtype="float64", compression="gzip"
37
+ )
38
+
39
+ self.test_hdf_source = HDFSource(source_reference="Test Data", resource_location=self.temp_file_path)
40
+
41
+ def tearDown(self):
42
+ self.test_hdf_source = None
43
+ self.test_file_path = None
44
+ self.test_dataset_name = None
45
+ self.test_dataset_shape = None
46
+ unlink(self.temp_file_path)
47
+
48
+ def test_open_file(self):
49
+ self.test_hdf_source._file_path = Path(self.temp_file_path)
50
+ self.test_hdf_source._preload()
51
+ self.assertEqual(Path(self.temp_file_path), self.test_hdf_source._file_path)
52
+ # self.assertEqual(self.temp_dataset_name, list(self.test_hdf_source._data_cache.keys())[0])
53
+ self.assertEqual(self.temp_dataset_shape, self.test_hdf_source._file_datasets_shapes[self.temp_dataset_name])
54
+
55
+ def test_get_data(self):
56
+ self.test_hdf_source._file_path = Path(self.temp_file_path)
57
+ self.test_hdf_source._preload()
58
+ data_array = self.test_hdf_source.get_data(self.temp_dataset_name)
59
+ self.assertTrue(isinstance(data_array, np.ndarray))
60
+ self.assertEqual(self.temp_dataset_shape, data_array.shape)
61
+
62
+ def test_get_data_with_slice(self):
63
+ self.test_hdf_source._file_path = Path(self.temp_file_path)
64
+ self.test_hdf_source._preload()
65
+ data_array = self.test_hdf_source.get_data(self.temp_dataset_name, load_slice=(slice(0, 5), slice(None)))
66
+ self.assertTrue(isinstance(data_array, np.ndarray))
67
+ self.assertEqual((5, 2), data_array.shape)
68
+
69
+ def test_get_data_shape(self):
70
+ self.test_hdf_source._file_path = Path(self.temp_file_path)
71
+ self.test_hdf_source._preload()
72
+ data_shape = self.test_hdf_source.get_data_shape(self.temp_dataset_name)
73
+ self.assertEqual(self.temp_dataset_shape, data_shape)
74
+
75
+ def test_get_data_dtype(self):
76
+ self.test_hdf_source._file_path = Path(self.temp_file_path)
77
+ self.test_hdf_source._preload()
78
+ data_dtype = self.test_hdf_source.get_data_dtype(self.temp_dataset_name)
79
+ self.assertEqual(np.dtype("float64"), data_dtype)
80
+
81
+ def test_get_static_metadata(self):
82
+ self.test_hdf_source._file_path = Path(self.temp_file_path)
83
+ self.test_hdf_source._preload()
84
+ static_metadata = self.test_hdf_source.get_static_metadata(self.temp_dataset_name)
85
+ self.assertTrue(isinstance(static_metadata, np.ndarray))
86
+ self.assertEqual(self.temp_dataset_shape, static_metadata.shape)
87
+
88
+ def test_get_data_attributes(self):
89
+ self.test_hdf_source._file_path = Path(self.temp_file_path)
90
+ self.test_hdf_source._preload()
91
+ data_attributes = self.test_hdf_source.get_data_attributes(self.temp_dataset_name)
92
+ self.assertEqual({}, data_attributes) # No attributes set, should return empty dict
@@ -0,0 +1,119 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+ # /usr/bin/env python3
3
+ # -*- coding: utf-8 -*-
4
+
5
+ from __future__ import annotations
6
+
7
+ __coding__ = "utf-8"
8
+ __authors__ = ["Malte Storm"] # 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 numpy as np
15
+ import pytest
16
+
17
+ from modacor.io import IoSources
18
+ from modacor.io.io_source import IoSource
19
+
20
+ data_shape = (20, 20)
21
+
22
+
23
+ class OnesSource(IoSource):
24
+ type_reference = "ones_source"
25
+
26
+ def __attrs_post_init__(self):
27
+ self.configuration["shape"] = data_shape
28
+
29
+ def get_data(self, index, key):
30
+ return np.ones(self.configuration["shape"])
31
+
32
+ def get_metadata(self, key):
33
+ return key
34
+
35
+
36
+ class IndexSource(IoSource):
37
+ type_reference = "index_source"
38
+
39
+ def __attrs_post_init__(self):
40
+ self.configuration["shape"] = data_shape
41
+
42
+ def get_data(self, index, key):
43
+ return index * np.ones(self.configuration["shape"])
44
+
45
+ def get_metadata(self, key):
46
+ return key
47
+
48
+
49
+ @pytest.fixture
50
+ def io_sources():
51
+ return IoSources()
52
+
53
+
54
+ def configure_test_sources(io_sources):
55
+ _source_30x10 = OnesSource()
56
+ _source_30x10.configuration["shape"] = (30, 10)
57
+ io_sources.register_source(source_reference="ones_30x10", source=_source_30x10)
58
+
59
+ _source_20x20 = OnesSource()
60
+ _source_20x20.configuration["shape"] = (20, 20)
61
+ io_sources.register_source(source_reference="ones_20x20", source=_source_20x20)
62
+
63
+ _index_source_25x25 = IndexSource()
64
+ _index_source_25x25.configuration["shape"] = (25, 25)
65
+ io_sources.register_source(source_reference="index_25x25", source=_index_source_25x25)
66
+ return io_sources
67
+
68
+
69
+ @pytest.mark.parametrize("ref", [42, ["a", "ref2"]])
70
+ def test_register_source__wrong_ref_type(io_sources, ref):
71
+ with pytest.raises(TypeError):
72
+ io_sources.register_source(source_reference=ref, source=OnesSource())
73
+
74
+
75
+ @pytest.mark.parametrize("source", [IoSource, object()])
76
+ def test_register_source__wrong_source_type(io_sources, source):
77
+ with pytest.raises(TypeError):
78
+ io_sources.register_source(source_reference="source", source=source)
79
+
80
+
81
+ def test_register_source__duplicate_ref(io_sources):
82
+ io_sources.register_source(source_reference="source", source=OnesSource())
83
+ with pytest.raises(ValueError):
84
+ io_sources.register_source(source_reference="source", source=OnesSource())
85
+
86
+
87
+ def test_register_source__valid(io_sources):
88
+ _source = OnesSource()
89
+ io_sources.register_source(source_reference="ones", source=_source)
90
+ assert "ones" in io_sources.defined_sources
91
+ assert io_sources.defined_sources["ones"] == _source
92
+
93
+
94
+ def test_get_source__invalid(io_sources):
95
+ with pytest.raises(KeyError):
96
+ io_sources.get_source("invalid_source")
97
+
98
+
99
+ def test_get_source__valid(io_sources):
100
+ io_sources = configure_test_sources(io_sources)
101
+ for _key in io_sources.defined_sources:
102
+ source = io_sources.get_source(_key)
103
+ assert isinstance(source, IoSource)
104
+
105
+
106
+ def test_split_data_reference__invalid(io_sources):
107
+ with pytest.raises(ValueError):
108
+ io_sources.split_data_reference("invalid_reference")
109
+
110
+
111
+ @pytest.mark.parametrize("ref", ["test::ref", "test::ref::extra", "test::/entry/data", "test::/entry/data::extra"])
112
+ def test_split_data_reference__valid(io_sources, ref):
113
+ source_ref, data_key = io_sources.split_data_reference(ref)
114
+ assert source_ref == "test"
115
+ assert data_key == ref.lstrip("test::")
116
+
117
+
118
+ if __name__ == "__main__":
119
+ pytest.main()
@@ -0,0 +1,12 @@
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