ctao-calibpipe 0.1.0rc9__py3-none-any.whl → 0.2.0rc1__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.
Potentially problematic release.
This version of ctao-calibpipe might be problematic. Click here for more details.
- calibpipe/_version.py +2 -2
- calibpipe/core/common_metadata_containers.py +3 -0
- calibpipe/database/adapter/adapter.py +1 -1
- calibpipe/database/adapter/database_containers/__init__.py +2 -0
- calibpipe/database/adapter/database_containers/common_metadata.py +2 -0
- calibpipe/database/adapter/database_containers/throughput.py +30 -0
- calibpipe/database/interfaces/table_handler.py +79 -97
- calibpipe/telescope/throughput/containers.py +59 -0
- calibpipe/tests/unittests/array/test_cross_calibration.py +417 -0
- calibpipe/tests/unittests/database/test_table_handler.py +95 -0
- calibpipe/tests/unittests/telescope/camera/test_calculate_camcalib_coefficients.py +347 -0
- calibpipe/tests/unittests/telescope/camera/test_produce_camcalib_test_data.py +42 -0
- calibpipe/tests/unittests/telescope/throughput/test_muon_throughput_calibrator.py +189 -0
- calibpipe/tools/camcalib_test_data.py +361 -0
- calibpipe/tools/camera_calibrator.py +558 -0
- calibpipe/tools/muon_throughput_calculator.py +239 -0
- calibpipe/tools/telescope_cross_calibration_calculator.py +721 -0
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/METADATA +3 -2
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/RECORD +24 -14
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/WHEEL +1 -1
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/entry_points.txt +4 -0
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/licenses/AUTHORS.md +0 -0
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/licenses/LICENSE +0 -0
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import MagicMock
|
|
5
|
+
|
|
6
|
+
import astropy.units as u
|
|
7
|
+
import pytest
|
|
8
|
+
from astropy.table import Table
|
|
9
|
+
from calibpipe.tools.telescope_cross_calibration_calculator import (
|
|
10
|
+
CalculateCrossCalibration,
|
|
11
|
+
RelativeThroughputFitter,
|
|
12
|
+
)
|
|
13
|
+
from ctapipe.instrument import SubarrayDescription
|
|
14
|
+
from ctapipe.io import read_table
|
|
15
|
+
from traitlets.config import Config
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("calibpipe.application")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestTelescopeCrossCalibration:
|
|
21
|
+
@pytest.fixture(scope="class")
|
|
22
|
+
def test_calibrator(self):
|
|
23
|
+
data_path = (
|
|
24
|
+
Path(__file__).parent / "../../data/array/cross_calibration_test_dl2.h5"
|
|
25
|
+
)
|
|
26
|
+
test_config = {
|
|
27
|
+
"CalculateCrossCalibration": {
|
|
28
|
+
"input_url": str(data_path),
|
|
29
|
+
"event_filters": {
|
|
30
|
+
"min_gammaness": 0.5,
|
|
31
|
+
},
|
|
32
|
+
"reconstruction_algorithm": "RandomForest",
|
|
33
|
+
"RelativeThroughputFitter": {
|
|
34
|
+
"throughput_normalization": [
|
|
35
|
+
["type", "LST*", 1.0],
|
|
36
|
+
["type", "MST*", 1.0],
|
|
37
|
+
["type", "SST*", 1.0],
|
|
38
|
+
],
|
|
39
|
+
"reference_telescopes": [
|
|
40
|
+
["type", "LST*", 1],
|
|
41
|
+
["type", "MST*", 5],
|
|
42
|
+
["type", "SST*", 37],
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
"PairFinder": {
|
|
46
|
+
"max_impact_distance": [
|
|
47
|
+
["type", "LST*", 125.0],
|
|
48
|
+
["type", "MST*", 125.0],
|
|
49
|
+
["type", "SST*", 225.0],
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
tool = CalculateCrossCalibration(config=Config(test_config))
|
|
55
|
+
|
|
56
|
+
return tool
|
|
57
|
+
|
|
58
|
+
@pytest.fixture()
|
|
59
|
+
def test_fitter_config(self):
|
|
60
|
+
fitter_config = {
|
|
61
|
+
"RelativeThroughputFitter": {
|
|
62
|
+
"throughput_normalization": [
|
|
63
|
+
["type", "LST*", 1.0],
|
|
64
|
+
["type", "MST*", 1.0],
|
|
65
|
+
["type", "SST*", 1.0],
|
|
66
|
+
],
|
|
67
|
+
"reference_telescopes": [
|
|
68
|
+
["type", "LST*", 1],
|
|
69
|
+
["type", "MST*", 5],
|
|
70
|
+
["type", "SST*", 37],
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
return Config(fitter_config)
|
|
75
|
+
|
|
76
|
+
@pytest.fixture()
|
|
77
|
+
def mock_minuit(self):
|
|
78
|
+
m = MagicMock()
|
|
79
|
+
m.values = {"x0": 0.74, "x1": 0.75, "x2": 0.76}
|
|
80
|
+
m.errors = {"x0": 1e-6, "x1": 2e-6, "x2": 3e-6}
|
|
81
|
+
return m
|
|
82
|
+
|
|
83
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
84
|
+
def test_create_telescope_pairs(self, test_calibrator):
|
|
85
|
+
test_calibrator.setup()
|
|
86
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
87
|
+
assert "MST" in telescope_pairs
|
|
88
|
+
assert len(telescope_pairs["MST"]) > 0
|
|
89
|
+
assert "SST" in telescope_pairs
|
|
90
|
+
assert len(telescope_pairs["SST"]) > 0
|
|
91
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs(by_tel_type=False)
|
|
92
|
+
assert "ALL" in telescope_pairs
|
|
93
|
+
assert len(telescope_pairs["ALL"]) > 0
|
|
94
|
+
|
|
95
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
96
|
+
def test_distance(self, test_calibrator):
|
|
97
|
+
# if the requested maximum telescope distance is too small, no tel pair should be returned
|
|
98
|
+
updated_pair_finder_config = {
|
|
99
|
+
"CalculateCrossCalibration": {
|
|
100
|
+
"PairFinder": {
|
|
101
|
+
"max_impact_distance": [
|
|
102
|
+
["type", "LST*", 10.0],
|
|
103
|
+
["type", "MST*", 10.0],
|
|
104
|
+
["type", "SST*", 10.0],
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
test_calibrator.update_config(Config(updated_pair_finder_config))
|
|
111
|
+
test_calibrator.setup()
|
|
112
|
+
|
|
113
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
114
|
+
assert all(
|
|
115
|
+
isinstance(value, set) and len(value) == 0
|
|
116
|
+
for value in telescope_pairs.values()
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
120
|
+
def test_no_reverse_pairs(self, test_calibrator):
|
|
121
|
+
# Ensure the reverse pair (j, i) is not present
|
|
122
|
+
test_calibrator.setup()
|
|
123
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
124
|
+
|
|
125
|
+
for telescope_type, pairs in telescope_pairs.items():
|
|
126
|
+
for i, j in pairs:
|
|
127
|
+
assert (
|
|
128
|
+
(j, i) not in pairs
|
|
129
|
+
), f"Reverse pair ({j}, {i}) exists in {telescope_type} pairs"
|
|
130
|
+
|
|
131
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
132
|
+
def test_allowed_telescope_names(self, test_calibrator):
|
|
133
|
+
# Ensure all keys in the dictionary belong to the allowed names
|
|
134
|
+
allowed_telescope_names = {"MST", "SST"}
|
|
135
|
+
test_calibrator.setup()
|
|
136
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
137
|
+
for telescope_name in telescope_pairs.keys():
|
|
138
|
+
assert (
|
|
139
|
+
telescope_name in allowed_telescope_names
|
|
140
|
+
), f"Unexpected telescope type '{telescope_name}' found in the output"
|
|
141
|
+
|
|
142
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
143
|
+
def test_maximum_telescope_pairs(self, test_calibrator):
|
|
144
|
+
updated_pair_finder_config = {
|
|
145
|
+
"CalculateCrossCalibration": {
|
|
146
|
+
"PairFinder": {
|
|
147
|
+
"max_impact_distance": [
|
|
148
|
+
["type", "LST*", 150000.0],
|
|
149
|
+
["type", "MST*", 150000.0],
|
|
150
|
+
["type", "SST*", 150000.0],
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
test_calibrator.update_config(Config(updated_pair_finder_config))
|
|
157
|
+
test_calibrator.setup()
|
|
158
|
+
|
|
159
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
160
|
+
|
|
161
|
+
array = read_table(
|
|
162
|
+
test_calibrator.input_url,
|
|
163
|
+
"configuration/instrument/subarray/layout",
|
|
164
|
+
)
|
|
165
|
+
telescope_types = [str(t).strip() for t in array["type"] if t]
|
|
166
|
+
for tel_type in set(telescope_types):
|
|
167
|
+
tel_type_counts = (array["type"] == tel_type).sum()
|
|
168
|
+
if tel_type_counts > 0:
|
|
169
|
+
assert (
|
|
170
|
+
len(telescope_pairs[tel_type])
|
|
171
|
+
== (tel_type_counts * (tel_type_counts - 1)) // 2
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
175
|
+
def test_merge_tables_success(self, test_calibrator):
|
|
176
|
+
"""Test successful merging of tables for telescope pairs."""
|
|
177
|
+
telescope_pairs = {"SST": [(37, 39)], "MST": [(6, 12)]}
|
|
178
|
+
result = test_calibrator.merge_tables(telescope_pairs)
|
|
179
|
+
|
|
180
|
+
assert (37, 39) in result["SST"]
|
|
181
|
+
assert (6, 12) in result["MST"]
|
|
182
|
+
assert len(result["SST"][(37, 39)]) > 0 # Ensure merged table is not empty
|
|
183
|
+
|
|
184
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
185
|
+
def test_merge_tables_empty_input(self, test_calibrator):
|
|
186
|
+
"""Test merge_tables returns empty dict when given an empty input."""
|
|
187
|
+
telescope_pairs = {}
|
|
188
|
+
result = test_calibrator.merge_tables(telescope_pairs)
|
|
189
|
+
assert result == {}
|
|
190
|
+
|
|
191
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
192
|
+
def test_merge_tables_missing_data(self, test_calibrator):
|
|
193
|
+
"""Test `merge_tables` handles missing telescope data correctly."""
|
|
194
|
+
telescope_pairs = {"SST": [(37, 39), (42, 143)], "MST": [(5, 6), (6, 120000)]}
|
|
195
|
+
result = test_calibrator.merge_tables(telescope_pairs)
|
|
196
|
+
assert set(result["MST"].keys()) == {(5, 6)}
|
|
197
|
+
|
|
198
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
199
|
+
def test_inter_calibration_result_format(self, test_fitter_config):
|
|
200
|
+
data_subsystem = {
|
|
201
|
+
(5, 6): {"mean_asymmetry": 0.1, "mean_uncertainty": 0.01},
|
|
202
|
+
(6, 8): {"mean_asymmetry": -0.05, "mean_uncertainty": 0.02},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
measured_telescopes = set(
|
|
206
|
+
tel_id for (i, j), entry in data_subsystem.items() for tel_id in (i, j)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
data_path = (
|
|
210
|
+
Path(__file__).parent / "../../data/array/cross_calibration_test_dl2.h5"
|
|
211
|
+
)
|
|
212
|
+
subarray = SubarrayDescription.read(data_path)
|
|
213
|
+
|
|
214
|
+
test_fitter = RelativeThroughputFitter(
|
|
215
|
+
subarray=subarray.select_subarray(tel_ids=measured_telescopes),
|
|
216
|
+
config=test_fitter_config,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
result = test_fitter.fit("MST", data_subsystem)
|
|
220
|
+
assert "MST" in result
|
|
221
|
+
assert isinstance(result["MST"], dict)
|
|
222
|
+
assert set(result["MST"].keys()) == {"5", "6", "8"}
|
|
223
|
+
assert all(isinstance(v, tuple) and len(v) == 2 for v in result["MST"].values())
|
|
224
|
+
|
|
225
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
226
|
+
def test_event_selection_with_multiple_filters(self, test_calibrator, monkeypatch):
|
|
227
|
+
test_calibrator.setup()
|
|
228
|
+
test_calibrator.event_filters = {
|
|
229
|
+
"min_gammaness": 0.5,
|
|
230
|
+
"min_energy": u.Quantity(0.0, u.GeV),
|
|
231
|
+
}
|
|
232
|
+
gamma_set = {(1, 100), (2, 200), (3, 300)}
|
|
233
|
+
energy_set = {(1, 100), (3, 300)}
|
|
234
|
+
|
|
235
|
+
monkeypatch.setattr(
|
|
236
|
+
test_calibrator,
|
|
237
|
+
"_apply_min_gammaness",
|
|
238
|
+
lambda tel_id1, tel_id2, threshold: gamma_set,
|
|
239
|
+
)
|
|
240
|
+
monkeypatch.setattr(
|
|
241
|
+
test_calibrator,
|
|
242
|
+
"_apply_min_energy",
|
|
243
|
+
lambda tel_id1, tel_id2, threshold: energy_set,
|
|
244
|
+
)
|
|
245
|
+
merged_table = Table({"obs_id": [1, 2, 3, 4], "event_id": [100, 200, 300, 400]})
|
|
246
|
+
|
|
247
|
+
filtered = test_calibrator.event_selection(merged_table, tel1=1, tel2=3)
|
|
248
|
+
assert len(filtered) == 2
|
|
249
|
+
|
|
250
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
251
|
+
def test_event_selection_with_invalid_filter(self, test_calibrator):
|
|
252
|
+
test_calibrator.setup()
|
|
253
|
+
test_calibrator.event_filters = {
|
|
254
|
+
"non_existent_filter": 4,
|
|
255
|
+
}
|
|
256
|
+
merged_table = Table({"obs_id": [1, 2, 3, 4], "event_id": [100, 200, 300, 400]})
|
|
257
|
+
|
|
258
|
+
with pytest.raises(
|
|
259
|
+
ValueError,
|
|
260
|
+
match="Filter non_existent_filter is not implemented or not recognized.",
|
|
261
|
+
):
|
|
262
|
+
_ = test_calibrator.event_selection(merged_table, tel1=1, tel2=2)
|
|
263
|
+
|
|
264
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
265
|
+
def test_system_cross_calibrator(self, test_calibrator, caplog):
|
|
266
|
+
# Small scale integration test
|
|
267
|
+
caplog.set_level(logging.ERROR, logger="calibpipe.application")
|
|
268
|
+
updated_cc_config = {
|
|
269
|
+
"CalculateCrossCalibration": {
|
|
270
|
+
"event_filters": {
|
|
271
|
+
"min_gammaness": 0.5,
|
|
272
|
+
},
|
|
273
|
+
"RelativeThroughputFitter": {
|
|
274
|
+
"throughput_normalization": [
|
|
275
|
+
["type", "LST*", 1.0],
|
|
276
|
+
["type", "MST*", 1.0],
|
|
277
|
+
["type", "SST*", 1.0],
|
|
278
|
+
],
|
|
279
|
+
"reference_telescopes": [
|
|
280
|
+
["type", "LST*", 1],
|
|
281
|
+
["type", "MST*", 5],
|
|
282
|
+
["type", "SST*", 37],
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
"PairFinder": {
|
|
286
|
+
"max_impact_distance": [
|
|
287
|
+
["type", "LST*", 125.0],
|
|
288
|
+
["type", "MST*", 125.0],
|
|
289
|
+
["type", "SST*", 225.0],
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
test_calibrator.update_config(Config(updated_cc_config))
|
|
295
|
+
test_calibrator.setup()
|
|
296
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
297
|
+
merged_tables = test_calibrator.merge_tables(telescope_pairs)
|
|
298
|
+
energy_asymmetry_results = test_calibrator.calculate_energy_asymmetry(
|
|
299
|
+
merged_tables
|
|
300
|
+
)
|
|
301
|
+
results = {}
|
|
302
|
+
|
|
303
|
+
for subarray_name, subarray_data in energy_asymmetry_results.items():
|
|
304
|
+
measured_telescopes = set(
|
|
305
|
+
tel_id for (i, j), entry in subarray_data.items() for tel_id in (i, j)
|
|
306
|
+
)
|
|
307
|
+
fitter = RelativeThroughputFitter(
|
|
308
|
+
subarray=test_calibrator.subarray.select_subarray(
|
|
309
|
+
tel_ids=measured_telescopes, name=subarray_name
|
|
310
|
+
),
|
|
311
|
+
parent=test_calibrator,
|
|
312
|
+
)
|
|
313
|
+
results.update(fitter.fit(subarray_name, subarray_data))
|
|
314
|
+
|
|
315
|
+
cross_type_pairs = test_calibrator.pair_finder.find_pairs(
|
|
316
|
+
by_tel_type=False, cross_type_only=True
|
|
317
|
+
)
|
|
318
|
+
cross_calibration_results = test_calibrator.compute_cross_type_energy_ratios(
|
|
319
|
+
cross_type_pairs["XTEL"], results
|
|
320
|
+
)
|
|
321
|
+
assert cross_calibration_results[("MST", "SST")][0] == pytest.approx(
|
|
322
|
+
1.0119, rel=1e-2
|
|
323
|
+
)
|
|
324
|
+
assert cross_calibration_results[("MST", "SST")][1] == pytest.approx(
|
|
325
|
+
0.0042591, rel=1e-2
|
|
326
|
+
)
|
|
327
|
+
# Below testing energy asymmetry
|
|
328
|
+
assert energy_asymmetry_results["MST"][(6, 12)][
|
|
329
|
+
"mean_uncertainty"
|
|
330
|
+
] == pytest.approx(1.3613071073770851e-06, rel=1e-4)
|
|
331
|
+
assert energy_asymmetry_results["MST"][(6, 12)][
|
|
332
|
+
"mean_asymmetry"
|
|
333
|
+
] == pytest.approx(-0.0008690296768284202, rel=1e-2)
|
|
334
|
+
# Below testing the fixed telescopes
|
|
335
|
+
assert results["SST"]["37"][0] == pytest.approx(1.0, rel=1e-4)
|
|
336
|
+
assert results["MST"]["5"][0] == pytest.approx(1.0, rel=1e-4)
|
|
337
|
+
|
|
338
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
339
|
+
def test_save_monitoring_data(self, test_calibrator, tmp_path):
|
|
340
|
+
# Prepare input
|
|
341
|
+
class SizeType(Enum):
|
|
342
|
+
MST = "MST"
|
|
343
|
+
SST = "SST"
|
|
344
|
+
LST = "LST"
|
|
345
|
+
|
|
346
|
+
intercalibration_results = {
|
|
347
|
+
SizeType.MST: {
|
|
348
|
+
"6": (1.23, 0.01),
|
|
349
|
+
"8": (0.97, 0.02),
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
cross_calibration_results = {
|
|
353
|
+
(SizeType.MST, SizeType.SST): (1.05, 0.05),
|
|
354
|
+
(SizeType.LST, SizeType.SST): (1.15, 0.05),
|
|
355
|
+
(SizeType.LST, SizeType.MST): (0.95, 0.05),
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# Run
|
|
359
|
+
updated_cc_config = {
|
|
360
|
+
"CalculateCrossCalibration": {
|
|
361
|
+
"event_filters": {
|
|
362
|
+
"get_gamma_like_events": 0.5,
|
|
363
|
+
},
|
|
364
|
+
"output_url": "x_calib_test_dl2.h5",
|
|
365
|
+
"overwrite": True,
|
|
366
|
+
"RelativeThroughputFitter": {
|
|
367
|
+
"throughput_normalization": [
|
|
368
|
+
["type", "LST*", 1.0],
|
|
369
|
+
["type", "MST*", 1.0],
|
|
370
|
+
["type", "SST*", 1.0],
|
|
371
|
+
],
|
|
372
|
+
"reference_telescopes": [
|
|
373
|
+
["type", "LST*", 1],
|
|
374
|
+
["type", "MST*", 5],
|
|
375
|
+
["type", "SST*", 37],
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
"PairFinder": {
|
|
379
|
+
"max_impact_distance": [
|
|
380
|
+
["type", "LST*", 125.0],
|
|
381
|
+
["type", "MST*", 125.0],
|
|
382
|
+
["type", "SST*", 225.0],
|
|
383
|
+
],
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
test_calibrator.update_config(Config(updated_cc_config))
|
|
388
|
+
test_calibrator.setup()
|
|
389
|
+
test_calibrator.save_monitoring_data(
|
|
390
|
+
intercalibration_results, cross_calibration_results
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Validate inter calibration table
|
|
394
|
+
inter_table = read_table(
|
|
395
|
+
test_calibrator.output_url, "/dl2/monitoring/inter_calibration"
|
|
396
|
+
)
|
|
397
|
+
assert len(inter_table) == 2
|
|
398
|
+
assert "tel_id" in inter_table.colnames
|
|
399
|
+
assert "value" in inter_table.colnames
|
|
400
|
+
assert "error" in inter_table.colnames
|
|
401
|
+
|
|
402
|
+
# Validate cross calibration table
|
|
403
|
+
cross_table = read_table(
|
|
404
|
+
test_calibrator.output_url, "/dl2/monitoring/cross_calibration"
|
|
405
|
+
)
|
|
406
|
+
assert len(cross_table) == 3
|
|
407
|
+
assert cross_table["ratio"][0] == pytest.approx(1.05)
|
|
408
|
+
assert cross_table["error"][0] == pytest.approx(0.05)
|
|
409
|
+
assert cross_table["ratio"][1] == pytest.approx(1.15)
|
|
410
|
+
assert cross_table["ratio"][2] == pytest.approx(0.95)
|
|
411
|
+
|
|
412
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
413
|
+
def test_get_equidistant_events(self, test_calibrator):
|
|
414
|
+
test_calibrator.setup()
|
|
415
|
+
set_of_events = test_calibrator._apply_max_distance_asymmetry(42, 143, 0.05)
|
|
416
|
+
assert (4991, 1181519) in set_of_events
|
|
417
|
+
assert (4991, 2196419) not in set_of_events
|
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
# Import the necessary modules and classes for testing
|
|
2
|
+
import datetime
|
|
2
3
|
from pathlib import Path
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
3
5
|
|
|
4
6
|
import astropy.units as u
|
|
5
7
|
import pytest
|
|
6
8
|
import yaml
|
|
9
|
+
from calibpipe.core.common_metadata_containers import (
|
|
10
|
+
ContactReferenceMetadataContainer,
|
|
11
|
+
ProductReferenceMetadataContainer,
|
|
12
|
+
ReferenceMetadataContainer,
|
|
13
|
+
)
|
|
14
|
+
from calibpipe.database.adapter import Adapter
|
|
7
15
|
from calibpipe.database.connections import CalibPipeDatabase
|
|
8
16
|
from calibpipe.database.interfaces import TableHandler
|
|
17
|
+
from calibpipe.telescope.throughput.containers import OpticalThoughtputContainer
|
|
9
18
|
from calibpipe.utils.observatory import (
|
|
10
19
|
Observatory,
|
|
11
20
|
)
|
|
12
21
|
from traitlets.config import Config
|
|
13
22
|
|
|
14
23
|
|
|
24
|
+
@pytest.fixture()
|
|
25
|
+
def mock_connection():
|
|
26
|
+
"""
|
|
27
|
+
Fixture to create a mock database connection.
|
|
28
|
+
"""
|
|
29
|
+
connection = MagicMock(spec=CalibPipeDatabase)
|
|
30
|
+
return connection
|
|
31
|
+
|
|
32
|
+
|
|
15
33
|
# Fixture to provide a database connection
|
|
16
34
|
@pytest.fixture()
|
|
17
35
|
def test_config():
|
|
@@ -64,3 +82,80 @@ class TestTableHandler:
|
|
|
64
82
|
assert qtable is not None
|
|
65
83
|
assert "elevation" in qtable.colnames
|
|
66
84
|
assert qtable["elevation"].unit == u.m
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_upload_data(mock_connection):
|
|
88
|
+
"""
|
|
89
|
+
Test the upload_data function to ensure it correctly uploads data and metadata.
|
|
90
|
+
"""
|
|
91
|
+
# Use OpticalThoughtputContainer as the data container
|
|
92
|
+
data = OpticalThoughtputContainer(
|
|
93
|
+
optical_throughput_coefficient=0.95,
|
|
94
|
+
optical_throughput_coefficient_std=0.02,
|
|
95
|
+
method="muon analysis",
|
|
96
|
+
validity_start=datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc),
|
|
97
|
+
validity_end=datetime.datetime(2025, 12, 31, tzinfo=datetime.timezone.utc),
|
|
98
|
+
obs_id=12345,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Mock metadata containers
|
|
102
|
+
reference_metadata = ReferenceMetadataContainer(
|
|
103
|
+
version_atmospheric_model="1.0",
|
|
104
|
+
version=1,
|
|
105
|
+
ID_optical_throughput=None, # Will be set during the upload
|
|
106
|
+
)
|
|
107
|
+
product_metadata = ProductReferenceMetadataContainer(
|
|
108
|
+
description="Test product",
|
|
109
|
+
creation_time="2025-04-08T12:00:00Z",
|
|
110
|
+
product_id="12345",
|
|
111
|
+
)
|
|
112
|
+
contact_metadata = ContactReferenceMetadataContainer(
|
|
113
|
+
organization="Test Organization",
|
|
114
|
+
name="Test User",
|
|
115
|
+
email="test@example.com",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Combine metadata into a list
|
|
119
|
+
metadata = [reference_metadata, product_metadata, contact_metadata]
|
|
120
|
+
|
|
121
|
+
# Mock database behavior
|
|
122
|
+
mock_connection.execute.return_value.fetchone.return_value = MagicMock(
|
|
123
|
+
_asdict=lambda: {"ID": 1}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Patch the insert_row_in_database method
|
|
127
|
+
with patch(
|
|
128
|
+
"calibpipe.database.interfaces.table_handler.TableHandler.insert_row_in_database",
|
|
129
|
+
) as mock_insert:
|
|
130
|
+
# Call the upload_data function
|
|
131
|
+
TableHandler.upload_data(data, metadata, mock_connection)
|
|
132
|
+
|
|
133
|
+
# Assertions to verify correct behavior
|
|
134
|
+
# Verify that the main data was inserted
|
|
135
|
+
data_table, data_kwargs = Adapter.to_postgres(data)
|
|
136
|
+
|
|
137
|
+
calls = mock_insert.call_args_list
|
|
138
|
+
|
|
139
|
+
# Check that a call was made with the expected table and connection
|
|
140
|
+
assert any(
|
|
141
|
+
call_args[0][0] == data_table
|
|
142
|
+
and call_args[0][2] == mock_connection
|
|
143
|
+
and call_args[0][1]["optical_throughput_coefficient"] == 0.95
|
|
144
|
+
for call_args in calls
|
|
145
|
+
), "Expected call to insert_row_in_database not found."
|
|
146
|
+
|
|
147
|
+
ref_table, ref_kwargs = Adapter.to_postgres(reference_metadata)
|
|
148
|
+
|
|
149
|
+
assert any(
|
|
150
|
+
call_args[0][0] == ref_table
|
|
151
|
+
and call_args[0][2] == mock_connection
|
|
152
|
+
and call_args[0][1]["version_atmospheric_model"] == "1.0"
|
|
153
|
+
for call_args in calls
|
|
154
|
+
), "Expected call to insert_row_in_database not found."
|
|
155
|
+
|
|
156
|
+
for container in metadata[1:]:
|
|
157
|
+
meta_table, meta_kwargs = Adapter.to_postgres(container)
|
|
158
|
+
assert any(
|
|
159
|
+
call_args[0][0] == meta_table and call_args[0][2] == mock_connection
|
|
160
|
+
for call_args in calls
|
|
161
|
+
), "Expected call to insert_row_in_database not found."
|