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.

Files changed (93) hide show
  1. calibpipe/__init__.py +5 -0
  2. calibpipe/_dev_version/__init__.py +9 -0
  3. calibpipe/_version.py +21 -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 +195 -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 +61 -0
  38. calibpipe/database/adapter/database_containers/atmosphere.py +199 -0
  39. calibpipe/database/adapter/database_containers/common_metadata.py +148 -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/version_control.py +17 -0
  44. calibpipe/database/connections/__init__.py +28 -0
  45. calibpipe/database/connections/calibpipe_database.py +60 -0
  46. calibpipe/database/connections/postgres_utils.py +97 -0
  47. calibpipe/database/connections/sql_connection.py +103 -0
  48. calibpipe/database/connections/user_confirmation.py +19 -0
  49. calibpipe/database/interfaces/__init__.py +71 -0
  50. calibpipe/database/interfaces/hashable_row_data.py +54 -0
  51. calibpipe/database/interfaces/queries.py +180 -0
  52. calibpipe/database/interfaces/sql_column_info.py +67 -0
  53. calibpipe/database/interfaces/sql_metadata.py +6 -0
  54. calibpipe/database/interfaces/sql_table_info.py +131 -0
  55. calibpipe/database/interfaces/table_handler.py +351 -0
  56. calibpipe/database/interfaces/types.py +96 -0
  57. calibpipe/tests/data/atmosphere/molecular_atmosphere/__init__.py +0 -0
  58. calibpipe/tests/data/atmosphere/molecular_atmosphere/contemporary_MDP.ecsv +34 -0
  59. calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.csv +852 -0
  60. calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.ecsv +23 -0
  61. calibpipe/tests/data/atmosphere/molecular_atmosphere/merged_file.ecsv +1082 -0
  62. calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_copernicus.ecsv +1082 -0
  63. calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_gdas.ecsv +66 -0
  64. calibpipe/tests/data/atmosphere/molecular_atmosphere/observatory_configurations.json +71 -0
  65. calibpipe/tests/data/utils/__init__.py +0 -0
  66. calibpipe/tests/data/utils/meteo_data_winter_and_summer.ecsv +12992 -0
  67. calibpipe/tests/unittests/atmosphere/astral_testing.py +107 -0
  68. calibpipe/tests/unittests/atmosphere/test_meteo_data_handler.py +775 -0
  69. calibpipe/tests/unittests/atmosphere/test_molecular_atmosphere.py +327 -0
  70. calibpipe/tests/unittests/database/test_table_handler.py +66 -0
  71. calibpipe/tests/unittests/database/test_types.py +38 -0
  72. calibpipe/tests/unittests/test_bootstrap_db.py +79 -0
  73. calibpipe/tests/unittests/utils/test_observatory.py +309 -0
  74. calibpipe/tools/atmospheric_base_tool.py +78 -0
  75. calibpipe/tools/atmospheric_model_db_loader.py +181 -0
  76. calibpipe/tools/basic_tool_with_db.py +38 -0
  77. calibpipe/tools/contemporary_mdp_producer.py +87 -0
  78. calibpipe/tools/init_db.py +37 -0
  79. calibpipe/tools/macobac_calculator.py +82 -0
  80. calibpipe/tools/molecular_atmospheric_model_producer.py +197 -0
  81. calibpipe/tools/observatory_data_db_loader.py +71 -0
  82. calibpipe/tools/reference_atmospheric_model_selector.py +201 -0
  83. calibpipe/utils/__init__.py +10 -0
  84. calibpipe/utils/observatory.py +486 -0
  85. calibpipe/utils/observatory_containers.py +26 -0
  86. calibpipe/version.py +24 -0
  87. ctao_calibpipe-0.1.0.dist-info/METADATA +86 -0
  88. ctao_calibpipe-0.1.0.dist-info/RECORD +93 -0
  89. ctao_calibpipe-0.1.0.dist-info/WHEEL +5 -0
  90. ctao_calibpipe-0.1.0.dist-info/entry_points.txt +8 -0
  91. ctao_calibpipe-0.1.0.dist-info/licenses/AUTHORS.md +13 -0
  92. ctao_calibpipe-0.1.0.dist-info/licenses/LICENSE +21 -0
  93. 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()