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,327 @@
|
|
|
1
|
+
# Set up logging
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import yaml
|
|
8
|
+
from astropy.table import QTable
|
|
9
|
+
from astropy.time import Time
|
|
10
|
+
from calibpipe.core.exceptions import IntermittentError, MissingInputDataError
|
|
11
|
+
from calibpipe.tools.contemporary_mdp_producer import CreateMolecularDensityProfile
|
|
12
|
+
from calibpipe.tools.macobac_calculator import CalculateMACOBAC
|
|
13
|
+
from calibpipe.tools.molecular_atmospheric_model_producer import (
|
|
14
|
+
CreateMolecularAtmosphericModel,
|
|
15
|
+
)
|
|
16
|
+
from calibpipe.tools.reference_atmospheric_model_selector import (
|
|
17
|
+
SelectMolecularAtmosphericModel,
|
|
18
|
+
)
|
|
19
|
+
from ctapipe.core import run_tool
|
|
20
|
+
from traitlets.config import Config
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.verifies_usecase("UC-120-1.2")
|
|
24
|
+
def test_calculate_macobac(tmp_path):
|
|
25
|
+
config = Config()
|
|
26
|
+
config.CalculateMACOBAC = Config()
|
|
27
|
+
config.CalculateMACOBAC.output_file = str(tmp_path / "macobac.ecsv")
|
|
28
|
+
|
|
29
|
+
with patch(
|
|
30
|
+
"calibpipe.tools.macobac_calculator.CO2DataHandler",
|
|
31
|
+
new_callable=MagicMock,
|
|
32
|
+
) as mock_class:
|
|
33
|
+
mock_class.return_value.data_path = str(
|
|
34
|
+
Path(__file__).parent.parent.parent
|
|
35
|
+
/ "data/atmosphere/molecular_atmosphere/"
|
|
36
|
+
)
|
|
37
|
+
tool = CalculateMACOBAC(config=config)
|
|
38
|
+
run_tool(tool)
|
|
39
|
+
|
|
40
|
+
output_file = Path(config.CalculateMACOBAC.output_file)
|
|
41
|
+
assert output_file.exists(), "Output file was not created."
|
|
42
|
+
|
|
43
|
+
result_table = QTable.read(output_file, format="ascii.ecsv")
|
|
44
|
+
expected_co2_concentration = 419.3
|
|
45
|
+
assert (
|
|
46
|
+
result_table["co2_concentration"][0].value
|
|
47
|
+
== pytest.approx(expected_co2_concentration, abs=0.1)
|
|
48
|
+
), f"CO2 concentration does not match expected value ({result_table['co2_concentration'][0]} != {expected_co2_concentration})."
|
|
49
|
+
assert (
|
|
50
|
+
result_table["estimation_date"][0]
|
|
51
|
+
== Time(str(datetime.now(timezone.utc).date()), out_subfmt="date").iso
|
|
52
|
+
), "Estimation date does not match expected value."
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.db()
|
|
56
|
+
@pytest.mark.verifies_usecase("UC-120-1.7")
|
|
57
|
+
@pytest.mark.verifies_usecase("UC-120-1.8")
|
|
58
|
+
def test_create_molecular_atmospheric_model(tmp_path):
|
|
59
|
+
config_path = (
|
|
60
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
61
|
+
/ "docs/source/user_guide/atmosphere/configuration/create_molecular_atmospheric_model.yaml"
|
|
62
|
+
)
|
|
63
|
+
with open(config_path) as file:
|
|
64
|
+
config = Config(yaml.load(file, Loader=yaml.SafeLoader))
|
|
65
|
+
|
|
66
|
+
config.CreateMolecularAtmosphericModel.output_path = str(tmp_path)
|
|
67
|
+
# Mock the necessary components
|
|
68
|
+
with patch(
|
|
69
|
+
# "calibpipe.tools.molecular_atmospheric_model_producer.MeteoDataHandler",
|
|
70
|
+
"calibpipe.tools.atmospheric_base_tool.MeteoDataHandler",
|
|
71
|
+
new_callable=MagicMock,
|
|
72
|
+
) as mock_meteo_handler_class:
|
|
73
|
+
tool = CreateMolecularAtmosphericModel(config=config)
|
|
74
|
+
# Test missing input data
|
|
75
|
+
with pytest.raises(MissingInputDataError):
|
|
76
|
+
run_tool(
|
|
77
|
+
tool,
|
|
78
|
+
argv=[
|
|
79
|
+
"-c",
|
|
80
|
+
str(
|
|
81
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
82
|
+
/ "docs/source/user_guide/utils/configuration/db_config.yaml"
|
|
83
|
+
),
|
|
84
|
+
"--macobac12-table-path",
|
|
85
|
+
str(
|
|
86
|
+
Path(__file__).parent.parent.parent
|
|
87
|
+
/ "data/atmosphere/molecular_atmosphere/macobac.ecsv"
|
|
88
|
+
),
|
|
89
|
+
],
|
|
90
|
+
raises=True,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Now set up the mock data handler
|
|
94
|
+
mock_meteo_handler = MagicMock()
|
|
95
|
+
mock_meteo_handler_class.from_name.return_value = mock_meteo_handler
|
|
96
|
+
mock_meteo_handler.data_path = str(
|
|
97
|
+
Path(__file__).parent.parent.parent
|
|
98
|
+
/ "data/atmosphere/molecular_atmosphere/"
|
|
99
|
+
)
|
|
100
|
+
mock_meteo_handler.request_data.return_value = 0
|
|
101
|
+
|
|
102
|
+
run_tool(
|
|
103
|
+
tool,
|
|
104
|
+
argv=[
|
|
105
|
+
"-c",
|
|
106
|
+
str(
|
|
107
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
108
|
+
/ "docs/source/user_guide/utils/configuration/db_config.yaml"
|
|
109
|
+
),
|
|
110
|
+
"--macobac12-table-path",
|
|
111
|
+
str(
|
|
112
|
+
Path(__file__).parent.parent.parent
|
|
113
|
+
/ "data/atmosphere/molecular_atmosphere/macobac.ecsv"
|
|
114
|
+
),
|
|
115
|
+
],
|
|
116
|
+
raises=True,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Check if the output files were created
|
|
120
|
+
output_profile = (
|
|
121
|
+
Path(config.CreateMolecularAtmosphericModel.output_path)
|
|
122
|
+
/ "contemporary_atmospheric_profile.ascii.ecsv"
|
|
123
|
+
)
|
|
124
|
+
output_extinction = (
|
|
125
|
+
Path(config.CreateMolecularAtmosphericModel.output_path)
|
|
126
|
+
/ "contemporary_rayleigh_extinction_profile.ascii.ecsv"
|
|
127
|
+
)
|
|
128
|
+
assert (
|
|
129
|
+
output_profile.exists()
|
|
130
|
+
), "Contemporary atmospheric profile file was not created."
|
|
131
|
+
assert (
|
|
132
|
+
output_extinction.exists()
|
|
133
|
+
), "Contemporary Rayleigh extinction profile file was not created."
|
|
134
|
+
|
|
135
|
+
# Read and validate the output files
|
|
136
|
+
profile_table = QTable.read(output_profile, format="ascii.ecsv")
|
|
137
|
+
extinction_table = QTable.read(output_extinction, format="ascii.ecsv")
|
|
138
|
+
|
|
139
|
+
expected_profile_columns = [
|
|
140
|
+
"altitude",
|
|
141
|
+
"atmospheric_density",
|
|
142
|
+
"atmospheric_thickness",
|
|
143
|
+
"refractive_index_m_1",
|
|
144
|
+
"temperature",
|
|
145
|
+
"pressure",
|
|
146
|
+
"partial_water_pressure",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for column in expected_profile_columns:
|
|
150
|
+
assert (
|
|
151
|
+
column in profile_table.colnames
|
|
152
|
+
), f"{column} column missing in profile table."
|
|
153
|
+
|
|
154
|
+
assert (
|
|
155
|
+
"altitude_max" in extinction_table.colnames
|
|
156
|
+
), "Altitude_max column missing in extinction table."
|
|
157
|
+
assert (
|
|
158
|
+
"altitude_min" in extinction_table.colnames
|
|
159
|
+
), "Altitude_min column missing in extinction table."
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.db()
|
|
163
|
+
@pytest.mark.verifies_usecase("UC-120-1.6")
|
|
164
|
+
def test_create_molecular_density_profile(tmp_path):
|
|
165
|
+
config_path = (
|
|
166
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
167
|
+
/ "docs/source/user_guide/atmosphere/configuration/create_molecular_density_profile.yaml"
|
|
168
|
+
)
|
|
169
|
+
with open(config_path) as file:
|
|
170
|
+
config = Config(yaml.load(file, Loader=yaml.SafeLoader))
|
|
171
|
+
|
|
172
|
+
config.CreateMolecularDensityProfile.output_path = str(tmp_path)
|
|
173
|
+
# Mock the necessary components
|
|
174
|
+
with patch(
|
|
175
|
+
"calibpipe.tools.atmospheric_base_tool.MeteoDataHandler",
|
|
176
|
+
new_callable=MagicMock,
|
|
177
|
+
) as mock_meteo_handler_class:
|
|
178
|
+
tool = CreateMolecularDensityProfile(config=config)
|
|
179
|
+
# Test missing input data
|
|
180
|
+
with pytest.raises(MissingInputDataError):
|
|
181
|
+
run_tool(
|
|
182
|
+
tool,
|
|
183
|
+
argv=[
|
|
184
|
+
"-c",
|
|
185
|
+
str(
|
|
186
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
187
|
+
/ "docs/source/user_guide/utils/configuration/db_config.yaml"
|
|
188
|
+
),
|
|
189
|
+
],
|
|
190
|
+
raises=True,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Now set up the mock data handler
|
|
194
|
+
mock_meteo_handler = MagicMock()
|
|
195
|
+
mock_meteo_handler_class.from_name.return_value = mock_meteo_handler
|
|
196
|
+
mock_meteo_handler.data_path = str(
|
|
197
|
+
Path(__file__).parent.parent.parent
|
|
198
|
+
/ "data/atmosphere/molecular_atmosphere/"
|
|
199
|
+
)
|
|
200
|
+
mock_meteo_handler.request_data.return_value = 0
|
|
201
|
+
|
|
202
|
+
run_tool(
|
|
203
|
+
tool,
|
|
204
|
+
argv=[
|
|
205
|
+
"-c",
|
|
206
|
+
str(
|
|
207
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
208
|
+
/ "docs/source/user_guide/utils/configuration/db_config.yaml"
|
|
209
|
+
),
|
|
210
|
+
],
|
|
211
|
+
raises=True,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Check if the output file was created
|
|
215
|
+
output_file = (
|
|
216
|
+
Path(config.CreateMolecularDensityProfile.output_path)
|
|
217
|
+
/ "contemporary_molecular_density_profile.ascii.ecsv"
|
|
218
|
+
)
|
|
219
|
+
assert (
|
|
220
|
+
output_file.exists()
|
|
221
|
+
), "Contemporary molecular density profile file was not created."
|
|
222
|
+
|
|
223
|
+
# Read and validate the output file
|
|
224
|
+
mdp_table = QTable.read(output_file, format="ascii.ecsv")
|
|
225
|
+
|
|
226
|
+
expected_columns = [
|
|
227
|
+
"altitude",
|
|
228
|
+
"number density",
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
for column in expected_columns:
|
|
232
|
+
assert (
|
|
233
|
+
column in mdp_table.colnames
|
|
234
|
+
), f"{column} column missing in molecular density profile table."
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@pytest.mark.db()
|
|
238
|
+
@pytest.mark.verifies_usecase("UC-120-1.3")
|
|
239
|
+
def test_select_molecular_atmospheric_model(tmp_path):
|
|
240
|
+
config_path = (
|
|
241
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
242
|
+
/ "docs/source/user_guide/atmosphere/configuration/select_reference_atmospheric_model.yaml"
|
|
243
|
+
)
|
|
244
|
+
with open(config_path) as file:
|
|
245
|
+
config = Config(yaml.load(file, Loader=yaml.SafeLoader))
|
|
246
|
+
|
|
247
|
+
config.SelectMolecularAtmosphericModel.output_path = str(tmp_path)
|
|
248
|
+
# Mock the necessary components
|
|
249
|
+
with patch(
|
|
250
|
+
"calibpipe.tools.atmospheric_base_tool.MeteoDataHandler",
|
|
251
|
+
new_callable=MagicMock,
|
|
252
|
+
) as mock_meteo_handler_class:
|
|
253
|
+
tool = SelectMolecularAtmosphericModel(config=config)
|
|
254
|
+
|
|
255
|
+
# Mock database and data handler behavior
|
|
256
|
+
mock_meteo_handler = MagicMock()
|
|
257
|
+
mock_meteo_handler_class.from_name.return_value = mock_meteo_handler
|
|
258
|
+
mock_meteo_handler.data_path = str(
|
|
259
|
+
Path(__file__).parent.parent.parent
|
|
260
|
+
/ "data/atmosphere/molecular_atmosphere/"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Test missing input data
|
|
264
|
+
mock_meteo_handler.request_data.return_value = 1
|
|
265
|
+
|
|
266
|
+
with pytest.raises(IntermittentError):
|
|
267
|
+
run_tool(
|
|
268
|
+
tool,
|
|
269
|
+
argv=[
|
|
270
|
+
"-c",
|
|
271
|
+
str(
|
|
272
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
273
|
+
/ "docs/source/user_guide/utils/configuration/db_config.yaml"
|
|
274
|
+
),
|
|
275
|
+
],
|
|
276
|
+
raises=True,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Check if the output files were created
|
|
280
|
+
output_profile = (
|
|
281
|
+
Path(config.SelectMolecularAtmosphericModel.output_path)
|
|
282
|
+
/ "selected_atmospheric_profile.ascii.ecsv"
|
|
283
|
+
)
|
|
284
|
+
output_extinction = (
|
|
285
|
+
Path(config.SelectMolecularAtmosphericModel.output_path)
|
|
286
|
+
/ "selected_rayleigh_extinction_profile.ascii.ecsv"
|
|
287
|
+
)
|
|
288
|
+
assert (
|
|
289
|
+
output_profile.exists()
|
|
290
|
+
), "Selected atmospheric profile file was not created."
|
|
291
|
+
assert (
|
|
292
|
+
output_extinction.exists()
|
|
293
|
+
), "Selected Rayleigh extinction profile file was not created."
|
|
294
|
+
|
|
295
|
+
# Read and validate the output files
|
|
296
|
+
profile_table = QTable.read(output_profile, format="ascii.ecsv")
|
|
297
|
+
extinction_table = QTable.read(output_extinction, format="ascii.ecsv")
|
|
298
|
+
|
|
299
|
+
expected_profile_columns = [
|
|
300
|
+
"altitude",
|
|
301
|
+
"atmospheric_density",
|
|
302
|
+
"atmospheric_thickness",
|
|
303
|
+
"refractive_index_m_1",
|
|
304
|
+
"temperature",
|
|
305
|
+
"pressure",
|
|
306
|
+
"partial_water_pressure",
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
for column in expected_profile_columns:
|
|
310
|
+
assert (
|
|
311
|
+
column in profile_table.colnames
|
|
312
|
+
), f"{column} column missing in profile table."
|
|
313
|
+
|
|
314
|
+
expected_extinction_columns = ["altitude_min", "altitude_max"]
|
|
315
|
+
expected_extinction_columns.extend([f"{i}.0 nm" for i in range(200, 1000)])
|
|
316
|
+
|
|
317
|
+
for column in expected_extinction_columns:
|
|
318
|
+
assert (
|
|
319
|
+
column in extinction_table.colnames
|
|
320
|
+
), f"{column} column missing in extinction table."
|
|
321
|
+
if "altitude" in column:
|
|
322
|
+
assert (
|
|
323
|
+
extinction_table[column].unit == "km"
|
|
324
|
+
), f"Column {column} unit does not match expected unit."
|
|
325
|
+
assert (
|
|
326
|
+
extinction_table[column].ndim == 1
|
|
327
|
+
), f"Column {column} does not have the expected number of dimensions."
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Import the necessary modules and classes for testing
|
|
2
|
+
import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import astropy.units as u
|
|
7
|
+
import pytest
|
|
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
|
|
15
|
+
from calibpipe.database.connections import CalibPipeDatabase
|
|
16
|
+
from calibpipe.database.interfaces import TableHandler
|
|
17
|
+
from calibpipe.telescope.throughput.containers import OpticalThoughtputContainer
|
|
18
|
+
from calibpipe.utils.observatory import (
|
|
19
|
+
Observatory,
|
|
20
|
+
)
|
|
21
|
+
from traitlets.config import Config
|
|
22
|
+
|
|
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
|
+
|
|
33
|
+
# Fixture to provide a database connection
|
|
34
|
+
@pytest.fixture()
|
|
35
|
+
def test_config():
|
|
36
|
+
# Setup and connect to the test database
|
|
37
|
+
config_path = Path(__file__).parent.joinpath(
|
|
38
|
+
"../../../../../docs/source/user_guide/utils/configuration/"
|
|
39
|
+
)
|
|
40
|
+
with open(config_path.joinpath("upload_observatory_data_db.yaml")) as yaml_file:
|
|
41
|
+
config_data = yaml.safe_load(yaml_file)
|
|
42
|
+
config_data = config_data["UploadObservatoryData"]
|
|
43
|
+
|
|
44
|
+
with open(config_path.joinpath("db_config.yaml")) as yaml_file:
|
|
45
|
+
config_data |= yaml.safe_load(yaml_file)
|
|
46
|
+
return config_data
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture()
|
|
50
|
+
def test_container(test_config):
|
|
51
|
+
return Observatory(config=Config(test_config["observatories"][0])).containers[0]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Test cases for TableHandler class and other functions in the module
|
|
55
|
+
class TestTableHandler:
|
|
56
|
+
# Test get_database_table_insertion method
|
|
57
|
+
@pytest.mark.db()
|
|
58
|
+
def test_get_database_table_insertion(self, test_container):
|
|
59
|
+
# Prepare a mock container and call the method
|
|
60
|
+
table, kwargs = TableHandler.get_database_table_insertion(test_container)
|
|
61
|
+
|
|
62
|
+
# Assert that the table and kwargs are not None
|
|
63
|
+
assert table is not None
|
|
64
|
+
assert kwargs is not None
|
|
65
|
+
|
|
66
|
+
# Test read_table_from_database method
|
|
67
|
+
@pytest.mark.db()
|
|
68
|
+
def test_read_table_from_database(self, test_container, test_config):
|
|
69
|
+
TableHandler.prepare_db_tables(
|
|
70
|
+
[
|
|
71
|
+
test_container,
|
|
72
|
+
],
|
|
73
|
+
test_config["database_configuration"],
|
|
74
|
+
)
|
|
75
|
+
condition = "c.elevation == 3000"
|
|
76
|
+
with CalibPipeDatabase(**test_config["database_configuration"]) as connection:
|
|
77
|
+
qtable = TableHandler.read_table_from_database(
|
|
78
|
+
type(test_container), connection, condition
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Assert that qtable is not None and has the expected columns
|
|
82
|
+
assert qtable is not None
|
|
83
|
+
assert "elevation" in qtable.colnames
|
|
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
|
+
mean=0.95,
|
|
94
|
+
median=0.90,
|
|
95
|
+
std=0.02,
|
|
96
|
+
sem=0.001,
|
|
97
|
+
method="muon analysis",
|
|
98
|
+
time_start=datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc),
|
|
99
|
+
time_end=datetime.datetime(2025, 12, 31, tzinfo=datetime.timezone.utc),
|
|
100
|
+
obs_id=12345,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Mock metadata containers
|
|
104
|
+
reference_metadata = ReferenceMetadataContainer(
|
|
105
|
+
version_atmospheric_model="1.0",
|
|
106
|
+
version=1,
|
|
107
|
+
ID_optical_throughput=None, # Will be set during the upload
|
|
108
|
+
)
|
|
109
|
+
product_metadata = ProductReferenceMetadataContainer(
|
|
110
|
+
description="Test product",
|
|
111
|
+
creation_time="2025-04-08T12:00:00Z",
|
|
112
|
+
product_id="12345",
|
|
113
|
+
)
|
|
114
|
+
contact_metadata = ContactReferenceMetadataContainer(
|
|
115
|
+
organization="Test Organization",
|
|
116
|
+
name="Test User",
|
|
117
|
+
email="test@example.com",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Combine metadata into a list
|
|
121
|
+
metadata = [reference_metadata, product_metadata, contact_metadata]
|
|
122
|
+
|
|
123
|
+
# Mock database behavior
|
|
124
|
+
mock_connection.execute.return_value.fetchone.return_value = MagicMock(
|
|
125
|
+
_asdict=lambda: {"ID": 1}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Patch the insert_row_in_database method
|
|
129
|
+
with patch(
|
|
130
|
+
"calibpipe.database.interfaces.table_handler.TableHandler.insert_row_in_database",
|
|
131
|
+
) as mock_insert:
|
|
132
|
+
# Call the upload_data function
|
|
133
|
+
TableHandler.upload_data(data, metadata, mock_connection)
|
|
134
|
+
|
|
135
|
+
# Assertions to verify correct behavior
|
|
136
|
+
# Verify that the main data was inserted
|
|
137
|
+
data_table, data_kwargs = Adapter.to_postgres(data)
|
|
138
|
+
|
|
139
|
+
calls = mock_insert.call_args_list
|
|
140
|
+
|
|
141
|
+
# Check that a call was made with the expected table and connection
|
|
142
|
+
assert any(
|
|
143
|
+
call_args[0][0] == data_table
|
|
144
|
+
and call_args[0][2] == mock_connection
|
|
145
|
+
and call_args[0][1]["mean"] == 0.95
|
|
146
|
+
for call_args in calls
|
|
147
|
+
), "Expected call to insert_row_in_database not found."
|
|
148
|
+
|
|
149
|
+
ref_table, ref_kwargs = Adapter.to_postgres(reference_metadata)
|
|
150
|
+
|
|
151
|
+
assert any(
|
|
152
|
+
call_args[0][0] == ref_table
|
|
153
|
+
and call_args[0][2] == mock_connection
|
|
154
|
+
and call_args[0][1]["version_atmospheric_model"] == "1.0"
|
|
155
|
+
for call_args in calls
|
|
156
|
+
), "Expected call to insert_row_in_database not found."
|
|
157
|
+
|
|
158
|
+
for container in metadata[1:]:
|
|
159
|
+
meta_table, meta_kwargs = Adapter.to_postgres(container)
|
|
160
|
+
assert any(
|
|
161
|
+
call_args[0][0] == meta_table and call_args[0][2] == mock_connection
|
|
162
|
+
for call_args in calls
|
|
163
|
+
), "Expected call to insert_row_in_database not found."
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Test sqlalchemy types."""
|
|
2
|
+
|
|
3
|
+
from calibpipe.database.interfaces.types import (
|
|
4
|
+
BigInteger,
|
|
5
|
+
Boolean,
|
|
6
|
+
ColumnType,
|
|
7
|
+
Date,
|
|
8
|
+
DateTime,
|
|
9
|
+
Double,
|
|
10
|
+
Float,
|
|
11
|
+
Integer,
|
|
12
|
+
NDArray,
|
|
13
|
+
Numeric,
|
|
14
|
+
SmallInteger,
|
|
15
|
+
String,
|
|
16
|
+
Time,
|
|
17
|
+
)
|
|
18
|
+
from sqlalchemy.sql.type_api import TypeEngine
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_types():
|
|
22
|
+
"""Test that types are instance of TypeEngine."""
|
|
23
|
+
for type_ in [
|
|
24
|
+
Boolean,
|
|
25
|
+
SmallInteger,
|
|
26
|
+
Integer,
|
|
27
|
+
BigInteger,
|
|
28
|
+
Float,
|
|
29
|
+
Double,
|
|
30
|
+
Numeric,
|
|
31
|
+
String,
|
|
32
|
+
Date,
|
|
33
|
+
DateTime,
|
|
34
|
+
Time,
|
|
35
|
+
ColumnType,
|
|
36
|
+
NDArray,
|
|
37
|
+
]:
|
|
38
|
+
assert issubclass(type_, TypeEngine)
|