ctao-calibpipe 0.1.0rc9__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ctao-calibpipe might be problematic. Click here for more details.
- calibpipe/_version.py +2 -2
- calibpipe/core/common_metadata_containers.py +3 -0
- calibpipe/database/adapter/adapter.py +1 -1
- calibpipe/database/adapter/database_containers/__init__.py +2 -0
- calibpipe/database/adapter/database_containers/common_metadata.py +2 -0
- calibpipe/database/adapter/database_containers/throughput.py +30 -0
- calibpipe/database/interfaces/table_handler.py +79 -97
- calibpipe/telescope/throughput/containers.py +59 -0
- calibpipe/tests/unittests/array/test_cross_calibration.py +417 -0
- calibpipe/tests/unittests/database/test_table_handler.py +95 -0
- calibpipe/tests/unittests/telescope/camera/test_calculate_camcalib_coefficients.py +347 -0
- calibpipe/tests/unittests/telescope/camera/test_produce_camcalib_test_data.py +42 -0
- calibpipe/tests/unittests/telescope/throughput/test_muon_throughput_calibrator.py +189 -0
- calibpipe/tools/camcalib_test_data.py +361 -0
- calibpipe/tools/camera_calibrator.py +558 -0
- calibpipe/tools/muon_throughput_calculator.py +239 -0
- calibpipe/tools/telescope_cross_calibration_calculator.py +721 -0
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0.dist-info}/METADATA +3 -2
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0.dist-info}/RECORD +24 -14
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0.dist-info}/WHEEL +1 -1
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0.dist-info}/entry_points.txt +4 -0
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0.dist-info}/licenses/AUTHORS.md +0 -0
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {ctao_calibpipe-0.1.0rc9.dist-info → ctao_calibpipe-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Tool for calculating of optical throughput and storing of the results in the DB."""
|
|
2
|
+
|
|
3
|
+
from os import path
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from astropy.table import QTable, vstack
|
|
7
|
+
from astropy.time import Time
|
|
8
|
+
from calibpipe.database.connections import CalibPipeDatabase
|
|
9
|
+
from calibpipe.database.interfaces import TableHandler
|
|
10
|
+
from calibpipe.telescope.throughput.containers import OpticalThoughtputContainer
|
|
11
|
+
from ctapipe.core import traits
|
|
12
|
+
from ctapipe.core.traits import CInt, Path, Set
|
|
13
|
+
from ctapipe.instrument import SubarrayDescription
|
|
14
|
+
from ctapipe.io import read_table
|
|
15
|
+
from traitlets import Float, Int, Unicode
|
|
16
|
+
|
|
17
|
+
from .basic_tool_with_db import BasicToolWithDB
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CalculateThroughputWithMuons(BasicToolWithDB):
|
|
21
|
+
"""Perform throughput calibration using muons for each telescope allowed in the EventSource."""
|
|
22
|
+
|
|
23
|
+
name = traits.Unicode("ThroughputCalibration")
|
|
24
|
+
description = __doc__
|
|
25
|
+
|
|
26
|
+
input_url = Path(
|
|
27
|
+
help="CTAO HDF5 files for DL1 calibration (muons).",
|
|
28
|
+
allow_none=False,
|
|
29
|
+
exists=True,
|
|
30
|
+
directory_ok=False,
|
|
31
|
+
file_ok=True,
|
|
32
|
+
).tag(config=True)
|
|
33
|
+
|
|
34
|
+
allowed_tels = Set(
|
|
35
|
+
trait=CInt(),
|
|
36
|
+
default_value=None,
|
|
37
|
+
allow_none=True,
|
|
38
|
+
help=(
|
|
39
|
+
"List of allowed telescope IDs, others will be ignored. If None, all "
|
|
40
|
+
"telescopes in the input stream will be included. Requires the "
|
|
41
|
+
"telescope IDs to match between the groups of the monitoring file."
|
|
42
|
+
),
|
|
43
|
+
).tag(config=True)
|
|
44
|
+
|
|
45
|
+
# TODO: Write back to the DL1 monitoring file instead
|
|
46
|
+
output_url = Path(
|
|
47
|
+
"./OpticalThroughput.ecsv",
|
|
48
|
+
help="Path to the output file where the optical throughput calibration will be saved",
|
|
49
|
+
allow_none=False,
|
|
50
|
+
directory_ok=True,
|
|
51
|
+
).tag(config=True)
|
|
52
|
+
|
|
53
|
+
output_format = Unicode(
|
|
54
|
+
"ascii.ecsv",
|
|
55
|
+
help="Output files format",
|
|
56
|
+
allow_none=False,
|
|
57
|
+
).tag(config=True)
|
|
58
|
+
|
|
59
|
+
min_events = Int(
|
|
60
|
+
default_value=0,
|
|
61
|
+
help="Min number of muon events required to pass the cuts for throughput calculation",
|
|
62
|
+
).tag(config=True)
|
|
63
|
+
|
|
64
|
+
min_ring_radius = Float(default_value=0.8, help="Minimum ring radius in deg").tag(
|
|
65
|
+
config=True
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
max_ring_radius = Float(default_value=1.2, help="Maximum ring radius in deg").tag(
|
|
69
|
+
config=True
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
min_impact_parameter = Float(
|
|
73
|
+
default_value=0.2,
|
|
74
|
+
help="Minimum impact parameter in mirror_radius fractional units",
|
|
75
|
+
).tag(config=True)
|
|
76
|
+
|
|
77
|
+
max_impact_parameter = Float(
|
|
78
|
+
default_value=0.9,
|
|
79
|
+
help="Maximum impact parameter in mirror_radius fractional units",
|
|
80
|
+
).tag(config=True)
|
|
81
|
+
|
|
82
|
+
ring_completeness_threshold = Float(
|
|
83
|
+
default_value=0.3, help="Lower threshold for ring completeness"
|
|
84
|
+
).tag(config=True)
|
|
85
|
+
|
|
86
|
+
ring_containment_threshold = Float(
|
|
87
|
+
default_value=0.3, help="Lower threshold for ring containment"
|
|
88
|
+
).tag(config=True)
|
|
89
|
+
|
|
90
|
+
intensity_ratio = Float(
|
|
91
|
+
default_value=0.5,
|
|
92
|
+
help="Ratio of the photons inside a given ring."
|
|
93
|
+
" The ring is assumed to be in [radius - 0.5 * width, radius + 0.5 * width]",
|
|
94
|
+
).tag(config=True)
|
|
95
|
+
|
|
96
|
+
TEL_GROUP = "/dl1/event/telescope"
|
|
97
|
+
METHOD = "Muon Rings"
|
|
98
|
+
|
|
99
|
+
def setup(self):
|
|
100
|
+
"""Read from the .h5 file necessary info and save it for further processing."""
|
|
101
|
+
# Load the subarray description from the input file
|
|
102
|
+
subarray = SubarrayDescription.from_hdf(self.input_url)
|
|
103
|
+
# Select a new subarray if the allowed_tels configuration is used
|
|
104
|
+
self.subarray = (
|
|
105
|
+
subarray
|
|
106
|
+
if self.allowed_tels is None
|
|
107
|
+
else subarray.select_subarray(self.allowed_tels)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self.throughput_containers = {}
|
|
111
|
+
|
|
112
|
+
def start(self):
|
|
113
|
+
"""
|
|
114
|
+
Apply the cuts on the muon data and store the results in containers.
|
|
115
|
+
|
|
116
|
+
Only the events that passed quality cuts provided by configuration are considered.
|
|
117
|
+
Only events for which intensity fit converged, and parameters were not at the limit are considered.
|
|
118
|
+
"""
|
|
119
|
+
for tel_id in self.subarray.tel_ids:
|
|
120
|
+
muon_table = read_table(
|
|
121
|
+
self.input_url,
|
|
122
|
+
f"{self.TEL_GROUP}/muon/tel_{tel_id:03d}",
|
|
123
|
+
)
|
|
124
|
+
throughput_container = OpticalThoughtputContainer()
|
|
125
|
+
trigger_table = read_table(
|
|
126
|
+
self.input_url,
|
|
127
|
+
f"{self.TEL_GROUP}/trigger",
|
|
128
|
+
)
|
|
129
|
+
start, end = trigger_table["time"].min(), trigger_table["time"].max()
|
|
130
|
+
|
|
131
|
+
mask = (
|
|
132
|
+
(muon_table["muonring_radius"] >= self.min_ring_radius)
|
|
133
|
+
& (muon_table["muonring_radius"] <= self.max_ring_radius)
|
|
134
|
+
& (muon_table["muonefficiency_impact"] >= self.min_impact_parameter)
|
|
135
|
+
& (muon_table["muonefficiency_impact"] <= self.max_impact_parameter)
|
|
136
|
+
& (
|
|
137
|
+
muon_table["muonparameters_completeness"]
|
|
138
|
+
>= self.ring_completeness_threshold
|
|
139
|
+
)
|
|
140
|
+
& (
|
|
141
|
+
muon_table["muonparameters_containment"]
|
|
142
|
+
>= self.ring_containment_threshold
|
|
143
|
+
)
|
|
144
|
+
& (muon_table["muonparameters_intensity_ratio"] >= self.intensity_ratio)
|
|
145
|
+
& (muon_table["muonefficiency_is_valid"] != 0)
|
|
146
|
+
& (muon_table["muonefficiency_parameters_at_limit"] != 1)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
filtered_table = muon_table[mask]
|
|
150
|
+
|
|
151
|
+
if len(filtered_table) > 0:
|
|
152
|
+
throughput_container.tel_id = tel_id
|
|
153
|
+
throughput_container.obs_id = filtered_table["obs_id"][0]
|
|
154
|
+
throughput_container.method = self.METHOD
|
|
155
|
+
throughput_container.optical_throughput_coefficient = np.mean(
|
|
156
|
+
filtered_table["muonefficiency_optical_efficiency"]
|
|
157
|
+
)
|
|
158
|
+
throughput_container.optical_throughput_coefficient_std = np.std(
|
|
159
|
+
filtered_table["muonefficiency_optical_efficiency"]
|
|
160
|
+
)
|
|
161
|
+
throughput_container.validity_start = Time(
|
|
162
|
+
start, format="mjd", scale="utc"
|
|
163
|
+
).to_datetime()
|
|
164
|
+
throughput_container.validity_end = Time(
|
|
165
|
+
end, format="mjd", scale="utc"
|
|
166
|
+
).to_datetime()
|
|
167
|
+
throughput_container.n_events = len(filtered_table)
|
|
168
|
+
|
|
169
|
+
self.throughput_containers[tel_id] = throughput_container
|
|
170
|
+
|
|
171
|
+
def finish(self):
|
|
172
|
+
"""Write the results to the output file and DB."""
|
|
173
|
+
with CalibPipeDatabase(
|
|
174
|
+
**self.database_configuration,
|
|
175
|
+
) as connection:
|
|
176
|
+
for tel_id, throughput_container in self.throughput_containers.items():
|
|
177
|
+
self.log.info(
|
|
178
|
+
"Optical throughput for telescope %s is uploaded to CalibPipe DB "
|
|
179
|
+
"from the calibration method %s",
|
|
180
|
+
tel_id,
|
|
181
|
+
throughput_container.method,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# reference metadata is identical at the moment
|
|
185
|
+
TableHandler.upload_data(throughput_container, None, connection)
|
|
186
|
+
|
|
187
|
+
with CalibPipeDatabase(
|
|
188
|
+
**self.database_configuration,
|
|
189
|
+
) as connection:
|
|
190
|
+
self.output_url.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
|
|
192
|
+
db_table = TableHandler.read_table_from_database(
|
|
193
|
+
type(OpticalThoughtputContainer()), connection
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if path.exists(self.output_url):
|
|
197
|
+
try:
|
|
198
|
+
existing_table = QTable.read(
|
|
199
|
+
self.output_url, format=self.output_format
|
|
200
|
+
)
|
|
201
|
+
db_table_subset = db_table[:-1].copy()
|
|
202
|
+
# Convert 'validity_end' and 'validity_start' to Time objects, to be consistent with the existing table
|
|
203
|
+
db_table_subset["validity_end"] = Time(
|
|
204
|
+
db_table_subset["validity_end"], scale="utc"
|
|
205
|
+
)
|
|
206
|
+
db_table_subset["validity_start"] = Time(
|
|
207
|
+
db_table_subset["validity_start"], scale="utc"
|
|
208
|
+
)
|
|
209
|
+
combined_table = vstack(
|
|
210
|
+
[existing_table, db_table_subset], join_type="exact"
|
|
211
|
+
)
|
|
212
|
+
combined_table.write(
|
|
213
|
+
self.output_url, format=self.output_format, overwrite=True
|
|
214
|
+
)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
self.log.exception(
|
|
217
|
+
"Error reading or writing the existing .ecsv file: %s", e
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
try:
|
|
221
|
+
# Need to properly handle datetime object for JSON serialization
|
|
222
|
+
db_table["validity_end"] = Time(
|
|
223
|
+
db_table["validity_end"], scale="utc"
|
|
224
|
+
)
|
|
225
|
+
db_table["validity_start"] = Time(
|
|
226
|
+
db_table["validity_start"], scale="utc"
|
|
227
|
+
)
|
|
228
|
+
db_table.write(
|
|
229
|
+
self.output_url, format=self.output_format, overwrite=True
|
|
230
|
+
)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
self.log.exception("Error writing the .ecsv file: %s", e)
|
|
233
|
+
raise
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def main():
|
|
237
|
+
"""Run the app."""
|
|
238
|
+
tool = CalculateThroughputWithMuons()
|
|
239
|
+
tool.run()
|