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