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.
- calibpipe/__init__.py +5 -0
- calibpipe/_dev_version/__init__.py +9 -0
- calibpipe/_version.py +34 -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 +198 -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 +63 -0
- calibpipe/database/adapter/database_containers/atmosphere.py +199 -0
- calibpipe/database/adapter/database_containers/common_metadata.py +150 -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/throughput.py +30 -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 +333 -0
- calibpipe/database/interfaces/types.py +96 -0
- calibpipe/telescope/throughput/containers.py +66 -0
- calibpipe/tests/conftest.py +274 -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/test_conftest_data.py +200 -0
- calibpipe/tests/unittests/array/test_cross_calibration.py +412 -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 +163 -0
- calibpipe/tests/unittests/database/test_types.py +38 -0
- calibpipe/tests/unittests/telescope/camera/test_calculate_camcalib_coefficients.py +456 -0
- calibpipe/tests/unittests/telescope/camera/test_produce_camcalib_test_data.py +37 -0
- calibpipe/tests/unittests/telescope/throughput/test_muon_throughput_calibrator.py +693 -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/camcalib_test_data.py +374 -0
- calibpipe/tools/camera_calibrator.py +462 -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/muon_throughput_calculator.py +219 -0
- calibpipe/tools/observatory_data_db_loader.py +71 -0
- calibpipe/tools/reference_atmospheric_model_selector.py +201 -0
- calibpipe/tools/telescope_cross_calibration_calculator.py +721 -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.3.0rc2.dist-info/METADATA +92 -0
- ctao_calibpipe-0.3.0rc2.dist-info/RECORD +105 -0
- ctao_calibpipe-0.3.0rc2.dist-info/WHEEL +5 -0
- ctao_calibpipe-0.3.0rc2.dist-info/entry_points.txt +12 -0
- ctao_calibpipe-0.3.0rc2.dist-info/licenses/AUTHORS.md +13 -0
- ctao_calibpipe-0.3.0rc2.dist-info/licenses/LICENSE +21 -0
- 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()
|