ctao-calibpipe 0.3.0rc2__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.
- calibpipe/__init__.py +5 -0
- calibpipe/_dev_version/__init__.py +9 -0
- calibpipe/_version.py +34 -0
- calibpipe/atmosphere/__init__.py +1 -0
- calibpipe/atmosphere/atmosphere_containers.py +109 -0
- calibpipe/atmosphere/meteo_data_handlers.py +485 -0
- calibpipe/atmosphere/models/README.md +14 -0
- calibpipe/atmosphere/models/__init__.py +1 -0
- calibpipe/atmosphere/models/macobac.ecsv +23 -0
- calibpipe/atmosphere/models/reference_MDPs/__init__.py +1 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_intermediate.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_summer.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_winter.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_summer.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_winter.ecsv +8 -0
- calibpipe/atmosphere/models/reference_atmospheres/__init__.py +1 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_intermediate.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_summer.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_winter.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_summer.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_winter.ecsv +73 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/__init__.py +1 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_intermediate.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_summer.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_winter.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_summer.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_winter.ecsv +857 -0
- calibpipe/atmosphere/templates/request_templates/__init__.py +1 -0
- calibpipe/atmosphere/templates/request_templates/copernicus.json +11 -0
- calibpipe/atmosphere/templates/request_templates/gdas.json +12 -0
- calibpipe/core/__init__.py +39 -0
- calibpipe/core/common_metadata_containers.py +198 -0
- calibpipe/core/exceptions.py +87 -0
- calibpipe/database/__init__.py +24 -0
- calibpipe/database/adapter/__init__.py +23 -0
- calibpipe/database/adapter/adapter.py +80 -0
- calibpipe/database/adapter/database_containers/__init__.py +63 -0
- calibpipe/database/adapter/database_containers/atmosphere.py +199 -0
- calibpipe/database/adapter/database_containers/common_metadata.py +150 -0
- calibpipe/database/adapter/database_containers/container_map.py +59 -0
- calibpipe/database/adapter/database_containers/observatory.py +61 -0
- calibpipe/database/adapter/database_containers/table_version_manager.py +39 -0
- calibpipe/database/adapter/database_containers/throughput.py +30 -0
- calibpipe/database/adapter/database_containers/version_control.py +17 -0
- calibpipe/database/connections/__init__.py +28 -0
- calibpipe/database/connections/calibpipe_database.py +60 -0
- calibpipe/database/connections/postgres_utils.py +97 -0
- calibpipe/database/connections/sql_connection.py +103 -0
- calibpipe/database/connections/user_confirmation.py +19 -0
- calibpipe/database/interfaces/__init__.py +71 -0
- calibpipe/database/interfaces/hashable_row_data.py +54 -0
- calibpipe/database/interfaces/queries.py +180 -0
- calibpipe/database/interfaces/sql_column_info.py +67 -0
- calibpipe/database/interfaces/sql_metadata.py +6 -0
- calibpipe/database/interfaces/sql_table_info.py +131 -0
- calibpipe/database/interfaces/table_handler.py +333 -0
- calibpipe/database/interfaces/types.py +96 -0
- calibpipe/telescope/throughput/containers.py +66 -0
- calibpipe/tests/conftest.py +274 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/__init__.py +0 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/contemporary_MDP.ecsv +34 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.csv +852 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.ecsv +23 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/merged_file.ecsv +1082 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_copernicus.ecsv +1082 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_gdas.ecsv +66 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/observatory_configurations.json +71 -0
- calibpipe/tests/data/utils/__init__.py +0 -0
- calibpipe/tests/data/utils/meteo_data_winter_and_summer.ecsv +12992 -0
- calibpipe/tests/test_conftest_data.py +200 -0
- calibpipe/tests/unittests/array/test_cross_calibration.py +412 -0
- calibpipe/tests/unittests/atmosphere/astral_testing.py +107 -0
- calibpipe/tests/unittests/atmosphere/test_meteo_data_handler.py +775 -0
- calibpipe/tests/unittests/atmosphere/test_molecular_atmosphere.py +327 -0
- calibpipe/tests/unittests/database/test_table_handler.py +163 -0
- calibpipe/tests/unittests/database/test_types.py +38 -0
- calibpipe/tests/unittests/telescope/camera/test_calculate_camcalib_coefficients.py +456 -0
- calibpipe/tests/unittests/telescope/camera/test_produce_camcalib_test_data.py +37 -0
- calibpipe/tests/unittests/telescope/throughput/test_muon_throughput_calibrator.py +693 -0
- calibpipe/tests/unittests/test_bootstrap_db.py +79 -0
- calibpipe/tests/unittests/utils/test_observatory.py +309 -0
- calibpipe/tools/atmospheric_base_tool.py +78 -0
- calibpipe/tools/atmospheric_model_db_loader.py +181 -0
- calibpipe/tools/basic_tool_with_db.py +38 -0
- calibpipe/tools/camcalib_test_data.py +374 -0
- calibpipe/tools/camera_calibrator.py +462 -0
- calibpipe/tools/contemporary_mdp_producer.py +87 -0
- calibpipe/tools/init_db.py +37 -0
- calibpipe/tools/macobac_calculator.py +82 -0
- calibpipe/tools/molecular_atmospheric_model_producer.py +197 -0
- calibpipe/tools/muon_throughput_calculator.py +219 -0
- calibpipe/tools/observatory_data_db_loader.py +71 -0
- calibpipe/tools/reference_atmospheric_model_selector.py +201 -0
- calibpipe/tools/telescope_cross_calibration_calculator.py +721 -0
- calibpipe/utils/__init__.py +10 -0
- calibpipe/utils/observatory.py +486 -0
- calibpipe/utils/observatory_containers.py +26 -0
- calibpipe/version.py +24 -0
- ctao_calibpipe-0.3.0rc2.dist-info/METADATA +92 -0
- ctao_calibpipe-0.3.0rc2.dist-info/RECORD +105 -0
- ctao_calibpipe-0.3.0rc2.dist-info/WHEEL +5 -0
- ctao_calibpipe-0.3.0rc2.dist-info/entry_points.txt +12 -0
- ctao_calibpipe-0.3.0rc2.dist-info/licenses/AUTHORS.md +13 -0
- ctao_calibpipe-0.3.0rc2.dist-info/licenses/LICENSE +21 -0
- ctao_calibpipe-0.3.0rc2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test for CalibPipe test data download and caching functionality.
|
|
3
|
+
|
|
4
|
+
This test verifies that all test files can be downloaded and cached properly
|
|
5
|
+
using the conftest.py infrastructure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from calibpipe.tests.conftest import (
|
|
9
|
+
DEFAULT_CALIBPIPE_URL,
|
|
10
|
+
FILE_PATH_MAPPING,
|
|
11
|
+
TEST_FILES,
|
|
12
|
+
)
|
|
13
|
+
from ctapipe.utils.datasets import get_dataset_path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestCalibPipeTestData:
|
|
17
|
+
"""Test class for CalibPipe test data functionality."""
|
|
18
|
+
|
|
19
|
+
def test_download_all_test_files(self):
|
|
20
|
+
"""
|
|
21
|
+
Test that all test files in TEST_FILES can be downloaded and cached.
|
|
22
|
+
|
|
23
|
+
This test will:
|
|
24
|
+
1. Download each file listed in TEST_FILES
|
|
25
|
+
2. Verify the file exists after download
|
|
26
|
+
3. Verify the file has non-zero size
|
|
27
|
+
4. Test that subsequent calls use the cached version
|
|
28
|
+
"""
|
|
29
|
+
downloaded_files = {}
|
|
30
|
+
|
|
31
|
+
for file_path in TEST_FILES:
|
|
32
|
+
print(f"Downloading and testing: {file_path}")
|
|
33
|
+
|
|
34
|
+
# Get the server path from the mapping and download
|
|
35
|
+
server_path = FILE_PATH_MAPPING[file_path]
|
|
36
|
+
cached_file = get_dataset_path(server_path, url=DEFAULT_CALIBPIPE_URL)
|
|
37
|
+
|
|
38
|
+
# Verify the file exists
|
|
39
|
+
assert (
|
|
40
|
+
cached_file.exists()
|
|
41
|
+
), f"Downloaded file does not exist: {cached_file}"
|
|
42
|
+
|
|
43
|
+
# Verify the file has content
|
|
44
|
+
file_size = cached_file.stat().st_size
|
|
45
|
+
assert (
|
|
46
|
+
file_size > 0
|
|
47
|
+
), f"Downloaded file is empty: {cached_file} (size: {file_size})"
|
|
48
|
+
|
|
49
|
+
# Store for further verification
|
|
50
|
+
downloaded_files[file_path] = {"path": cached_file, "size": file_size}
|
|
51
|
+
|
|
52
|
+
print(f" ✓ Downloaded to: {cached_file}")
|
|
53
|
+
print(f" ✓ File size: {file_size:,} bytes")
|
|
54
|
+
|
|
55
|
+
# Test that all files are accessible
|
|
56
|
+
assert len(downloaded_files) == len(
|
|
57
|
+
TEST_FILES
|
|
58
|
+
), f"Expected {len(TEST_FILES)} files, got {len(downloaded_files)}"
|
|
59
|
+
|
|
60
|
+
print(
|
|
61
|
+
f"\n✓ Successfully downloaded and verified {len(downloaded_files)} test files"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Test caching by downloading again and ensuring we get the same paths
|
|
65
|
+
print("\nTesting cache functionality...")
|
|
66
|
+
for file_path in TEST_FILES[:3]: # Test first 3 files for caching
|
|
67
|
+
server_path = FILE_PATH_MAPPING[file_path]
|
|
68
|
+
cached_file_again = get_dataset_path(server_path, url=DEFAULT_CALIBPIPE_URL)
|
|
69
|
+
original_file = downloaded_files[file_path]["path"]
|
|
70
|
+
|
|
71
|
+
assert (
|
|
72
|
+
cached_file_again == original_file
|
|
73
|
+
), f"Cache miss: got different path for {file_path}"
|
|
74
|
+
print(f" ✓ Cache hit for: {file_path}")
|
|
75
|
+
|
|
76
|
+
print("✓ Cache functionality verified")
|
|
77
|
+
|
|
78
|
+
def test_specific_file_types(self):
|
|
79
|
+
"""Test specific file types and their expected characteristics."""
|
|
80
|
+
|
|
81
|
+
# Test HDF5 files
|
|
82
|
+
h5_files = [f for f in TEST_FILES if f.endswith(".h5")]
|
|
83
|
+
for h5_file in h5_files:
|
|
84
|
+
server_path = FILE_PATH_MAPPING[h5_file]
|
|
85
|
+
cached_file = get_dataset_path(server_path, url=DEFAULT_CALIBPIPE_URL)
|
|
86
|
+
|
|
87
|
+
# HDF5 files should have the HDF5 signature
|
|
88
|
+
with open(cached_file, "rb") as f:
|
|
89
|
+
header = f.read(8)
|
|
90
|
+
assert header.startswith(
|
|
91
|
+
b"\x89HDF"
|
|
92
|
+
), f"File {h5_file} does not appear to be a valid HDF5 file"
|
|
93
|
+
|
|
94
|
+
print(f" ✓ Verified HDF5 format: {h5_file}")
|
|
95
|
+
|
|
96
|
+
# Test gzipped files
|
|
97
|
+
gz_files = [f for f in TEST_FILES if f.endswith(".gz")]
|
|
98
|
+
for gz_file in gz_files:
|
|
99
|
+
server_path = FILE_PATH_MAPPING[gz_file]
|
|
100
|
+
cached_file = get_dataset_path(server_path, url=DEFAULT_CALIBPIPE_URL)
|
|
101
|
+
|
|
102
|
+
# Gzipped files should have the gzip magic number
|
|
103
|
+
with open(cached_file, "rb") as f:
|
|
104
|
+
header = f.read(2)
|
|
105
|
+
assert (
|
|
106
|
+
header == b"\x1f\x8b"
|
|
107
|
+
), f"File {gz_file} does not appear to be a valid gzip file"
|
|
108
|
+
|
|
109
|
+
print(f" ✓ Verified gzip format: {gz_file}")
|
|
110
|
+
|
|
111
|
+
def test_file_categories(self):
|
|
112
|
+
"""Test that files are properly categorized by their directory structure."""
|
|
113
|
+
|
|
114
|
+
categories = {"array": [], "telescope/throughput": [], "telescope/camera": []}
|
|
115
|
+
|
|
116
|
+
for file_path in TEST_FILES:
|
|
117
|
+
if file_path.startswith("array/"):
|
|
118
|
+
categories["array"].append(file_path)
|
|
119
|
+
elif file_path.startswith("telescope/throughput/"):
|
|
120
|
+
categories["telescope/throughput"].append(file_path)
|
|
121
|
+
elif file_path.startswith("telescope/camera/"):
|
|
122
|
+
categories["telescope/camera"].append(file_path)
|
|
123
|
+
|
|
124
|
+
# Verify we have files in each category
|
|
125
|
+
assert len(categories["array"]) > 0, "No array test files found"
|
|
126
|
+
assert (
|
|
127
|
+
len(categories["telescope/throughput"]) > 0
|
|
128
|
+
), "No throughput test files found"
|
|
129
|
+
assert len(categories["telescope/camera"]) > 0, "No camera test files found"
|
|
130
|
+
|
|
131
|
+
print(f"✓ Array files: {len(categories['array'])}")
|
|
132
|
+
print(f"✓ Throughput files: {len(categories['telescope/throughput'])}")
|
|
133
|
+
print(f"✓ Camera files: {len(categories['telescope/camera'])}")
|
|
134
|
+
|
|
135
|
+
def test_fixtures_work(
|
|
136
|
+
self,
|
|
137
|
+
cross_calibration_dl2_file,
|
|
138
|
+
lst_muon_table_file,
|
|
139
|
+
flatfield_file,
|
|
140
|
+
pedestal_file,
|
|
141
|
+
):
|
|
142
|
+
"""Test that the pytest fixtures work correctly."""
|
|
143
|
+
|
|
144
|
+
test_fixtures = [
|
|
145
|
+
("cross_calibration_dl2_file", cross_calibration_dl2_file),
|
|
146
|
+
("lst_muon_table_file", lst_muon_table_file),
|
|
147
|
+
("flatfield_file", flatfield_file),
|
|
148
|
+
("pedestal_file", pedestal_file),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
for fixture_name, fixture_path in test_fixtures:
|
|
152
|
+
assert (
|
|
153
|
+
fixture_path.exists()
|
|
154
|
+
), f"Fixture {fixture_name} does not exist: {fixture_path}"
|
|
155
|
+
assert fixture_path.stat().st_size > 0, f"Fixture {fixture_name} is empty"
|
|
156
|
+
print(f" ✓ Fixture {fixture_name}: {fixture_path}")
|
|
157
|
+
|
|
158
|
+
print("✓ All fixtures are working correctly")
|
|
159
|
+
|
|
160
|
+
def test_parametrized_fixture(self, calibpipe_dl1_file):
|
|
161
|
+
"""Test the parametrized fixture for DL1 files."""
|
|
162
|
+
assert (
|
|
163
|
+
calibpipe_dl1_file.exists()
|
|
164
|
+
), f"Parametrized DL1 file does not exist: {calibpipe_dl1_file}"
|
|
165
|
+
assert (
|
|
166
|
+
calibpipe_dl1_file.stat().st_size > 0
|
|
167
|
+
), f"Parametrized DL1 file is empty: {calibpipe_dl1_file}"
|
|
168
|
+
|
|
169
|
+
# Check that the filename contains one of the expected modes
|
|
170
|
+
filename = calibpipe_dl1_file.name
|
|
171
|
+
expected_modes = ["single_chunk", "same_chunks", "different_chunks"]
|
|
172
|
+
assert any(
|
|
173
|
+
mode in filename for mode in expected_modes
|
|
174
|
+
), f"DL1 filename {filename} doesn't contain expected mode"
|
|
175
|
+
|
|
176
|
+
print(f" ✓ Parametrized DL1 file: {calibpipe_dl1_file}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
# Allow running this test directly for quick verification
|
|
181
|
+
import sys
|
|
182
|
+
|
|
183
|
+
print("Running CalibPipe test data download verification...")
|
|
184
|
+
print(f"Test data URL: {DEFAULT_CALIBPIPE_URL}")
|
|
185
|
+
print(f"Number of test files: {len(TEST_FILES)}")
|
|
186
|
+
print("=" * 60)
|
|
187
|
+
|
|
188
|
+
test_instance = TestCalibPipeTestData()
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
test_instance.test_download_all_test_files()
|
|
192
|
+
test_instance.test_specific_file_types()
|
|
193
|
+
test_instance.test_file_categories()
|
|
194
|
+
print("\n" + "=" * 60)
|
|
195
|
+
print(
|
|
196
|
+
"✅ All tests passed! Test data download and caching is working correctly."
|
|
197
|
+
)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
print(f"\n❌ Test failed: {e}")
|
|
200
|
+
sys.exit(1)
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
|
|
5
|
+
import astropy.units as u
|
|
6
|
+
import pytest
|
|
7
|
+
from astropy.table import Table
|
|
8
|
+
from calibpipe.tools.telescope_cross_calibration_calculator import (
|
|
9
|
+
CalculateCrossCalibration,
|
|
10
|
+
RelativeThroughputFitter,
|
|
11
|
+
)
|
|
12
|
+
from ctapipe.instrument import SubarrayDescription
|
|
13
|
+
from ctapipe.io import read_table
|
|
14
|
+
from traitlets.config import Config
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("calibpipe.application")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestTelescopeCrossCalibration:
|
|
20
|
+
@pytest.fixture(scope="class")
|
|
21
|
+
def test_calibrator(self, cross_calibration_dl2_file):
|
|
22
|
+
test_config = {
|
|
23
|
+
"CalculateCrossCalibration": {
|
|
24
|
+
"input_url": str(cross_calibration_dl2_file),
|
|
25
|
+
"event_filters": {
|
|
26
|
+
"min_gammaness": 0.5,
|
|
27
|
+
},
|
|
28
|
+
"reconstruction_algorithm": "RandomForest",
|
|
29
|
+
"RelativeThroughputFitter": {
|
|
30
|
+
"throughput_normalization": [
|
|
31
|
+
["type", "LST*", 1.0],
|
|
32
|
+
["type", "MST*", 1.0],
|
|
33
|
+
["type", "SST*", 1.0],
|
|
34
|
+
],
|
|
35
|
+
"reference_telescopes": [
|
|
36
|
+
["type", "LST*", 1],
|
|
37
|
+
["type", "MST*", 5],
|
|
38
|
+
["type", "SST*", 37],
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
"PairFinder": {
|
|
42
|
+
"max_impact_distance": [
|
|
43
|
+
["type", "LST*", 125.0],
|
|
44
|
+
["type", "MST*", 125.0],
|
|
45
|
+
["type", "SST*", 225.0],
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
tool = CalculateCrossCalibration(config=Config(test_config))
|
|
51
|
+
|
|
52
|
+
return tool
|
|
53
|
+
|
|
54
|
+
@pytest.fixture()
|
|
55
|
+
def test_fitter_config(self):
|
|
56
|
+
fitter_config = {
|
|
57
|
+
"RelativeThroughputFitter": {
|
|
58
|
+
"throughput_normalization": [
|
|
59
|
+
["type", "LST*", 1.0],
|
|
60
|
+
["type", "MST*", 1.0],
|
|
61
|
+
["type", "SST*", 1.0],
|
|
62
|
+
],
|
|
63
|
+
"reference_telescopes": [
|
|
64
|
+
["type", "LST*", 1],
|
|
65
|
+
["type", "MST*", 5],
|
|
66
|
+
["type", "SST*", 37],
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
return Config(fitter_config)
|
|
71
|
+
|
|
72
|
+
@pytest.fixture()
|
|
73
|
+
def mock_minuit(self):
|
|
74
|
+
m = MagicMock()
|
|
75
|
+
m.values = {"x0": 0.74, "x1": 0.75, "x2": 0.76}
|
|
76
|
+
m.errors = {"x0": 1e-6, "x1": 2e-6, "x2": 3e-6}
|
|
77
|
+
return m
|
|
78
|
+
|
|
79
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
80
|
+
def test_create_telescope_pairs(self, test_calibrator):
|
|
81
|
+
test_calibrator.setup()
|
|
82
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
83
|
+
assert "MST" in telescope_pairs
|
|
84
|
+
assert len(telescope_pairs["MST"]) > 0
|
|
85
|
+
assert "SST" in telescope_pairs
|
|
86
|
+
assert len(telescope_pairs["SST"]) > 0
|
|
87
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs(by_tel_type=False)
|
|
88
|
+
assert "ALL" in telescope_pairs
|
|
89
|
+
assert len(telescope_pairs["ALL"]) > 0
|
|
90
|
+
|
|
91
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
92
|
+
def test_distance(self, test_calibrator):
|
|
93
|
+
# if the requested maximum telescope distance is too small, no tel pair should be returned
|
|
94
|
+
updated_pair_finder_config = {
|
|
95
|
+
"CalculateCrossCalibration": {
|
|
96
|
+
"PairFinder": {
|
|
97
|
+
"max_impact_distance": [
|
|
98
|
+
["type", "LST*", 10.0],
|
|
99
|
+
["type", "MST*", 10.0],
|
|
100
|
+
["type", "SST*", 10.0],
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
test_calibrator.update_config(Config(updated_pair_finder_config))
|
|
107
|
+
test_calibrator.setup()
|
|
108
|
+
|
|
109
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
110
|
+
assert all(
|
|
111
|
+
isinstance(value, set) and len(value) == 0
|
|
112
|
+
for value in telescope_pairs.values()
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
116
|
+
def test_no_reverse_pairs(self, test_calibrator):
|
|
117
|
+
# Ensure the reverse pair (j, i) is not present
|
|
118
|
+
test_calibrator.setup()
|
|
119
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
120
|
+
|
|
121
|
+
for telescope_type, pairs in telescope_pairs.items():
|
|
122
|
+
for i, j in pairs:
|
|
123
|
+
assert (
|
|
124
|
+
(j, i) not in pairs
|
|
125
|
+
), f"Reverse pair ({j}, {i}) exists in {telescope_type} pairs"
|
|
126
|
+
|
|
127
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
128
|
+
def test_allowed_telescope_names(self, test_calibrator):
|
|
129
|
+
# Ensure all keys in the dictionary belong to the allowed names
|
|
130
|
+
allowed_telescope_names = {"MST", "SST"}
|
|
131
|
+
test_calibrator.setup()
|
|
132
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
133
|
+
for telescope_name in telescope_pairs.keys():
|
|
134
|
+
assert (
|
|
135
|
+
telescope_name in allowed_telescope_names
|
|
136
|
+
), f"Unexpected telescope type '{telescope_name}' found in the output"
|
|
137
|
+
|
|
138
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
139
|
+
def test_maximum_telescope_pairs(self, test_calibrator):
|
|
140
|
+
updated_pair_finder_config = {
|
|
141
|
+
"CalculateCrossCalibration": {
|
|
142
|
+
"PairFinder": {
|
|
143
|
+
"max_impact_distance": [
|
|
144
|
+
["type", "LST*", 150000.0],
|
|
145
|
+
["type", "MST*", 150000.0],
|
|
146
|
+
["type", "SST*", 150000.0],
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
test_calibrator.update_config(Config(updated_pair_finder_config))
|
|
153
|
+
test_calibrator.setup()
|
|
154
|
+
|
|
155
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
156
|
+
|
|
157
|
+
array = read_table(
|
|
158
|
+
test_calibrator.input_url,
|
|
159
|
+
"configuration/instrument/subarray/layout",
|
|
160
|
+
)
|
|
161
|
+
telescope_types = [str(t).strip() for t in array["type"] if t]
|
|
162
|
+
for tel_type in set(telescope_types):
|
|
163
|
+
tel_type_counts = (array["type"] == tel_type).sum()
|
|
164
|
+
if tel_type_counts > 0:
|
|
165
|
+
assert (
|
|
166
|
+
len(telescope_pairs[tel_type])
|
|
167
|
+
== (tel_type_counts * (tel_type_counts - 1)) // 2
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
171
|
+
def test_merge_tables_success(self, test_calibrator):
|
|
172
|
+
"""Test successful merging of tables for telescope pairs."""
|
|
173
|
+
telescope_pairs = {"SST": [(37, 39)], "MST": [(6, 12)]}
|
|
174
|
+
result = test_calibrator.merge_tables(telescope_pairs)
|
|
175
|
+
|
|
176
|
+
assert (37, 39) in result["SST"]
|
|
177
|
+
assert (6, 12) in result["MST"]
|
|
178
|
+
assert len(result["SST"][(37, 39)]) > 0 # Ensure merged table is not empty
|
|
179
|
+
|
|
180
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
181
|
+
def test_merge_tables_empty_input(self, test_calibrator):
|
|
182
|
+
"""Test merge_tables returns empty dict when given an empty input."""
|
|
183
|
+
telescope_pairs = {}
|
|
184
|
+
result = test_calibrator.merge_tables(telescope_pairs)
|
|
185
|
+
assert result == {}
|
|
186
|
+
|
|
187
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
188
|
+
def test_merge_tables_missing_data(self, test_calibrator):
|
|
189
|
+
"""Test `merge_tables` handles missing telescope data correctly."""
|
|
190
|
+
telescope_pairs = {"SST": [(37, 39), (42, 143)], "MST": [(5, 6), (6, 120000)]}
|
|
191
|
+
result = test_calibrator.merge_tables(telescope_pairs)
|
|
192
|
+
assert set(result["MST"].keys()) == {(5, 6)}
|
|
193
|
+
|
|
194
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
195
|
+
def test_inter_calibration_result_format(
|
|
196
|
+
self, test_fitter_config, cross_calibration_dl2_file
|
|
197
|
+
):
|
|
198
|
+
data_subsystem = {
|
|
199
|
+
(5, 6): {"mean_asymmetry": 0.1, "mean_uncertainty": 0.01},
|
|
200
|
+
(6, 8): {"mean_asymmetry": -0.05, "mean_uncertainty": 0.02},
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
measured_telescopes = set(
|
|
204
|
+
tel_id for (i, j), entry in data_subsystem.items() for tel_id in (i, j)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
subarray = SubarrayDescription.read(cross_calibration_dl2_file)
|
|
208
|
+
|
|
209
|
+
test_fitter = RelativeThroughputFitter(
|
|
210
|
+
subarray=subarray.select_subarray(tel_ids=measured_telescopes),
|
|
211
|
+
config=test_fitter_config,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
result = test_fitter.fit("MST", data_subsystem)
|
|
215
|
+
assert "MST" in result
|
|
216
|
+
assert isinstance(result["MST"], dict)
|
|
217
|
+
assert set(result["MST"].keys()) == {"5", "6", "8"}
|
|
218
|
+
assert all(isinstance(v, tuple) and len(v) == 2 for v in result["MST"].values())
|
|
219
|
+
|
|
220
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
221
|
+
def test_event_selection_with_multiple_filters(self, test_calibrator, monkeypatch):
|
|
222
|
+
test_calibrator.setup()
|
|
223
|
+
test_calibrator.event_filters = {
|
|
224
|
+
"min_gammaness": 0.5,
|
|
225
|
+
"min_energy": u.Quantity(0.0, u.GeV),
|
|
226
|
+
}
|
|
227
|
+
gamma_set = {(1, 100), (2, 200), (3, 300)}
|
|
228
|
+
energy_set = {(1, 100), (3, 300)}
|
|
229
|
+
|
|
230
|
+
monkeypatch.setattr(
|
|
231
|
+
test_calibrator,
|
|
232
|
+
"_apply_min_gammaness",
|
|
233
|
+
lambda tel_id1, tel_id2, threshold: gamma_set,
|
|
234
|
+
)
|
|
235
|
+
monkeypatch.setattr(
|
|
236
|
+
test_calibrator,
|
|
237
|
+
"_apply_min_energy",
|
|
238
|
+
lambda tel_id1, tel_id2, threshold: energy_set,
|
|
239
|
+
)
|
|
240
|
+
merged_table = Table({"obs_id": [1, 2, 3, 4], "event_id": [100, 200, 300, 400]})
|
|
241
|
+
|
|
242
|
+
filtered = test_calibrator.event_selection(merged_table, tel1=1, tel2=3)
|
|
243
|
+
assert len(filtered) == 2
|
|
244
|
+
|
|
245
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
246
|
+
def test_event_selection_with_invalid_filter(self, test_calibrator):
|
|
247
|
+
test_calibrator.setup()
|
|
248
|
+
test_calibrator.event_filters = {
|
|
249
|
+
"non_existent_filter": 4,
|
|
250
|
+
}
|
|
251
|
+
merged_table = Table({"obs_id": [1, 2, 3, 4], "event_id": [100, 200, 300, 400]})
|
|
252
|
+
|
|
253
|
+
with pytest.raises(
|
|
254
|
+
ValueError,
|
|
255
|
+
match="Filter non_existent_filter is not implemented or not recognized.",
|
|
256
|
+
):
|
|
257
|
+
_ = test_calibrator.event_selection(merged_table, tel1=1, tel2=2)
|
|
258
|
+
|
|
259
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
260
|
+
def test_system_cross_calibrator(self, test_calibrator, caplog):
|
|
261
|
+
# Small scale integration test
|
|
262
|
+
caplog.set_level(logging.ERROR, logger="calibpipe.application")
|
|
263
|
+
updated_cc_config = {
|
|
264
|
+
"CalculateCrossCalibration": {
|
|
265
|
+
"event_filters": {
|
|
266
|
+
"min_gammaness": 0.5,
|
|
267
|
+
},
|
|
268
|
+
"RelativeThroughputFitter": {
|
|
269
|
+
"throughput_normalization": [
|
|
270
|
+
["type", "LST*", 1.0],
|
|
271
|
+
["type", "MST*", 1.0],
|
|
272
|
+
["type", "SST*", 1.0],
|
|
273
|
+
],
|
|
274
|
+
"reference_telescopes": [
|
|
275
|
+
["type", "LST*", 1],
|
|
276
|
+
["type", "MST*", 5],
|
|
277
|
+
["type", "SST*", 37],
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
"PairFinder": {
|
|
281
|
+
"max_impact_distance": [
|
|
282
|
+
["type", "LST*", 125.0],
|
|
283
|
+
["type", "MST*", 125.0],
|
|
284
|
+
["type", "SST*", 225.0],
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
test_calibrator.update_config(Config(updated_cc_config))
|
|
290
|
+
test_calibrator.setup()
|
|
291
|
+
telescope_pairs = test_calibrator.pair_finder.find_pairs()
|
|
292
|
+
merged_tables = test_calibrator.merge_tables(telescope_pairs)
|
|
293
|
+
energy_asymmetry_results = test_calibrator.calculate_energy_asymmetry(
|
|
294
|
+
merged_tables
|
|
295
|
+
)
|
|
296
|
+
results = {}
|
|
297
|
+
|
|
298
|
+
for subarray_name, subarray_data in energy_asymmetry_results.items():
|
|
299
|
+
measured_telescopes = set(
|
|
300
|
+
tel_id for (i, j), entry in subarray_data.items() for tel_id in (i, j)
|
|
301
|
+
)
|
|
302
|
+
fitter = RelativeThroughputFitter(
|
|
303
|
+
subarray=test_calibrator.subarray.select_subarray(
|
|
304
|
+
tel_ids=measured_telescopes, name=subarray_name
|
|
305
|
+
),
|
|
306
|
+
parent=test_calibrator,
|
|
307
|
+
)
|
|
308
|
+
results.update(fitter.fit(subarray_name, subarray_data))
|
|
309
|
+
|
|
310
|
+
cross_type_pairs = test_calibrator.pair_finder.find_pairs(
|
|
311
|
+
by_tel_type=False, cross_type_only=True
|
|
312
|
+
)
|
|
313
|
+
cross_calibration_results = test_calibrator.compute_cross_type_energy_ratios(
|
|
314
|
+
cross_type_pairs["XTEL"], results
|
|
315
|
+
)
|
|
316
|
+
assert cross_calibration_results[("MST", "SST")][0] == pytest.approx(
|
|
317
|
+
1.0119, rel=1e-2
|
|
318
|
+
)
|
|
319
|
+
assert cross_calibration_results[("MST", "SST")][1] == pytest.approx(
|
|
320
|
+
0.0042591, rel=1e-2
|
|
321
|
+
)
|
|
322
|
+
# Below testing energy asymmetry
|
|
323
|
+
assert energy_asymmetry_results["MST"][(6, 12)][
|
|
324
|
+
"mean_uncertainty"
|
|
325
|
+
] == pytest.approx(1.3613071073770851e-06, rel=1e-4)
|
|
326
|
+
assert energy_asymmetry_results["MST"][(6, 12)][
|
|
327
|
+
"mean_asymmetry"
|
|
328
|
+
] == pytest.approx(-0.0008690296768284202, rel=1e-2)
|
|
329
|
+
# Below testing the fixed telescopes
|
|
330
|
+
assert results["SST"]["37"][0] == pytest.approx(1.0, rel=1e-4)
|
|
331
|
+
assert results["MST"]["5"][0] == pytest.approx(1.0, rel=1e-4)
|
|
332
|
+
|
|
333
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
334
|
+
def test_save_monitoring_data(self, test_calibrator, tmp_path):
|
|
335
|
+
# Prepare input
|
|
336
|
+
class SizeType(Enum):
|
|
337
|
+
MST = "MST"
|
|
338
|
+
SST = "SST"
|
|
339
|
+
LST = "LST"
|
|
340
|
+
|
|
341
|
+
intercalibration_results = {
|
|
342
|
+
SizeType.MST: {
|
|
343
|
+
"6": (1.23, 0.01),
|
|
344
|
+
"8": (0.97, 0.02),
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
cross_calibration_results = {
|
|
348
|
+
(SizeType.MST, SizeType.SST): (1.05, 0.05),
|
|
349
|
+
(SizeType.LST, SizeType.SST): (1.15, 0.05),
|
|
350
|
+
(SizeType.LST, SizeType.MST): (0.95, 0.05),
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# Run
|
|
354
|
+
updated_cc_config = {
|
|
355
|
+
"CalculateCrossCalibration": {
|
|
356
|
+
"event_filters": {
|
|
357
|
+
"get_gamma_like_events": 0.5,
|
|
358
|
+
},
|
|
359
|
+
"output_url": "x_calib_test_dl2.h5",
|
|
360
|
+
"overwrite": True,
|
|
361
|
+
"RelativeThroughputFitter": {
|
|
362
|
+
"throughput_normalization": [
|
|
363
|
+
["type", "LST*", 1.0],
|
|
364
|
+
["type", "MST*", 1.0],
|
|
365
|
+
["type", "SST*", 1.0],
|
|
366
|
+
],
|
|
367
|
+
"reference_telescopes": [
|
|
368
|
+
["type", "LST*", 1],
|
|
369
|
+
["type", "MST*", 5],
|
|
370
|
+
["type", "SST*", 37],
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
"PairFinder": {
|
|
374
|
+
"max_impact_distance": [
|
|
375
|
+
["type", "LST*", 125.0],
|
|
376
|
+
["type", "MST*", 125.0],
|
|
377
|
+
["type", "SST*", 225.0],
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
test_calibrator.update_config(Config(updated_cc_config))
|
|
383
|
+
test_calibrator.setup()
|
|
384
|
+
test_calibrator.save_monitoring_data(
|
|
385
|
+
intercalibration_results, cross_calibration_results
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Validate inter calibration table
|
|
389
|
+
inter_table = read_table(
|
|
390
|
+
test_calibrator.output_url, "/dl2/monitoring/inter_calibration"
|
|
391
|
+
)
|
|
392
|
+
assert len(inter_table) == 2
|
|
393
|
+
assert "tel_id" in inter_table.colnames
|
|
394
|
+
assert "value" in inter_table.colnames
|
|
395
|
+
assert "error" in inter_table.colnames
|
|
396
|
+
|
|
397
|
+
# Validate cross calibration table
|
|
398
|
+
cross_table = read_table(
|
|
399
|
+
test_calibrator.output_url, "/dl2/monitoring/cross_calibration"
|
|
400
|
+
)
|
|
401
|
+
assert len(cross_table) == 3
|
|
402
|
+
assert cross_table["ratio"][0] == pytest.approx(1.05)
|
|
403
|
+
assert cross_table["error"][0] == pytest.approx(0.05)
|
|
404
|
+
assert cross_table["ratio"][1] == pytest.approx(1.15)
|
|
405
|
+
assert cross_table["ratio"][2] == pytest.approx(0.95)
|
|
406
|
+
|
|
407
|
+
@pytest.mark.verifies_usecase("UC-120-2.3")
|
|
408
|
+
def test_get_equidistant_events(self, test_calibrator):
|
|
409
|
+
test_calibrator.setup()
|
|
410
|
+
set_of_events = test_calibrator._apply_max_distance_asymmetry(42, 143, 0.05)
|
|
411
|
+
assert (4991, 1181519) in set_of_events
|
|
412
|
+
assert (4991, 2196419) not in set_of_events
|