ctao-calibpipe 0.1.0rc8__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.

Files changed (24) hide show
  1. calibpipe/_version.py +2 -2
  2. calibpipe/core/common_metadata_containers.py +3 -0
  3. calibpipe/database/adapter/adapter.py +1 -1
  4. calibpipe/database/adapter/database_containers/__init__.py +2 -0
  5. calibpipe/database/adapter/database_containers/common_metadata.py +2 -0
  6. calibpipe/database/adapter/database_containers/throughput.py +30 -0
  7. calibpipe/database/interfaces/table_handler.py +79 -97
  8. calibpipe/telescope/throughput/containers.py +59 -0
  9. calibpipe/tests/unittests/array/test_cross_calibration.py +417 -0
  10. calibpipe/tests/unittests/database/test_table_handler.py +95 -0
  11. calibpipe/tests/unittests/telescope/camera/test_calculate_camcalib_coefficients.py +347 -0
  12. calibpipe/tests/unittests/telescope/camera/test_produce_camcalib_test_data.py +42 -0
  13. calibpipe/tests/unittests/telescope/throughput/test_muon_throughput_calibrator.py +189 -0
  14. calibpipe/tools/camcalib_test_data.py +361 -0
  15. calibpipe/tools/camera_calibrator.py +558 -0
  16. calibpipe/tools/muon_throughput_calculator.py +239 -0
  17. calibpipe/tools/telescope_cross_calibration_calculator.py +721 -0
  18. {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0.dist-info}/METADATA +3 -2
  19. {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0.dist-info}/RECORD +24 -14
  20. {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0.dist-info}/WHEEL +1 -1
  21. {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0.dist-info}/entry_points.txt +4 -0
  22. {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0.dist-info}/licenses/AUTHORS.md +0 -0
  23. {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0.dist-info}/licenses/LICENSE +0 -0
  24. {ctao_calibpipe-0.1.0rc8.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()