ctao-calibpipe 0.1.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.
Potentially problematic release.
This version of ctao-calibpipe might be problematic. Click here for more details.
- calibpipe/__init__.py +5 -0
- calibpipe/_dev_version/__init__.py +9 -0
- calibpipe/_version.py +21 -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 +195 -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 +61 -0
- calibpipe/database/adapter/database_containers/atmosphere.py +199 -0
- calibpipe/database/adapter/database_containers/common_metadata.py +148 -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/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 +351 -0
- calibpipe/database/interfaces/types.py +96 -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/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 +66 -0
- calibpipe/tests/unittests/database/test_types.py +38 -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/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/observatory_data_db_loader.py +71 -0
- calibpipe/tools/reference_atmospheric_model_selector.py +201 -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.1.0.dist-info/METADATA +86 -0
- ctao_calibpipe-0.1.0.dist-info/RECORD +93 -0
- ctao_calibpipe-0.1.0.dist-info/WHEEL +5 -0
- ctao_calibpipe-0.1.0.dist-info/entry_points.txt +8 -0
- ctao_calibpipe-0.1.0.dist-info/licenses/AUTHORS.md +13 -0
- ctao_calibpipe-0.1.0.dist-info/licenses/LICENSE +21 -0
- ctao_calibpipe-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Python built-in imports
|
|
2
|
+
import importlib.resources
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from datetime import date, datetime, timezone
|
|
5
|
+
|
|
6
|
+
import astropy.units as u
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pytest
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
|
|
11
|
+
# Third-party imports
|
|
12
|
+
from astropy.coordinates import Latitude, Longitude
|
|
13
|
+
from astropy.table import Table
|
|
14
|
+
from calibpipe.database.adapter.database_containers import ContainerMap
|
|
15
|
+
from calibpipe.database.connections import CalibPipeDatabase
|
|
16
|
+
from calibpipe.database.interfaces import TableHandler
|
|
17
|
+
from calibpipe.tests.data import utils
|
|
18
|
+
|
|
19
|
+
# Internal imports
|
|
20
|
+
from calibpipe.utils.observatory import (
|
|
21
|
+
Observatory,
|
|
22
|
+
ObservatoryContainer,
|
|
23
|
+
Season,
|
|
24
|
+
SeasonContainer,
|
|
25
|
+
)
|
|
26
|
+
from sqlalchemy.exc import IntegrityError
|
|
27
|
+
from traitlets.config import Config
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestSeason:
|
|
31
|
+
# test season configuration shall contain new year to test the start>stop condition
|
|
32
|
+
test_season_configuration = {
|
|
33
|
+
"Season": {
|
|
34
|
+
"name": "winter",
|
|
35
|
+
"start_month": 11,
|
|
36
|
+
"start_day": 16,
|
|
37
|
+
"stop_month": 4,
|
|
38
|
+
"stop_day": 30,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@pytest.fixture()
|
|
43
|
+
def test_season(self):
|
|
44
|
+
return Season(config=Config(self.test_season_configuration))
|
|
45
|
+
|
|
46
|
+
def test_contains_timestamp(self, test_season):
|
|
47
|
+
test_timestamp = datetime(
|
|
48
|
+
year=2020, month=2, day=29, hour=0, minute=0, second=0, microsecond=0
|
|
49
|
+
)
|
|
50
|
+
assert test_timestamp in test_season
|
|
51
|
+
test_date = date(year=2020, month=2, day=29)
|
|
52
|
+
assert test_date in test_season
|
|
53
|
+
test_date_begin = date(year=2020, month=11, day=16)
|
|
54
|
+
test_date_end = date(year=2020, month=4, day=30)
|
|
55
|
+
assert test_date_begin in test_season
|
|
56
|
+
assert test_date_end in test_season
|
|
57
|
+
test_date_out = date(year=2020, month=7, day=29)
|
|
58
|
+
assert test_date_out not in test_season
|
|
59
|
+
|
|
60
|
+
def test_properties(self, test_season):
|
|
61
|
+
assert test_season.start == (11, 16)
|
|
62
|
+
assert test_season.stop == (4, 30)
|
|
63
|
+
assert test_season.months == [11, 12, 1, 2, 3, 4]
|
|
64
|
+
assert test_season.reference_dates == (date(1999, 11, 16), date(2000, 4, 30))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestObservatory:
|
|
68
|
+
cta_north_cfg = {
|
|
69
|
+
"Observatory": {
|
|
70
|
+
"name": "CTAO-North",
|
|
71
|
+
"latitude": 28.761795,
|
|
72
|
+
"longitude": -17.890701,
|
|
73
|
+
"elevation": 2150,
|
|
74
|
+
"seasons": [
|
|
75
|
+
{
|
|
76
|
+
"Season": {
|
|
77
|
+
"name": "spring",
|
|
78
|
+
"start_month": 5,
|
|
79
|
+
"start_day": 1,
|
|
80
|
+
"stop_month": 6,
|
|
81
|
+
"stop_day": 20,
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"Season": {
|
|
86
|
+
"name": "summer",
|
|
87
|
+
"start_month": 6,
|
|
88
|
+
"start_day": 21,
|
|
89
|
+
"stop_month": 10,
|
|
90
|
+
"stop_day": 4,
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"Season": {
|
|
95
|
+
"name": "winter",
|
|
96
|
+
"start_month": 11,
|
|
97
|
+
"start_day": 16,
|
|
98
|
+
"stop_month": 4,
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"Season": {
|
|
103
|
+
"name": "fall",
|
|
104
|
+
"start_month": 10,
|
|
105
|
+
"start_day": 5,
|
|
106
|
+
"stop_month": 11,
|
|
107
|
+
"stop_day": 15,
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
cta_south_cfg = {
|
|
115
|
+
"Observatory": {
|
|
116
|
+
"name": "CTAO-South",
|
|
117
|
+
"latitude": -24.6272,
|
|
118
|
+
"longitude": -70.4039,
|
|
119
|
+
"elevation": 2200,
|
|
120
|
+
"seasons": [
|
|
121
|
+
{
|
|
122
|
+
"Season": {
|
|
123
|
+
"name": "summer",
|
|
124
|
+
"start_month": 11,
|
|
125
|
+
"start_day": 1,
|
|
126
|
+
"stop_month": 5,
|
|
127
|
+
"stop_day": 1,
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"Season": {
|
|
132
|
+
"name": "winter",
|
|
133
|
+
"start_month": 5,
|
|
134
|
+
"start_day": 2,
|
|
135
|
+
"stop_month": 10,
|
|
136
|
+
"stop_day": 31,
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# CI testing configuration
|
|
144
|
+
db_config = {
|
|
145
|
+
"user": "TEST_CALIBPIPE_DB_USER",
|
|
146
|
+
"password": "DUMMY_PSWD",
|
|
147
|
+
"database": "TEST_CALIBPIPE_DB",
|
|
148
|
+
"host": "postgres",
|
|
149
|
+
"autocommit": True,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@pytest.fixture()
|
|
153
|
+
def cta_north(self):
|
|
154
|
+
return Observatory(config=Config(self.cta_north_cfg))
|
|
155
|
+
|
|
156
|
+
@pytest.fixture()
|
|
157
|
+
def cta_south(self):
|
|
158
|
+
return Observatory(config=Config(self.cta_south_cfg))
|
|
159
|
+
|
|
160
|
+
def test_check_seasons(self):
|
|
161
|
+
test_cfg = deepcopy(self.cta_north_cfg)
|
|
162
|
+
test_cfg["Observatory"]["seasons"][2]["Season"]["start_day"] = 17
|
|
163
|
+
with pytest.raises(ValueError, match="The seasons don't cover a year"):
|
|
164
|
+
_ = Observatory(config=Config(test_cfg))
|
|
165
|
+
test_cfg["Observatory"]["seasons"][2]["Season"]["start_day"] = 16
|
|
166
|
+
test_cfg["Observatory"]["seasons"][0]["Season"]["start_day"] = 2
|
|
167
|
+
with pytest.raises(
|
|
168
|
+
ValueError, match="The season coverage has gaps or overlaps!"
|
|
169
|
+
):
|
|
170
|
+
_ = Observatory(config=Config(test_cfg))
|
|
171
|
+
|
|
172
|
+
def test_properties(self, cta_north):
|
|
173
|
+
assert cta_north.name == "CTAO-NORTH"
|
|
174
|
+
assert cta_north.coordinates == (
|
|
175
|
+
Latitude(
|
|
176
|
+
angle=self.cta_north_cfg["Observatory"]["latitude"],
|
|
177
|
+
unit=u.deg,
|
|
178
|
+
),
|
|
179
|
+
Longitude(
|
|
180
|
+
angle=self.cta_north_cfg["Observatory"]["longitude"],
|
|
181
|
+
unit=u.deg,
|
|
182
|
+
wrap_angle=180 * u.deg,
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def test_get_astronomical_night(self, cta_north):
|
|
187
|
+
test_timestamp = datetime(2020, 11, 22, 1, 0, 0, 0)
|
|
188
|
+
test_dusk = datetime(2020, 11, 21, 19, 44, 47, 805822, tzinfo=timezone.utc)
|
|
189
|
+
test_dawn = datetime(2020, 11, 22, 6, 10, 44, 48436, tzinfo=timezone.utc)
|
|
190
|
+
assert cta_north.get_astronomical_night(test_timestamp) == (
|
|
191
|
+
test_dusk,
|
|
192
|
+
test_dawn,
|
|
193
|
+
)
|
|
194
|
+
test_timestamp_daytime = datetime(2020, 11, 22, 12, 0, 0, 0)
|
|
195
|
+
with pytest.raises(
|
|
196
|
+
ValueError, match=r"The provided timestamp .* corresponds to daytime"
|
|
197
|
+
):
|
|
198
|
+
dusk, down = cta_north.get_astronomical_night(test_timestamp_daytime)
|
|
199
|
+
|
|
200
|
+
def test_get_season_from_timestamp(self, cta_north):
|
|
201
|
+
test_timestamp = datetime(2020, 11, 22, 1, 0, 0, 0)
|
|
202
|
+
assert cta_north.get_season_from_timestamp(test_timestamp).upper() == "WINTER"
|
|
203
|
+
|
|
204
|
+
def test_select_season_data(self, cta_north):
|
|
205
|
+
test_data = Table.read(
|
|
206
|
+
importlib.resources.files(utils).joinpath(
|
|
207
|
+
"meteo_data_winter_and_summer.ecsv"
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
summer_data = cta_north.select_season_data(test_data, "SUMMER")
|
|
211
|
+
assert np.all(
|
|
212
|
+
np.vectorize(lambda x: x.month)(summer_data["Timestamp"].to_datetime()) == 7
|
|
213
|
+
)
|
|
214
|
+
winter_data = cta_north.select_season_data(test_data, "WINTER")
|
|
215
|
+
assert np.all(
|
|
216
|
+
np.vectorize(lambda x: x.month)(winter_data["Timestamp"].to_datetime())
|
|
217
|
+
== 12
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
@pytest.mark.db()
|
|
221
|
+
@pytest.mark.observatory()
|
|
222
|
+
def test_db_write(self, cta_north, cta_south):
|
|
223
|
+
with CalibPipeDatabase(
|
|
224
|
+
**self.db_config,
|
|
225
|
+
) as connection:
|
|
226
|
+
table, insertion = TableHandler.get_database_table_insertion(
|
|
227
|
+
cta_north.containers[0], # Observatory table
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Check if the Observatory table exists, create one if needed
|
|
231
|
+
if not sa.inspect(connection.engine).has_table(table.name):
|
|
232
|
+
table.create(bind=connection.engine)
|
|
233
|
+
|
|
234
|
+
with CalibPipeDatabase(
|
|
235
|
+
**self.db_config,
|
|
236
|
+
) as connection:
|
|
237
|
+
table, insertion = TableHandler.get_database_table_insertion(
|
|
238
|
+
cta_north.containers[1], # Season table
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Check if the Season table exists, create one if needed
|
|
242
|
+
if not sa.inspect(connection.engine).has_table(table.name):
|
|
243
|
+
table.create(bind=connection.engine)
|
|
244
|
+
|
|
245
|
+
with CalibPipeDatabase(
|
|
246
|
+
**self.db_config,
|
|
247
|
+
) as connection:
|
|
248
|
+
for test_observatory in [cta_north, cta_south]:
|
|
249
|
+
for container in test_observatory.containers:
|
|
250
|
+
table, insertion = TableHandler.get_database_table_insertion(
|
|
251
|
+
container,
|
|
252
|
+
)
|
|
253
|
+
try:
|
|
254
|
+
TableHandler.insert_row_in_database(
|
|
255
|
+
table, insertion, connection
|
|
256
|
+
)
|
|
257
|
+
except IntegrityError as e:
|
|
258
|
+
if "duplicate key value violates unique constraint" in str(e):
|
|
259
|
+
connection.session.rollback() # Unique constraint violation is expected in some tests due to previous DB setup
|
|
260
|
+
else:
|
|
261
|
+
raise e
|
|
262
|
+
|
|
263
|
+
@pytest.mark.db()
|
|
264
|
+
@pytest.mark.observatory()
|
|
265
|
+
def test_db_read_raw(self):
|
|
266
|
+
observatory_table = ContainerMap.map_to_db_container(
|
|
267
|
+
ObservatoryContainer
|
|
268
|
+
).get_table()
|
|
269
|
+
season_table = ContainerMap.map_to_db_container(SeasonContainer).get_table()
|
|
270
|
+
query_observatory = observatory_table.select().where(
|
|
271
|
+
observatory_table.c.name == "CTAO-NORTH"
|
|
272
|
+
)
|
|
273
|
+
query_season = season_table.select().where(
|
|
274
|
+
season_table.c.name_Observatory == "CTAO-NORTH"
|
|
275
|
+
)
|
|
276
|
+
with CalibPipeDatabase(
|
|
277
|
+
**self.db_config,
|
|
278
|
+
) as connection:
|
|
279
|
+
read_observatory = connection.execute(query_observatory).fetchall()
|
|
280
|
+
assert len(read_observatory) == 1
|
|
281
|
+
read_season = connection.execute(query_season).fetchall()
|
|
282
|
+
assert len(read_season) == 4
|
|
283
|
+
|
|
284
|
+
@pytest.mark.db()
|
|
285
|
+
@pytest.mark.observatory()
|
|
286
|
+
def test_db_table_read_simple(self):
|
|
287
|
+
with CalibPipeDatabase(
|
|
288
|
+
**self.db_config,
|
|
289
|
+
) as connection:
|
|
290
|
+
observatory_qtable = TableHandler.read_table_from_database(
|
|
291
|
+
container=ObservatoryContainer,
|
|
292
|
+
connection=connection,
|
|
293
|
+
)
|
|
294
|
+
# DEBUG purposes only. Use pytest -s -v in order to see this printout
|
|
295
|
+
print(observatory_qtable)
|
|
296
|
+
|
|
297
|
+
@pytest.mark.db()
|
|
298
|
+
@pytest.mark.observatory()
|
|
299
|
+
def test_db_table_read_conditional(self):
|
|
300
|
+
with CalibPipeDatabase(
|
|
301
|
+
**self.db_config,
|
|
302
|
+
) as connection:
|
|
303
|
+
season_qtable = TableHandler.read_table_from_database(
|
|
304
|
+
container=SeasonContainer,
|
|
305
|
+
connection=connection,
|
|
306
|
+
condition="(c.name_Observatory == 'CTAO-NORTH') & (c.alias == 'INTERMEDIATE')",
|
|
307
|
+
)
|
|
308
|
+
# DEBUG purposes only. Use pytest -s -v in order to see this printout
|
|
309
|
+
print(season_qtable)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from datetime import datetime # noqa: D100
|
|
2
|
+
|
|
3
|
+
from ctapipe.core.traits import (
|
|
4
|
+
AstroTime,
|
|
5
|
+
CaselessStrEnum,
|
|
6
|
+
Dict,
|
|
7
|
+
Integer,
|
|
8
|
+
Path,
|
|
9
|
+
Unicode,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from ..atmosphere.meteo_data_handlers import (
|
|
13
|
+
MeteoDataHandler,
|
|
14
|
+
)
|
|
15
|
+
from .basic_tool_with_db import BasicToolWithDB
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AtmosphericBaseTool(BasicToolWithDB):
|
|
19
|
+
"""Basic tool for atmospheric data processing."""
|
|
20
|
+
|
|
21
|
+
meteo_data_handler = CaselessStrEnum(
|
|
22
|
+
values=["GDASDataHandler", "ECMWFDataHandler"],
|
|
23
|
+
default_value="ECMWFDataHandler",
|
|
24
|
+
help="Meteorological data handler name",
|
|
25
|
+
).tag(config=True)
|
|
26
|
+
|
|
27
|
+
observatory = Dict(
|
|
28
|
+
per_key_traits={
|
|
29
|
+
"name": Unicode(),
|
|
30
|
+
"version": Integer(),
|
|
31
|
+
},
|
|
32
|
+
default_value={
|
|
33
|
+
"name": "CTAO-NORTH",
|
|
34
|
+
"version": 1,
|
|
35
|
+
},
|
|
36
|
+
help="Observatory name and configuration version",
|
|
37
|
+
).tag(config=True)
|
|
38
|
+
|
|
39
|
+
timestamp = AstroTime(
|
|
40
|
+
allow_none=False,
|
|
41
|
+
help="A timestamp used to retrieve meteorological data. "
|
|
42
|
+
"Should correspond to a night time of the observatory. ",
|
|
43
|
+
).tag(config=True)
|
|
44
|
+
|
|
45
|
+
output_path = Path(
|
|
46
|
+
"./",
|
|
47
|
+
help="Path to the output folder where the atmospheric model files will be saved",
|
|
48
|
+
allow_none=False,
|
|
49
|
+
directory_ok=True,
|
|
50
|
+
file_ok=False,
|
|
51
|
+
).tag(config=True)
|
|
52
|
+
|
|
53
|
+
output_format = Unicode(
|
|
54
|
+
"ascii.ecsv",
|
|
55
|
+
help="Output files format",
|
|
56
|
+
allow_none=False,
|
|
57
|
+
).tag(config=True)
|
|
58
|
+
|
|
59
|
+
DEFAULT_METEO_COLUMNS = [
|
|
60
|
+
"Pressure",
|
|
61
|
+
"Altitude",
|
|
62
|
+
"Density",
|
|
63
|
+
"Temperature",
|
|
64
|
+
"Wind Speed",
|
|
65
|
+
"Wind Direction",
|
|
66
|
+
"Relative humidity",
|
|
67
|
+
"Exponential Density",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
classes = [MeteoDataHandler]
|
|
71
|
+
|
|
72
|
+
def setup(self):
|
|
73
|
+
"""Set up the tool."""
|
|
74
|
+
super().setup()
|
|
75
|
+
self.data_handler = MeteoDataHandler.from_name(
|
|
76
|
+
self.meteo_data_handler, parent=self
|
|
77
|
+
)
|
|
78
|
+
self._timestamp = datetime.fromisoformat(self.timestamp.iso)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Tool to upload atmospheric models to the CalibPipe DB."""
|
|
2
|
+
|
|
3
|
+
import astropy.units as u
|
|
4
|
+
import numpy as np
|
|
5
|
+
from astropy.table import QTable
|
|
6
|
+
from astropy.units import Quantity
|
|
7
|
+
from astropy.units.cds import ppm
|
|
8
|
+
|
|
9
|
+
# Internal imports
|
|
10
|
+
from calibpipe.atmosphere.atmosphere_containers import (
|
|
11
|
+
AtmosphericModelContainer,
|
|
12
|
+
MacobacContainer,
|
|
13
|
+
MolecularAtmosphericProfileContainer,
|
|
14
|
+
MolecularAtmosphericProfileMetaContainer,
|
|
15
|
+
MolecularDensityContainer,
|
|
16
|
+
RayleighExtinctionContainer,
|
|
17
|
+
)
|
|
18
|
+
from calibpipe.database.connections import CalibPipeDatabase
|
|
19
|
+
from calibpipe.database.interfaces import TableHandler
|
|
20
|
+
|
|
21
|
+
# CTA-related imports
|
|
22
|
+
from ctapipe.core.traits import AstroTime, Bool, Dict, Integer, Path, Unicode
|
|
23
|
+
|
|
24
|
+
from .basic_tool_with_db import BasicToolWithDB
|
|
25
|
+
|
|
26
|
+
u.add_enabled_units([ppm])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class UploadAtmosphericModel(BasicToolWithDB):
|
|
30
|
+
"""
|
|
31
|
+
Upload a (reference) atmospheric model to the calibpipe database.
|
|
32
|
+
|
|
33
|
+
For the time being the model consists of
|
|
34
|
+
- a molecular atmospheric profile;
|
|
35
|
+
- a molecular number density at 15km a.s.l.;
|
|
36
|
+
- a 12MACOBAC value;
|
|
37
|
+
- a Rayleigh extinction table - TBA.
|
|
38
|
+
The input data for the atmospheric profile is provided in ecsv data format.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name = Unicode("UploadAtmosphericModel")
|
|
43
|
+
description = "Upload an atmospheric model to the calibpipe database"
|
|
44
|
+
|
|
45
|
+
atmospheric_model = Dict(
|
|
46
|
+
per_key_traits={
|
|
47
|
+
"start": AstroTime(),
|
|
48
|
+
"stop": AstroTime(),
|
|
49
|
+
"version": Unicode(),
|
|
50
|
+
"current": Bool(),
|
|
51
|
+
"season": Unicode(),
|
|
52
|
+
"name_Observatory": Unicode(),
|
|
53
|
+
"version_Observatory": Integer(),
|
|
54
|
+
},
|
|
55
|
+
help="Atmospheric model metadata",
|
|
56
|
+
).tag(config=True)
|
|
57
|
+
|
|
58
|
+
macobac_data_path = Path(
|
|
59
|
+
help="Path to an ecsv file with macobac data that contains the 12-MACOBAC value and estimation date",
|
|
60
|
+
directory_ok=False,
|
|
61
|
+
allow_none=False,
|
|
62
|
+
).tag(config=True)
|
|
63
|
+
molecular_density_data_path = Path(
|
|
64
|
+
help="Path to an ecsv file that contains the molecular number density at 15km a.s.l. for a given atmospheric model",
|
|
65
|
+
directory_ok=False,
|
|
66
|
+
allow_none=False,
|
|
67
|
+
).tag(config=True)
|
|
68
|
+
|
|
69
|
+
molecular_atmospheric_profile = Dict(
|
|
70
|
+
per_key_traits={
|
|
71
|
+
"data_assimilation_system": Unicode(),
|
|
72
|
+
"dataset": Unicode(),
|
|
73
|
+
"description": Unicode(),
|
|
74
|
+
"data_path": Path(allow_none=False),
|
|
75
|
+
},
|
|
76
|
+
default_value={
|
|
77
|
+
"data_assimilation_system": "GDAS",
|
|
78
|
+
"dataset": "ds.083.2",
|
|
79
|
+
"description": "Test",
|
|
80
|
+
"data_path": "src/calibpipe/atmosphere/models/test.ecsv",
|
|
81
|
+
},
|
|
82
|
+
help="Molecular atmospheric profile data",
|
|
83
|
+
).tag(config=True)
|
|
84
|
+
|
|
85
|
+
rayleigh_extinction_data_path = Path(
|
|
86
|
+
help="Path to an ecsv file with rayleigh extinction profile data",
|
|
87
|
+
directory_ok=False,
|
|
88
|
+
allow_none=False,
|
|
89
|
+
).tag(config=True)
|
|
90
|
+
|
|
91
|
+
def setup(self):
|
|
92
|
+
"""Configure atmopsheric model container."""
|
|
93
|
+
super().setup()
|
|
94
|
+
self.am_container = AtmosphericModelContainer(**self.atmospheric_model)
|
|
95
|
+
if self.am_container.start is not None:
|
|
96
|
+
self.am_container.start = self.am_container.start.to_datetime()
|
|
97
|
+
if self.am_container.stop is not None:
|
|
98
|
+
self.am_container.stop = self.am_container.stop.to_datetime()
|
|
99
|
+
self.map_data_path = self.molecular_atmospheric_profile.pop("data_path", None)
|
|
100
|
+
self.map_meta_container = MolecularAtmosphericProfileMetaContainer(
|
|
101
|
+
**self.molecular_atmospheric_profile
|
|
102
|
+
)
|
|
103
|
+
self.map_meta_container.version = self.am_container.version
|
|
104
|
+
|
|
105
|
+
def start(self):
|
|
106
|
+
"""Fetch atmospheric tables and upload them to the DB."""
|
|
107
|
+
map_table = QTable.read(self.map_data_path)
|
|
108
|
+
map_container = MolecularAtmosphericProfileContainer(**dict(map_table.items()))
|
|
109
|
+
map_container.version = self.am_container.version
|
|
110
|
+
macobac_table = QTable.read(self.macobac_data_path)
|
|
111
|
+
macobac_container = MacobacContainer()
|
|
112
|
+
macobac_container.co2_concentration = macobac_table["co2_concentration"][0]
|
|
113
|
+
macobac_container.estimation_date = (
|
|
114
|
+
macobac_table["estimation_date"][0].to_datetime().date()
|
|
115
|
+
)
|
|
116
|
+
macobac_container.version = self.am_container.version
|
|
117
|
+
re_table = QTable.read(self.rayleigh_extinction_data_path)
|
|
118
|
+
wl_cols = [
|
|
119
|
+
wl
|
|
120
|
+
for wl in re_table.colnames
|
|
121
|
+
if wl != "altitude_min" and wl != "altitude_max"
|
|
122
|
+
]
|
|
123
|
+
wls = np.array([Quantity(wl).to_value(u.nm) for wl in wl_cols]) * u.nm
|
|
124
|
+
altitudes = re_table[["altitude_min", "altitude_max"]].to_pandas().values * u.km
|
|
125
|
+
aods = re_table[wl_cols].to_pandas().values * u.dimensionless_unscaled
|
|
126
|
+
re_container = RayleighExtinctionContainer(
|
|
127
|
+
wavelength=wls,
|
|
128
|
+
altitude=altitudes,
|
|
129
|
+
AOD=aods,
|
|
130
|
+
)
|
|
131
|
+
re_container.version = self.am_container.version
|
|
132
|
+
md_container = MolecularDensityContainer()
|
|
133
|
+
md_container.version = self.am_container.version
|
|
134
|
+
md_table = QTable.read(self.molecular_density_data_path)
|
|
135
|
+
md_container.density = md_table["density"][0]
|
|
136
|
+
md_container.season = md_table["season"][0]
|
|
137
|
+
|
|
138
|
+
containers = [
|
|
139
|
+
self.am_container,
|
|
140
|
+
self.map_meta_container,
|
|
141
|
+
map_container,
|
|
142
|
+
macobac_container,
|
|
143
|
+
re_container,
|
|
144
|
+
md_container,
|
|
145
|
+
]
|
|
146
|
+
with CalibPipeDatabase(
|
|
147
|
+
**self.database_configuration,
|
|
148
|
+
) as connection:
|
|
149
|
+
for container in containers:
|
|
150
|
+
table, insertion = TableHandler.get_database_table_insertion(
|
|
151
|
+
container,
|
|
152
|
+
)
|
|
153
|
+
if (container == self.am_container) and (self.am_container.current):
|
|
154
|
+
stmt = (
|
|
155
|
+
table.update()
|
|
156
|
+
.where(
|
|
157
|
+
(table.c.current)
|
|
158
|
+
& (table.c.season == self.am_container.season)
|
|
159
|
+
& (
|
|
160
|
+
table.c.name_Observatory
|
|
161
|
+
== self.am_container.name_Observatory
|
|
162
|
+
)
|
|
163
|
+
& (
|
|
164
|
+
table.c.version_Observatory
|
|
165
|
+
== self.am_container.version_Observatory
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
.values(current=False)
|
|
169
|
+
)
|
|
170
|
+
connection.execute(stmt)
|
|
171
|
+
TableHandler.insert_row_in_database(table, insertion, connection)
|
|
172
|
+
|
|
173
|
+
def finish(self):
|
|
174
|
+
"""Do nothing."""
|
|
175
|
+
self.log.info("Shutting down.")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def main():
|
|
179
|
+
"""Run the tool."""
|
|
180
|
+
tool = UploadAtmosphericModel()
|
|
181
|
+
tool.run()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# CTA-related imports # noqa: D100
|
|
2
|
+
from ctapipe.core import Tool
|
|
3
|
+
from ctapipe.core.traits import Bool, Dict, Integer, Unicode
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BasicToolWithDB(Tool):
|
|
7
|
+
"""Basic tool with database connection."""
|
|
8
|
+
|
|
9
|
+
name = Unicode("BasicToolWithDB")
|
|
10
|
+
description = "Basic tool with database connection."
|
|
11
|
+
|
|
12
|
+
database_configuration = Dict(
|
|
13
|
+
per_key_traits={
|
|
14
|
+
"user": Unicode(),
|
|
15
|
+
"password": Unicode(),
|
|
16
|
+
"database": Unicode(),
|
|
17
|
+
"host": Unicode(),
|
|
18
|
+
"port": Integer(allow_none=True),
|
|
19
|
+
"autocommit": Bool(),
|
|
20
|
+
},
|
|
21
|
+
default_value={
|
|
22
|
+
"user": "TEST_CALIBPIPE_DB_USER",
|
|
23
|
+
"password": "DUMMY_PSWRD",
|
|
24
|
+
"database": "TEST_CALIBPIPE_DB",
|
|
25
|
+
"host": "localhost",
|
|
26
|
+
"port": 5432,
|
|
27
|
+
"autocommit": True,
|
|
28
|
+
},
|
|
29
|
+
help="Database configuration",
|
|
30
|
+
).tag(config=True)
|
|
31
|
+
|
|
32
|
+
def setup(self):
|
|
33
|
+
"""Set up the database connection."""
|
|
34
|
+
if (
|
|
35
|
+
"database_configuration" in self.config
|
|
36
|
+
and "database_configuration" not in self.config[self.name]
|
|
37
|
+
):
|
|
38
|
+
self.database_configuration = self.config["database_configuration"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# noqa: D100
|
|
2
|
+
from ctapipe.core.traits import (
|
|
3
|
+
Dict,
|
|
4
|
+
Unicode,
|
|
5
|
+
)
|
|
6
|
+
from molecularprofiles.molecularprofiles import MolecularProfile
|
|
7
|
+
|
|
8
|
+
from ..core.exceptions import MissingInputDataError
|
|
9
|
+
from ..utils.observatory import Observatory
|
|
10
|
+
from .atmospheric_base_tool import AtmosphericBaseTool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CreateMolecularDensityProfile(AtmosphericBaseTool):
|
|
14
|
+
"""
|
|
15
|
+
Tool for creating a contemporary Molecular Density Profile (MDP).
|
|
16
|
+
|
|
17
|
+
This tool downloads and processes meteorological data from a specified data assimilation system
|
|
18
|
+
for a night, corresponding to the provided timestamp, and produces a molecular density profile.
|
|
19
|
+
This implementation follows the specifications outlined in UC-DPPS-CP-115.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name = Unicode("CreateMDP")
|
|
23
|
+
description = "Create a contemporary MDP"
|
|
24
|
+
aliases = Dict(
|
|
25
|
+
{
|
|
26
|
+
"timestamp": "CreateMDP.timestamp",
|
|
27
|
+
"output_path": "CreateMDP.output_path",
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def setup(self):
|
|
32
|
+
"""Parse configuration and setup the database connection and MeteoDataHandler."""
|
|
33
|
+
super().setup()
|
|
34
|
+
self.mdp_table = None
|
|
35
|
+
|
|
36
|
+
def start(self):
|
|
37
|
+
"""
|
|
38
|
+
Download meteorological data and create a molecular density profile.
|
|
39
|
+
|
|
40
|
+
This method performs the following operations:
|
|
41
|
+
1. Retrieves the observatory data from the database.
|
|
42
|
+
2. Calculates the astronomical night based on the observatory's coordinates and the provided timestamp.
|
|
43
|
+
3. Creates a data request for the calculated time frame and coordinates.
|
|
44
|
+
4. Attempts to fetch the meteorological data; raises an exception if unavailable.
|
|
45
|
+
5. Generates and saves the molecular density profile to the specified output path.
|
|
46
|
+
|
|
47
|
+
Raises
|
|
48
|
+
------
|
|
49
|
+
MissingInputDataError: If the required meteorological data is not available.
|
|
50
|
+
"""
|
|
51
|
+
observatory = Observatory.from_db(
|
|
52
|
+
self.database_configuration,
|
|
53
|
+
site=self.observatory["name"].upper(),
|
|
54
|
+
version=self.observatory["version"],
|
|
55
|
+
)
|
|
56
|
+
latitude, longitude = observatory.coordinates
|
|
57
|
+
dusk, dawn = observatory.get_astronomical_night(self._timestamp)
|
|
58
|
+
self.data_handler.create_request(
|
|
59
|
+
start=dusk, stop=dawn, latitude=latitude, longitude=longitude
|
|
60
|
+
)
|
|
61
|
+
data_status = self.data_handler.request_data()
|
|
62
|
+
if data_status:
|
|
63
|
+
raise MissingInputDataError(
|
|
64
|
+
f"Meteorologocal data from {self.meteo_data_handler} is not available."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
molecular_profile = MolecularProfile(
|
|
68
|
+
f"{self.data_handler.data_path}/merged_file.ecsv",
|
|
69
|
+
stat_columns=self.DEFAULT_METEO_COLUMNS,
|
|
70
|
+
)
|
|
71
|
+
molecular_profile.get_data()
|
|
72
|
+
self.mdp_table = molecular_profile.create_molecular_density_profile()
|
|
73
|
+
|
|
74
|
+
def finish(self):
|
|
75
|
+
"""Store the molecular density profile in the output file and perform cleanup."""
|
|
76
|
+
self.mdp_table.write(
|
|
77
|
+
f"{self.output_path}/contemporary_molecular_density_profile.{self.output_format}",
|
|
78
|
+
format=f"{self.output_format}",
|
|
79
|
+
)
|
|
80
|
+
self.log.info("Shutting down.")
|
|
81
|
+
self.data_handler.cleanup()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main():
|
|
85
|
+
"""Run the app."""
|
|
86
|
+
tool = CreateMolecularDensityProfile()
|
|
87
|
+
tool.run()
|