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,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,219 @@
1
+ """Tool for calculating of optical throughput and storing of the results in the DB."""
2
+
3
+ import numpy as np
4
+ from astropy.table import Table, join
5
+ from astropy.time import Time
6
+ from calibpipe.telescope.throughput.containers import OpticalThoughtputContainer
7
+ from ctapipe.core import QualityQuery, Tool, traits
8
+ from ctapipe.core.traits import (
9
+ Bool,
10
+ CInt,
11
+ ComponentName,
12
+ List,
13
+ Path,
14
+ Set,
15
+ classes_with_traits,
16
+ )
17
+ from ctapipe.instrument import SubarrayDescription
18
+ from ctapipe.io import read_table, write_table
19
+ from ctapipe.io.hdf5dataformat import (
20
+ DL1_TEL_CALIBRATION_GROUP,
21
+ DL1_TEL_GROUP,
22
+ DL1_TEL_TRIGGER_TABLE,
23
+ )
24
+ from ctapipe.monitoring.aggregator import StatisticsAggregator
25
+
26
+
27
+ class MuonQualityQuery(QualityQuery):
28
+ """Quality criteria for muon parameters."""
29
+
30
+ quality_criteria = List(
31
+ default_value=[
32
+ ("min impact parameter", "muonefficiency_impact >= 0.0"),
33
+ ("max impact parameter", "muonefficiency_impact <= 10.0"),
34
+ (
35
+ "muon efficiency parameters are not at limit",
36
+ "~muonefficiency_parameters_at_limit",
37
+ ),
38
+ ("throughput calculation is valid", "muonefficiency_is_valid"),
39
+ ],
40
+ allow_none=True,
41
+ help=QualityQuery.quality_criteria.help,
42
+ ).tag(config=True)
43
+
44
+
45
+ class CalculateThroughputWithMuons(Tool):
46
+ """Perform throughput calibration using muons for each telescope allowed in the EventSource."""
47
+
48
+ name = traits.Unicode("ThroughputCalibration")
49
+ description = __doc__
50
+
51
+ aliases = {
52
+ ("i", "input"): "CalculateThroughputWithMuons.input_url",
53
+ }
54
+
55
+ classes = [MuonQualityQuery] + classes_with_traits(StatisticsAggregator)
56
+
57
+ input_url = Path(
58
+ help="CTAO HDF5 files for DL1 calibration (muons).",
59
+ allow_none=False,
60
+ exists=True,
61
+ directory_ok=False,
62
+ file_ok=True,
63
+ ).tag(config=True)
64
+
65
+ allowed_tels = Set(
66
+ trait=CInt(),
67
+ default_value=None,
68
+ allow_none=True,
69
+ help=(
70
+ "List of allowed telescope IDs, others will be ignored. If None, all "
71
+ "telescopes in the input stream will be included. Requires the "
72
+ "telescope IDs to match between the groups of the monitoring file."
73
+ ),
74
+ ).tag(config=True)
75
+
76
+ aggregator_type = ComponentName(
77
+ StatisticsAggregator,
78
+ default_value="SigmaClippingAggregator",
79
+ help="The aggregation strategy to use for throughput calculation.",
80
+ ).tag(config=True)
81
+
82
+ append = Bool(
83
+ default_value=False,
84
+ help="If the throughput table already exists in the file, append to it.",
85
+ ).tag(config=True)
86
+
87
+ # Set the method name for throughput calculation
88
+ METHOD = "Muon Rings"
89
+
90
+ def setup(self):
91
+ """Read from the .h5 file necessary info and save it for further processing."""
92
+ # Load the subarray description from the input file
93
+ self.subarray = SubarrayDescription.from_hdf(self.input_url)
94
+
95
+ # Select a new subarray if the allowed_tels configuration is used
96
+ if self.allowed_tels is not None:
97
+ self.subarray = self.subarray.select_subarray(self.allowed_tels)
98
+
99
+ # Initialize the quality query for muon selection
100
+ self.quality_query = MuonQualityQuery(parent=self)
101
+
102
+ # Initialize the aggregator based on configuration
103
+ self.aggregator = StatisticsAggregator.from_name(
104
+ self.aggregator_type,
105
+ parent=self,
106
+ )
107
+
108
+ self.throughput_containers = {}
109
+
110
+ def _process_tel(self, tel_id):
111
+ """Process muon data for a single telescope ID."""
112
+ muon_table = read_table(
113
+ self.input_url,
114
+ f"{DL1_TEL_GROUP}/muon/tel_{tel_id:03d}",
115
+ )
116
+
117
+ filtered_table = muon_table[self.quality_query.get_table_mask(muon_table)]
118
+
119
+ # Read trigger table to get time information
120
+ trigger_table = read_table(
121
+ self.input_url,
122
+ DL1_TEL_TRIGGER_TABLE,
123
+ )
124
+
125
+ # Join timing information from trigger table
126
+ filtered_table = join(
127
+ filtered_table,
128
+ trigger_table[["obs_id", "event_id", "time"]],
129
+ keys=["obs_id", "event_id"],
130
+ join_type="left", # keeps all muon events, even if no trigger match
131
+ )
132
+
133
+ # Ensure table is sorted by time
134
+ filtered_table.sort("time")
135
+
136
+ # Add time_mono column for aggregator (reusing original table)
137
+ filtered_table["time_mono"] = filtered_table["time"]
138
+
139
+ # Run aggregator with chunk processing - will raise ValueError if insufficient data
140
+ chunk_stats = self.aggregator(
141
+ table=filtered_table,
142
+ col_name="muonefficiency_optical_efficiency",
143
+ )
144
+
145
+ # Convert aggregator results to throughput containers
146
+ containers = []
147
+ for i in range(len(chunk_stats)):
148
+ # Create container with all parameters at once
149
+ container = OpticalThoughtputContainer(
150
+ obs_id=filtered_table["obs_id"],
151
+ method=self.METHOD,
152
+ mean=chunk_stats["mean"][i],
153
+ median=chunk_stats["median"][i],
154
+ std=chunk_stats["std"][i],
155
+ sem=np.squeeze(
156
+ chunk_stats["std"][i] / (chunk_stats["n_events"][i] ** 0.5)
157
+ ),
158
+ time_start=Time(
159
+ chunk_stats["time_start"][i], format="mjd", scale="tai"
160
+ ),
161
+ time_end=Time(chunk_stats["time_end"][i], format="mjd", scale="tai"),
162
+ n_events=np.squeeze(chunk_stats["n_events"][i]),
163
+ )
164
+ containers.append(container)
165
+ return containers
166
+
167
+ def start(self):
168
+ """
169
+ Apply the cuts on the muon data and process in chunks.
170
+
171
+ Only the events that passed quality cuts provided by configuration are considered.
172
+ Only events for which intensity fit converged, and parameters were not at the limit are considered.
173
+ """
174
+ for tel_id in self.subarray.tel_ids:
175
+ try:
176
+ containers = self._process_tel(tel_id)
177
+ self.throughput_containers[tel_id] = containers
178
+ except ValueError as e:
179
+ self.log.warning("Skipping telescope %s: %s", tel_id, e)
180
+ self.throughput_containers[tel_id] = {}
181
+ continue
182
+
183
+ def finish(self):
184
+ """Write the chunk-based results to the output file using write_table."""
185
+ for tel_id in self.subarray.tel_ids:
186
+ containers_list = self.throughput_containers[tel_id]
187
+
188
+ # Convert containers to table data using as_dict()
189
+ table_data = []
190
+ for container in containers_list:
191
+ table_data.append(container.as_dict())
192
+
193
+ if not table_data:
194
+ self.log.info("No throughput data to write for telescope %s", tel_id)
195
+ continue
196
+ # Create astropy Table from container data
197
+ throughput_table = Table(table_data)
198
+
199
+ # Write table to HDF5 file
200
+ if self.append:
201
+ write_table(
202
+ throughput_table,
203
+ self.input_url,
204
+ f"{DL1_TEL_CALIBRATION_GROUP}/optical_throughput/tel_{tel_id:03d}",
205
+ append=True,
206
+ )
207
+ else:
208
+ write_table(
209
+ throughput_table,
210
+ self.input_url,
211
+ f"{DL1_TEL_CALIBRATION_GROUP}/optical_throughput/tel_{tel_id:03d}",
212
+ overwrite=True,
213
+ )
214
+
215
+
216
+ def main():
217
+ """Run the app."""
218
+ tool = CalculateThroughputWithMuons()
219
+ 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()