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.
Files changed (105) hide show
  1. calibpipe/__init__.py +5 -0
  2. calibpipe/_dev_version/__init__.py +9 -0
  3. calibpipe/_version.py +34 -0
  4. calibpipe/atmosphere/__init__.py +1 -0
  5. calibpipe/atmosphere/atmosphere_containers.py +109 -0
  6. calibpipe/atmosphere/meteo_data_handlers.py +485 -0
  7. calibpipe/atmosphere/models/README.md +14 -0
  8. calibpipe/atmosphere/models/__init__.py +1 -0
  9. calibpipe/atmosphere/models/macobac.ecsv +23 -0
  10. calibpipe/atmosphere/models/reference_MDPs/__init__.py +1 -0
  11. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_intermediate.ecsv +8 -0
  12. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_summer.ecsv +8 -0
  13. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_winter.ecsv +8 -0
  14. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_summer.ecsv +8 -0
  15. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_winter.ecsv +8 -0
  16. calibpipe/atmosphere/models/reference_atmospheres/__init__.py +1 -0
  17. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_intermediate.ecsv +73 -0
  18. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_summer.ecsv +73 -0
  19. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_winter.ecsv +73 -0
  20. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_summer.ecsv +73 -0
  21. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_winter.ecsv +73 -0
  22. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/__init__.py +1 -0
  23. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_intermediate.ecsv +857 -0
  24. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_summer.ecsv +857 -0
  25. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_winter.ecsv +857 -0
  26. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_summer.ecsv +857 -0
  27. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_winter.ecsv +857 -0
  28. calibpipe/atmosphere/templates/request_templates/__init__.py +1 -0
  29. calibpipe/atmosphere/templates/request_templates/copernicus.json +11 -0
  30. calibpipe/atmosphere/templates/request_templates/gdas.json +12 -0
  31. calibpipe/core/__init__.py +39 -0
  32. calibpipe/core/common_metadata_containers.py +198 -0
  33. calibpipe/core/exceptions.py +87 -0
  34. calibpipe/database/__init__.py +24 -0
  35. calibpipe/database/adapter/__init__.py +23 -0
  36. calibpipe/database/adapter/adapter.py +80 -0
  37. calibpipe/database/adapter/database_containers/__init__.py +63 -0
  38. calibpipe/database/adapter/database_containers/atmosphere.py +199 -0
  39. calibpipe/database/adapter/database_containers/common_metadata.py +150 -0
  40. calibpipe/database/adapter/database_containers/container_map.py +59 -0
  41. calibpipe/database/adapter/database_containers/observatory.py +61 -0
  42. calibpipe/database/adapter/database_containers/table_version_manager.py +39 -0
  43. calibpipe/database/adapter/database_containers/throughput.py +30 -0
  44. calibpipe/database/adapter/database_containers/version_control.py +17 -0
  45. calibpipe/database/connections/__init__.py +28 -0
  46. calibpipe/database/connections/calibpipe_database.py +60 -0
  47. calibpipe/database/connections/postgres_utils.py +97 -0
  48. calibpipe/database/connections/sql_connection.py +103 -0
  49. calibpipe/database/connections/user_confirmation.py +19 -0
  50. calibpipe/database/interfaces/__init__.py +71 -0
  51. calibpipe/database/interfaces/hashable_row_data.py +54 -0
  52. calibpipe/database/interfaces/queries.py +180 -0
  53. calibpipe/database/interfaces/sql_column_info.py +67 -0
  54. calibpipe/database/interfaces/sql_metadata.py +6 -0
  55. calibpipe/database/interfaces/sql_table_info.py +131 -0
  56. calibpipe/database/interfaces/table_handler.py +333 -0
  57. calibpipe/database/interfaces/types.py +96 -0
  58. calibpipe/telescope/throughput/containers.py +66 -0
  59. calibpipe/tests/conftest.py +274 -0
  60. calibpipe/tests/data/atmosphere/molecular_atmosphere/__init__.py +0 -0
  61. calibpipe/tests/data/atmosphere/molecular_atmosphere/contemporary_MDP.ecsv +34 -0
  62. calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.csv +852 -0
  63. calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.ecsv +23 -0
  64. calibpipe/tests/data/atmosphere/molecular_atmosphere/merged_file.ecsv +1082 -0
  65. calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_copernicus.ecsv +1082 -0
  66. calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_gdas.ecsv +66 -0
  67. calibpipe/tests/data/atmosphere/molecular_atmosphere/observatory_configurations.json +71 -0
  68. calibpipe/tests/data/utils/__init__.py +0 -0
  69. calibpipe/tests/data/utils/meteo_data_winter_and_summer.ecsv +12992 -0
  70. calibpipe/tests/test_conftest_data.py +200 -0
  71. calibpipe/tests/unittests/array/test_cross_calibration.py +412 -0
  72. calibpipe/tests/unittests/atmosphere/astral_testing.py +107 -0
  73. calibpipe/tests/unittests/atmosphere/test_meteo_data_handler.py +775 -0
  74. calibpipe/tests/unittests/atmosphere/test_molecular_atmosphere.py +327 -0
  75. calibpipe/tests/unittests/database/test_table_handler.py +163 -0
  76. calibpipe/tests/unittests/database/test_types.py +38 -0
  77. calibpipe/tests/unittests/telescope/camera/test_calculate_camcalib_coefficients.py +456 -0
  78. calibpipe/tests/unittests/telescope/camera/test_produce_camcalib_test_data.py +37 -0
  79. calibpipe/tests/unittests/telescope/throughput/test_muon_throughput_calibrator.py +693 -0
  80. calibpipe/tests/unittests/test_bootstrap_db.py +79 -0
  81. calibpipe/tests/unittests/utils/test_observatory.py +309 -0
  82. calibpipe/tools/atmospheric_base_tool.py +78 -0
  83. calibpipe/tools/atmospheric_model_db_loader.py +181 -0
  84. calibpipe/tools/basic_tool_with_db.py +38 -0
  85. calibpipe/tools/camcalib_test_data.py +374 -0
  86. calibpipe/tools/camera_calibrator.py +462 -0
  87. calibpipe/tools/contemporary_mdp_producer.py +87 -0
  88. calibpipe/tools/init_db.py +37 -0
  89. calibpipe/tools/macobac_calculator.py +82 -0
  90. calibpipe/tools/molecular_atmospheric_model_producer.py +197 -0
  91. calibpipe/tools/muon_throughput_calculator.py +219 -0
  92. calibpipe/tools/observatory_data_db_loader.py +71 -0
  93. calibpipe/tools/reference_atmospheric_model_selector.py +201 -0
  94. calibpipe/tools/telescope_cross_calibration_calculator.py +721 -0
  95. calibpipe/utils/__init__.py +10 -0
  96. calibpipe/utils/observatory.py +486 -0
  97. calibpipe/utils/observatory_containers.py +26 -0
  98. calibpipe/version.py +24 -0
  99. ctao_calibpipe-0.3.0rc2.dist-info/METADATA +92 -0
  100. ctao_calibpipe-0.3.0rc2.dist-info/RECORD +105 -0
  101. ctao_calibpipe-0.3.0rc2.dist-info/WHEEL +5 -0
  102. ctao_calibpipe-0.3.0rc2.dist-info/entry_points.txt +12 -0
  103. ctao_calibpipe-0.3.0rc2.dist-info/licenses/AUTHORS.md +13 -0
  104. ctao_calibpipe-0.3.0rc2.dist-info/licenses/LICENSE +21 -0
  105. ctao_calibpipe-0.3.0rc2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,79 @@
1
+ from copy import deepcopy
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+ import yaml
6
+ from calibpipe.tools.atmospheric_model_db_loader import UploadAtmosphericModel
7
+ from calibpipe.tools.init_db import CalibPipeDatabaseInitialization
8
+ from calibpipe.tools.observatory_data_db_loader import UploadObservatoryData
9
+ from ctapipe.core import run_tool
10
+ from traitlets.config import Config
11
+
12
+
13
+ @pytest.mark.db()
14
+ @pytest.mark.order(1)
15
+ def test_init_database():
16
+ """
17
+ Fixture to initialize the database and upload required data.
18
+ This runs before any other database-related tests.
19
+ """
20
+ # Paths to configuration files
21
+ db_config_path = (
22
+ Path(__file__).parent.parent.parent.parent.parent
23
+ / "docs/source/user_guide/utils/configuration/db_config.yaml"
24
+ )
25
+
26
+ # Initialize the database
27
+ with open(db_config_path) as file:
28
+ db_config = Config(yaml.load(file, Loader=yaml.SafeLoader))
29
+ tool = CalibPipeDatabaseInitialization(config=db_config)
30
+ run_tool(tool)
31
+
32
+
33
+ @pytest.mark.db()
34
+ @pytest.mark.order(2)
35
+ def test_upload_observatory():
36
+ db_config_path = (
37
+ Path(__file__).parent.parent.parent.parent.parent
38
+ / "docs/source/user_guide/utils/configuration/db_config.yaml"
39
+ )
40
+ with open(db_config_path) as file:
41
+ db_config = Config(yaml.load(file, Loader=yaml.SafeLoader))
42
+
43
+ observatory_data_config_path = (
44
+ Path(__file__).parent.parent.parent.parent.parent
45
+ / "docs/source/user_guide/utils/configuration/upload_observatory_data_db.yaml"
46
+ )
47
+ # Upload observatory data
48
+ config = deepcopy(db_config)
49
+ with open(observatory_data_config_path) as file:
50
+ observatory_data_config = Config(yaml.load(file, Loader=yaml.SafeLoader))
51
+ config.update(observatory_data_config)
52
+ tool = UploadObservatoryData(config=config)
53
+ run_tool(tool)
54
+
55
+
56
+ @pytest.mark.db()
57
+ @pytest.mark.order(3)
58
+ def test_upload_atmospheric_models():
59
+ db_config_path = (
60
+ Path(__file__).parent.parent.parent.parent.parent
61
+ / "docs/source/user_guide/utils/configuration/db_config.yaml"
62
+ )
63
+
64
+ with open(db_config_path) as file:
65
+ db_config = Config(yaml.load(file, Loader=yaml.SafeLoader))
66
+
67
+ atmospheric_model_configs = list(
68
+ Path(__file__).parent.parent.parent.parent.parent.glob(
69
+ "docs/source/user_guide/utils/configuration/upload_atmospheric*.yaml"
70
+ )
71
+ )
72
+ # Upload atmospheric model data
73
+ for config_path in atmospheric_model_configs:
74
+ config = deepcopy(db_config)
75
+ with open(config_path) as file:
76
+ atmospheric_model_config = Config(yaml.load(file, Loader=yaml.SafeLoader))
77
+ config.update(atmospheric_model_config)
78
+ tool = UploadAtmosphericModel(config=config)
79
+ run_tool(tool)
@@ -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"]