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.
- 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.0rc7.dist-info/METADATA +86 -0
- ctao_calibpipe-0.1.0rc7.dist-info/RECORD +93 -0
- ctao_calibpipe-0.1.0rc7.dist-info/WHEEL +5 -0
- ctao_calibpipe-0.1.0rc7.dist-info/entry_points.txt +8 -0
- ctao_calibpipe-0.1.0rc7.dist-info/licenses/AUTHORS.md +13 -0
- ctao_calibpipe-0.1.0rc7.dist-info/licenses/LICENSE +21 -0
- 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()
|