ctao-calibpipe 0.1.0rc7__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.0rc7.dist-info/METADATA +86 -0
  88. ctao_calibpipe-0.1.0rc7.dist-info/RECORD +93 -0
  89. ctao_calibpipe-0.1.0rc7.dist-info/WHEEL +5 -0
  90. ctao_calibpipe-0.1.0rc7.dist-info/entry_points.txt +8 -0
  91. ctao_calibpipe-0.1.0rc7.dist-info/licenses/AUTHORS.md +13 -0
  92. ctao_calibpipe-0.1.0rc7.dist-info/licenses/LICENSE +21 -0
  93. ctao_calibpipe-0.1.0rc7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,37 @@
1
+ # CTA-related imports # noqa: D100
2
+ from ctapipe.core.traits import Unicode
3
+
4
+ from ..database.adapter.database_containers import ContainerMap
5
+ from ..database.interfaces import TableHandler
6
+
7
+ # Internal imports
8
+ from .basic_tool_with_db import BasicToolWithDB
9
+
10
+
11
+ class CalibPipeDatabaseInitialization(BasicToolWithDB):
12
+ """Tool to create empty data and metadata tables in the CalibPipe DB."""
13
+
14
+ name = Unicode("CalibPipeDatabaseInitialization")
15
+ description = "Populate an empty databased with empty tables."
16
+
17
+ def setup(self):
18
+ """Parse configuration, setup the database connection and fetch CalibPipe containers."""
19
+ super().setup()
20
+ self.containers = ContainerMap.get_cp_containers()
21
+
22
+ def start(self):
23
+ """Create tables in the database."""
24
+ TableHandler.prepare_db_tables(self.containers, self.database_configuration)
25
+
26
+ def finish(self):
27
+ """Log created tables."""
28
+ self.log.info(
29
+ "Data tables for %s was created and uploaded to CalibPipe DB",
30
+ [_.__name__ for _ in self.containers],
31
+ )
32
+
33
+
34
+ def main():
35
+ """Run the app."""
36
+ tool = CalibPipeDatabaseInitialization()
37
+ tool.run()
@@ -0,0 +1,82 @@
1
+ """Calculate the average CO2 concentration of the last 12 months (12-MACOBAC)."""
2
+
3
+ # Python built-in imports
4
+ from datetime import datetime, timezone
5
+
6
+ # Third-party imports
7
+ import astropy.units as u
8
+ import numpy as np
9
+ from astropy.table import QTable, Table
10
+ from astropy.time import Time
11
+ from astropy.units.cds import ppm
12
+
13
+ # CTA-related imports
14
+ from ctapipe.core import Tool
15
+ from ctapipe.core.traits import Path, Unicode
16
+
17
+ from ..atmosphere.atmosphere_containers import MacobacContainer
18
+
19
+ # Internal imports
20
+ from ..atmosphere.meteo_data_handlers import CO2DataHandler
21
+
22
+
23
+ class CalculateMACOBAC(Tool):
24
+ """Download Keeling curve data and calculate the average CO2 concentration of the past 12 months."""
25
+
26
+ name = Unicode("CalculateMACOBAC")
27
+ description = "Download Keeling curve data and calculate average CO2 concentration of the past 12 months."
28
+
29
+ output_file = Path(
30
+ "macobac.ecsv", help="Output ecsv file where macobac container will be written"
31
+ ).tag(config=True)
32
+
33
+ classes = [CO2DataHandler]
34
+
35
+ def setup(self):
36
+ """Create CO2DataHandler."""
37
+ u.add_enabled_units([ppm])
38
+ self.data_handler = CO2DataHandler(parent=self)
39
+ self.macobac12_table = None
40
+
41
+ def start(self):
42
+ """Request meteorological data from Scripps server and compute 12-MACOBAC."""
43
+ self.data_handler.request_data()
44
+ macobac_table = Table.read(
45
+ f"{self.data_handler.data_path}/macobac.csv",
46
+ comment='"',
47
+ skipinitialspace=True,
48
+ format="pandas.csv",
49
+ )
50
+ mask = macobac_table["CO2"].value != "-99.99"
51
+ co2_values = macobac_table[mask][::-1][0:12]["CO2"].data
52
+ macobac12 = np.mean(co2_values.data.astype(float)) * ppm
53
+ self.log.debug(
54
+ "CO2 average atmospheric concentration for the previous 12 months: %f",
55
+ macobac12,
56
+ )
57
+ macobac12_container = MacobacContainer(
58
+ co2_concentration=macobac12,
59
+ estimation_date=Time(
60
+ str(datetime.now(timezone.utc).date()), out_subfmt="date"
61
+ ),
62
+ )
63
+ self.macobac12_table = QTable(
64
+ names=macobac12_container.keys(),
65
+ rows=[macobac12_container.values()],
66
+ )
67
+
68
+ def finish(self):
69
+ """Store results and perform the cleanup."""
70
+ self.log.info("Storing the results and performing the cleanup.")
71
+ self.macobac12_table.write(
72
+ self.output_file,
73
+ format="ascii.ecsv",
74
+ serialize_method={"estimation_date": "formatted_value"},
75
+ )
76
+ self.data_handler.cleanup()
77
+
78
+
79
+ def main():
80
+ """Run the app."""
81
+ tool = CalculateMACOBAC()
82
+ tool.run()
@@ -0,0 +1,197 @@
1
+ # noqa: D100
2
+
3
+ import astropy.units as u
4
+ import numpy as np
5
+ from astropy.table import Table
6
+
7
+ # CTA-related imports
8
+ from ctapipe.core.traits import (
9
+ Dict,
10
+ Path,
11
+ Unicode,
12
+ )
13
+ from molecularprofiles.molecularprofiles import MolecularProfile
14
+
15
+ from ..atmosphere.atmosphere_containers import (
16
+ AtmosphericModelContainer,
17
+ MolecularAtmosphericProfileContainer,
18
+ MolecularDensityContainer,
19
+ )
20
+
21
+ # Internal imports
22
+ from ..core.exceptions import (
23
+ CorruptedInputDataError,
24
+ MissingInputDataError,
25
+ )
26
+ from ..database.connections import CalibPipeDatabase
27
+ from ..database.interfaces import TableHandler
28
+ from ..utils.observatory import Observatory
29
+ from .atmospheric_base_tool import AtmosphericBaseTool
30
+
31
+
32
+ class CreateMolecularAtmosphericModel(AtmosphericBaseTool):
33
+ """
34
+ Create an atmospheric model to be used as input for tailored MC simulations.
35
+
36
+ The model consists of:
37
+ - An atmospheric profile.
38
+ - A Rayleigh extinction table.
39
+
40
+ The output data is provided in ECSV data format.
41
+
42
+ Raises
43
+ ------
44
+ CorruptedInputDataError
45
+ If the MACOBAC12 table does not contain exactly one row.
46
+ MissingInputDataError
47
+ If the requested atmospheric DAS data is not available.
48
+ """
49
+
50
+ name = Unicode("CreateAtmosphericModel")
51
+ description = "Create an atmospheric profile"
52
+ aliases = Dict(
53
+ {"macobac12-table-path": "CreateMolecularAtmosphericModel.macobac12_table_path"}
54
+ )
55
+
56
+ macobac12_table_path = Path(
57
+ allow_none=False,
58
+ directory_ok=False,
59
+ default_value="macobac.ecsv",
60
+ help="Path to the MACOBAC12 table.",
61
+ ).tag(config=True)
62
+
63
+ def setup(self):
64
+ """
65
+ Update configuration and set up the MeteoDataHandler.
66
+
67
+ Raises
68
+ ------
69
+ CorruptedInputDataError
70
+ If the MACOBAC12 table does not contain exactly one row.
71
+ """
72
+ super().setup()
73
+
74
+ self.macobac12_table = Table.read(self.macobac12_table_path)
75
+ if len(self.macobac12_table) != 1:
76
+ raise CorruptedInputDataError(
77
+ "The MACOBAC12 table should contain exactly one row."
78
+ )
79
+ self.contemporary_atmospheric_profile = None
80
+ self.contemporary_rayleigh_extinction_profile = None
81
+
82
+ def start(self):
83
+ """
84
+ Produce molecular atmopsheric model.
85
+
86
+ This method performs the following steps:
87
+ 1. Retrieves necessary atmospheric data from the specified
88
+ data assimilation system (usually ECMWF).
89
+ 2. Combines the retrieved data with the provided 12-MACOBAC
90
+ and stored reference atmospheric model
91
+ to create a contemporary atmospheric profile and a Rayleigh extinction table.
92
+
93
+ Raises
94
+ ------
95
+ MissingInputDataError
96
+ If there is an error retrieving the necessary atmospheric data.
97
+ """
98
+ observatory = Observatory.from_db(
99
+ self.database_configuration,
100
+ site=self.observatory["name"].upper(),
101
+ version=self.observatory["version"],
102
+ )
103
+
104
+ latitude, longitude = observatory.coordinates
105
+ dusk, dawn = observatory.get_astronomical_night(self._timestamp)
106
+ self.data_handler.create_request(
107
+ start=dusk, stop=dawn, latitude=latitude, longitude=longitude
108
+ )
109
+ data_status = self.data_handler.request_data()
110
+ if data_status == 0:
111
+ co2_concentration = self.macobac12_table["co2_concentration"][0]
112
+ molecular_profile = MolecularProfile(
113
+ f"{self.data_handler.data_path}/merged_file.ecsv",
114
+ stat_columns=self.DEFAULT_METEO_COLUMNS,
115
+ )
116
+ molecular_profile.get_data()
117
+ # compute the molecular density profile
118
+ # in order to select a reference atmospheric model
119
+ # for upper atmosphere
120
+ site = observatory.name
121
+ with CalibPipeDatabase(
122
+ **self.database_configuration,
123
+ ) as connection:
124
+ atmospheric_model_table = TableHandler.read_table_from_database(
125
+ AtmosphericModelContainer,
126
+ connection,
127
+ condition=f"(c.current == True) & (c.name_Observatory == '{site}')",
128
+ )
129
+ reference_density_table = TableHandler.read_table_from_database(
130
+ MolecularDensityContainer,
131
+ connection,
132
+ condition=f"c.version.in_({list(atmospheric_model_table['version'].data)})",
133
+ )
134
+ contemporary_mdp = molecular_profile.create_molecular_density_profile()
135
+ number_density_at_15km = contemporary_mdp[
136
+ contemporary_mdp["altitude"] == 15000 * u.m
137
+ ]["number density"].quantity[0]
138
+ _, _, version = min(
139
+ reference_density_table,
140
+ key=lambda x: abs(
141
+ x["density"]
142
+ - number_density_at_15km # FIXME: MDP table should have "number density" column instead of "density"
143
+ ),
144
+ )
145
+ with CalibPipeDatabase(
146
+ **self.database_configuration,
147
+ ) as connection:
148
+ reference_profile_table = TableHandler.read_table_from_database(
149
+ MolecularAtmosphericProfileContainer,
150
+ connection,
151
+ condition=f'c.version == "{version}"',
152
+ )
153
+ reference_profile_table.remove_columns(["version"])
154
+ reference_profile = Table(
155
+ [
156
+ np.squeeze(reference_profile_table[col])
157
+ for col in reference_profile_table.columns
158
+ ],
159
+ names=reference_profile_table.columns,
160
+ )
161
+
162
+ self.contemporary_atmospheric_profile = (
163
+ molecular_profile.create_atmospheric_profile(
164
+ co2_concentration=co2_concentration,
165
+ reference_atmosphere=reference_profile,
166
+ )
167
+ )
168
+ self.contemporary_rayleigh_extinction_profile = (
169
+ molecular_profile.create_rayleigh_extinction_profile(
170
+ co2_concentration=co2_concentration,
171
+ reference_atmosphere=reference_profile,
172
+ )
173
+ )
174
+ else:
175
+ raise MissingInputDataError(
176
+ "Contemporary meteorological data is not available. "
177
+ "Please check the configuration and/or try again later."
178
+ )
179
+
180
+ def finish(self):
181
+ """Save the results and perform cleanup."""
182
+ self.contemporary_atmospheric_profile.write(
183
+ f"{self.output_path}/contemporary_atmospheric_profile.{self.output_format}",
184
+ format=f"{self.output_format}",
185
+ )
186
+ self.contemporary_rayleigh_extinction_profile.write(
187
+ f"{self.output_path}/contemporary_rayleigh_extinction_profile.{self.output_format}",
188
+ format=f"{self.output_format}",
189
+ )
190
+ self.log.info("Shutting down.")
191
+ self.data_handler.cleanup()
192
+
193
+
194
+ def main():
195
+ """Run the tool."""
196
+ tool = CreateMolecularAtmosphericModel()
197
+ tool.run()
@@ -0,0 +1,71 @@
1
+ import sqlalchemy as sa # noqa: D100
2
+
3
+ # Internal imports
4
+ from calibpipe.database.connections import CalibPipeDatabase
5
+ from calibpipe.database.interfaces import TableHandler
6
+ from calibpipe.utils.observatory import Observatory
7
+
8
+ # CTA-related imports
9
+ from ctapipe.core.traits import List, Unicode
10
+ from traitlets.config import Config
11
+
12
+ from .basic_tool_with_db import BasicToolWithDB
13
+
14
+
15
+ class UploadObservatoryData(BasicToolWithDB):
16
+ """Upload observatory data to the calibpipe database."""
17
+
18
+ name = Unicode("Upload observatory data")
19
+ description = "Upload observatory data to the calibpipe database."
20
+
21
+ observatories = List(help="List of observatories configurations", minlen=1).tag(
22
+ config=True
23
+ )
24
+
25
+ classes = [
26
+ Observatory,
27
+ ]
28
+
29
+ def setup(self):
30
+ """Create Observatory objects from the configuration and register database configuration."""
31
+ super().setup()
32
+ self._observatories = [
33
+ Observatory(config=Config(key))
34
+ for key in self.config["UploadObservatoryData"]["observatories"]
35
+ ]
36
+
37
+ def start(self):
38
+ """Create the database tables and insert the data."""
39
+ containers = [
40
+ container
41
+ for observatory in self._observatories
42
+ for container in observatory.containers
43
+ ]
44
+ # Check if the tables exist, if not create them
45
+ with CalibPipeDatabase(
46
+ **self.database_configuration,
47
+ ) as connection:
48
+ for container in containers:
49
+ table, insertion = TableHandler.get_database_table_insertion(
50
+ container,
51
+ )
52
+ if not sa.inspect(connection.engine).has_table(table.name):
53
+ table.create(bind=connection.engine)
54
+ # Insert the data
55
+ with CalibPipeDatabase(
56
+ **self.database_configuration,
57
+ ) as connection:
58
+ for container in containers:
59
+ table, insertion = TableHandler.get_database_table_insertion(
60
+ container,
61
+ )
62
+ TableHandler.insert_row_in_database(table, insertion, connection)
63
+
64
+ def finish(self):
65
+ """No finishing actions needed."""
66
+
67
+
68
+ def main():
69
+ """Run the app."""
70
+ tool = UploadObservatoryData()
71
+ tool.run()
@@ -0,0 +1,201 @@
1
+ import astropy.units as u # noqa: D100
2
+ import numpy as np
3
+ from astropy.table import Table
4
+ from ctapipe.core.traits import (
5
+ Dict,
6
+ Unicode,
7
+ )
8
+ from molecularprofiles.molecularprofiles import MolecularProfile
9
+
10
+ from ..atmosphere.atmosphere_containers import (
11
+ AtmosphericModelContainer,
12
+ MolecularAtmosphericProfileContainer,
13
+ MolecularDensityContainer,
14
+ RayleighExtinctionContainer,
15
+ SelectedAtmosphericModelContainer,
16
+ )
17
+ from ..core.exceptions import IntermittentError
18
+ from ..database.connections import CalibPipeDatabase
19
+ from ..database.interfaces import TableHandler
20
+ from ..utils.observatory import Observatory, SeasonAlias
21
+ from .atmospheric_base_tool import AtmosphericBaseTool
22
+
23
+
24
+ class SelectMolecularAtmosphericModel(AtmosphericBaseTool):
25
+ """Select a reference molecular atmospheric model."""
26
+
27
+ name = Unicode("SelectMolecularAtmosphericModel")
28
+ description = (
29
+ "Select a reference Molecular Atmospheric Model "
30
+ "based on the molecular number density at 15km a.s.l. "
31
+ "computed from contemporary meteorological data."
32
+ )
33
+ aliases = Dict(
34
+ {
35
+ "timestamp": "SelectMolecularAtmosphericModel.timestamp",
36
+ "output_path": "SelectMolecularAtmosphericModel.output_path",
37
+ }
38
+ )
39
+
40
+ def setup(self):
41
+ """Parse configuration and setup the database connection and MeteoDataHandler."""
42
+ super().setup()
43
+ self.selected_model_container = None
44
+ self.failed_to_retrieve_meteo_data = True
45
+
46
+ def start(self):
47
+ """
48
+ Download meteorological data and select a reference atmospheric model.
49
+
50
+ This method performs the following operations:
51
+ 1. Retrieves the observatory data from the database.
52
+ 2. Calculates the astronomical night based on the observatory's coordinates and the provided timestamp.
53
+ 3. Creates a data request for the calculated time frame and coordinates.
54
+ 4. Attempts to fetch the meteorological data.
55
+ 5a. If meteorological data is not available, select a reference model based on the date.
56
+ 5b. If meteorological data is available, select a reference model based on the molecular number density at 15km a.s.l.
57
+ """
58
+ observatory = Observatory.from_db(
59
+ self.database_configuration,
60
+ site=self.observatory["name"].upper(),
61
+ version=self.observatory["version"],
62
+ )
63
+ site = observatory.name
64
+ with CalibPipeDatabase(
65
+ **self.database_configuration,
66
+ ) as connection:
67
+ atmospheric_model_table = TableHandler.read_table_from_database(
68
+ AtmosphericModelContainer,
69
+ connection,
70
+ condition=f"(c.current == True) & (c.name_Observatory == '{site}')",
71
+ )
72
+ reference_density_table = TableHandler.read_table_from_database(
73
+ MolecularDensityContainer,
74
+ connection,
75
+ condition=f"c.version.in_({list(atmospheric_model_table['version'].data)})",
76
+ )
77
+
78
+ latitude, longitude = observatory.coordinates
79
+ dusk, dawn = observatory.get_astronomical_night(self._timestamp)
80
+ self.data_handler.create_request(
81
+ start=dusk, stop=dawn, latitude=latitude, longitude=longitude
82
+ )
83
+ self.failed_to_retrieve_meteo_data = self.data_handler.request_data()
84
+ if self.failed_to_retrieve_meteo_data:
85
+ season = observatory.get_season_from_timestamp(self._timestamp)
86
+ version = -1
87
+ else:
88
+ molecular_profile = MolecularProfile(
89
+ f"{self.data_handler.data_path}/merged_file.ecsv",
90
+ stat_columns=self.DEFAULT_METEO_COLUMNS,
91
+ )
92
+ molecular_profile.get_data()
93
+ contemporary_mdp = molecular_profile.create_molecular_density_profile()
94
+ number_density_at_15km = contemporary_mdp[
95
+ contemporary_mdp["altitude"] == 15000 * u.m
96
+ ]["number density"].quantity[0]
97
+ season, _, version = min(
98
+ reference_density_table,
99
+ key=lambda x: abs(
100
+ x["density"]
101
+ - number_density_at_15km # FIXME: MDP table should have "number density" column instead of "density"
102
+ ),
103
+ )
104
+
105
+ selected_season_table = atmospheric_model_table[
106
+ (atmospheric_model_table["season"] == season)
107
+ ]
108
+ if version == -1:
109
+ version = selected_season_table[(selected_season_table["current"])][
110
+ "version"
111
+ ][0]
112
+
113
+ self.selected_model_container = SelectedAtmosphericModelContainer(
114
+ date=self._timestamp.date(),
115
+ version=version,
116
+ season=SeasonAlias[season.upper()].value,
117
+ site=observatory.name,
118
+ provenance=self.meteo_data_handler,
119
+ )
120
+
121
+ def finish(self):
122
+ """Store the selected atmospheric model, and perform cleanup.
123
+
124
+ Raises
125
+ ------
126
+ IntermittentError
127
+ In case of missing meteorological data.
128
+ """
129
+ self.log.info(
130
+ "Selected atmospheric model container:\n%s", self.selected_model_container
131
+ )
132
+
133
+ with CalibPipeDatabase(
134
+ **self.database_configuration,
135
+ ) as connection:
136
+ table, insertion = TableHandler.get_database_table_insertion(
137
+ self.selected_model_container,
138
+ )
139
+ TableHandler.insert_row_in_database(table, insertion, connection)
140
+ selected_atmospheric_table = TableHandler.read_table_from_database(
141
+ MolecularAtmosphericProfileContainer,
142
+ connection,
143
+ condition=f"c.version == \"{self.selected_model_container['version']}\"",
144
+ )
145
+ selected_rayleigh_extinction_table = TableHandler.read_table_from_database(
146
+ RayleighExtinctionContainer,
147
+ connection,
148
+ condition=f"c.version == \"{self.selected_model_container['version']}\"",
149
+ )
150
+ selected_atmospheric_table.remove_column("version")
151
+ selected_atmospheric_profile = Table(
152
+ [
153
+ np.squeeze(selected_atmospheric_table[col])
154
+ for col in selected_atmospheric_table.columns
155
+ ],
156
+ names=selected_atmospheric_table.columns,
157
+ )
158
+ selected_atmospheric_profile.write(
159
+ f"{self.output_path}/selected_atmospheric_profile.{self.output_format}",
160
+ format=f"{self.output_format}",
161
+ )
162
+ selected_rayleigh_extinction_table.remove_column("version")
163
+
164
+ rayleigh_extinction_col_names = ["altitude_min", "altitude_max"]
165
+ rayleigh_extinction_col_names.extend(
166
+ [
167
+ f"{name:.1f}"
168
+ for name in selected_rayleigh_extinction_table["wavelength"].squeeze()
169
+ ]
170
+ )
171
+ data_array = np.hstack(
172
+ (
173
+ selected_rayleigh_extinction_table["altitude"].squeeze().to_value(u.km),
174
+ selected_rayleigh_extinction_table["AOD"].squeeze(),
175
+ )
176
+ )
177
+ selected_rayleigh_extinction_profile = Table(
178
+ data=data_array,
179
+ names=rayleigh_extinction_col_names,
180
+ )
181
+ selected_rayleigh_extinction_profile["altitude_min"] *= u.km
182
+ selected_rayleigh_extinction_profile["altitude_max"] *= u.km
183
+
184
+ selected_rayleigh_extinction_profile.write(
185
+ f"{self.output_path}/selected_rayleigh_extinction_profile.{self.output_format}",
186
+ format=f"{self.output_format}",
187
+ )
188
+
189
+ self.log.info("Shutting down.")
190
+ self.data_handler.cleanup()
191
+ if self.failed_to_retrieve_meteo_data:
192
+ raise IntermittentError(
193
+ f"Missing meteorological data from {self.meteo_data_handler}. "
194
+ "This is a known issue, the reference model was selected based on the date."
195
+ )
196
+
197
+
198
+ def main():
199
+ """Run the app."""
200
+ tool = SelectMolecularAtmosphericModel()
201
+ tool.run()
@@ -0,0 +1,10 @@
1
+ from .observatory import Observatory, Season, SeasonAlias # noqa: D104
2
+ from .observatory_containers import ObservatoryContainer, SeasonContainer
3
+
4
+ __all__ = [
5
+ "Observatory",
6
+ "Season",
7
+ "SeasonAlias",
8
+ "ObservatoryContainer",
9
+ "SeasonContainer",
10
+ ]