ctao-calibpipe 0.1.0rc8__py3-none-any.whl → 0.2.0rc1__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.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/METADATA +3 -2
- {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/RECORD +24 -14
- {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/WHEEL +1 -1
- {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/entry_points.txt +4 -0
- {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/licenses/AUTHORS.md +0 -0
- {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/licenses/LICENSE +0 -0
- {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"""Calculate camera calibration coefficients using the FFactor method."""
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
|
|
5
|
+
import astropy.units as u
|
|
6
|
+
import h5py
|
|
7
|
+
import numpy as np
|
|
8
|
+
from astropy.table import Column, Table
|
|
9
|
+
from ctapipe.core import Tool
|
|
10
|
+
from ctapipe.core.traits import (
|
|
11
|
+
AstroQuantity,
|
|
12
|
+
Bool,
|
|
13
|
+
CInt,
|
|
14
|
+
Float,
|
|
15
|
+
Int,
|
|
16
|
+
List,
|
|
17
|
+
Path,
|
|
18
|
+
Set,
|
|
19
|
+
classes_with_traits,
|
|
20
|
+
)
|
|
21
|
+
from ctapipe.instrument import SubarrayDescription
|
|
22
|
+
from ctapipe.io import read_table, write_table
|
|
23
|
+
from ctapipe.monitoring import ChunkInterpolator, StdOutlierDetector
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"CameraCalibratorTool",
|
|
27
|
+
"NpeStdOutlierDetector",
|
|
28
|
+
"StatisticsInterpolator",
|
|
29
|
+
"PedestalImageInterpolator",
|
|
30
|
+
"FlatfieldImageInterpolator",
|
|
31
|
+
"FlatfieldPeakTimeInterpolator",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# TODO These interpolator classes are temporary and should be migrated to or adjusted in ctapipe once interface with DataPipe is defined
|
|
36
|
+
# start_time and end_time should be renamed to time_start and time_end
|
|
37
|
+
class StatisticsInterpolator(ChunkInterpolator):
|
|
38
|
+
"""Interpolator for statistics tables."""
|
|
39
|
+
|
|
40
|
+
required_columns = frozenset(["start_time", "end_time", "mean", "median", "std"])
|
|
41
|
+
expected_units = {"mean": None, "median": None, "std": None}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PedestalImageInterpolator(StatisticsInterpolator):
|
|
45
|
+
"""Interpolator for pedestal image tables."""
|
|
46
|
+
|
|
47
|
+
telescope_data_group = "/dl1/monitoring/telescope/pedestal_image"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FlatfieldImageInterpolator(StatisticsInterpolator):
|
|
51
|
+
"""Interpolator for flatfield image tables."""
|
|
52
|
+
|
|
53
|
+
telescope_data_group = "/dl1/monitoring/telescope/flatfield_image"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FlatfieldPeakTimeInterpolator(StatisticsInterpolator):
|
|
57
|
+
"""Interpolator for flatfield peak time tables."""
|
|
58
|
+
|
|
59
|
+
telescope_data_group = "/dl1/monitoring/telescope/flatfield_peak_time"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class NpeStdOutlierDetector(StdOutlierDetector):
|
|
63
|
+
"""
|
|
64
|
+
Detect outliers based on the deviation from the expected standard deviation of the number of photoelectrons.
|
|
65
|
+
|
|
66
|
+
The clipping interval to set the thresholds for detecting outliers is computed by multiplying
|
|
67
|
+
the configurable factors and the expected standard deviation of the number of photoelectrons. The
|
|
68
|
+
expected standard deviation of the number of photoelectrons is calculated based on the median number
|
|
69
|
+
of photoelectrons and the number of events.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
n_events = Int(
|
|
73
|
+
default_value=2500,
|
|
74
|
+
help="Number of events used for the chunk-wise aggregation of the statistic values of the calibration data.",
|
|
75
|
+
).tag(config=True)
|
|
76
|
+
|
|
77
|
+
relative_qe_dispersion = Float(
|
|
78
|
+
0.07,
|
|
79
|
+
help="Relative (effective) quantum efficiency dispersion of PMs over the camera",
|
|
80
|
+
).tag(config=True)
|
|
81
|
+
|
|
82
|
+
linear_noise_coeff = List(
|
|
83
|
+
trait=Float(),
|
|
84
|
+
default_value=[1.79717813, 1.72458305],
|
|
85
|
+
minlen=1,
|
|
86
|
+
maxlen=2,
|
|
87
|
+
help=(
|
|
88
|
+
"Linear noise coefficients [high gain, low gain] or [single gain] obtained with a fit of the std of the "
|
|
89
|
+
"LST-1 filter scan taken on 2023/05/10."
|
|
90
|
+
),
|
|
91
|
+
).tag(config=True)
|
|
92
|
+
|
|
93
|
+
linear_noise_offset = List(
|
|
94
|
+
trait=Float(),
|
|
95
|
+
default_value=[0.0231544, -0.00162036639],
|
|
96
|
+
minlen=1,
|
|
97
|
+
maxlen=2,
|
|
98
|
+
help=(
|
|
99
|
+
"Linear noise offsets [high gain, low gain] or [single gain] obtained with a fit of the std of the "
|
|
100
|
+
"LST-1 filter scan taken on 2023/05/10."
|
|
101
|
+
),
|
|
102
|
+
).tag(config=True)
|
|
103
|
+
|
|
104
|
+
quadratic_noise_coeff = List(
|
|
105
|
+
trait=Float(),
|
|
106
|
+
default_value=[0.000499670969, 0.00142218],
|
|
107
|
+
minlen=1,
|
|
108
|
+
maxlen=2,
|
|
109
|
+
help=(
|
|
110
|
+
"Quadratic noise coefficients [high gain, low gain] or [single gain] obtained with a fit of the std of the "
|
|
111
|
+
"LST-1 filter scan taken on 2023/05/10."
|
|
112
|
+
),
|
|
113
|
+
).tag(config=True)
|
|
114
|
+
|
|
115
|
+
quadratic_noise_offset = List(
|
|
116
|
+
trait=Float(),
|
|
117
|
+
default_value=[0.0000249034290, 0.0001207],
|
|
118
|
+
minlen=1,
|
|
119
|
+
maxlen=2,
|
|
120
|
+
help=(
|
|
121
|
+
"Quadratic noise offsets [high gain, low gain] or [single gain] obtained with a fit of the std of the LST-1 "
|
|
122
|
+
"LST-1 filter scan taken on 2023/05/10."
|
|
123
|
+
),
|
|
124
|
+
).tag(config=True)
|
|
125
|
+
|
|
126
|
+
def __call__(self, column):
|
|
127
|
+
r"""
|
|
128
|
+
Detect outliers based on the deviation from the expected standard deviation of the number of photoelectrons.
|
|
129
|
+
|
|
130
|
+
The clipping interval to set the thresholds for detecting outliers is computed by multiplying
|
|
131
|
+
the configurable factors and the expected standard deviation of the number of photoelectrons
|
|
132
|
+
(npe) over the camera. The expected standard deviation of the estimated npe is given by
|
|
133
|
+
``std_pe_mean = \frac{std_npe}{\sqrt{n_events + (relative_qe_dispersion \cdot npe)^2}}`` where the
|
|
134
|
+
relative_qe_dispersion is mainly due to different detection QE among PMs. However, due to
|
|
135
|
+
the systematics correction associated to the B term, a linear and quadratic noise component
|
|
136
|
+
must be added, these components depend on the sample statistics (n_events).
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
column : astropy.table.Column
|
|
141
|
+
Column of the calculated the number of photoelectrons using the chunk-wise aggregated statistic values
|
|
142
|
+
of the calibration data of shape (n_entries, n_channels, n_pixels).
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
outliers : np.ndarray of bool
|
|
147
|
+
The mask of outliers of shape (n_entries, n_channels, n_pixels) based on the deviation
|
|
148
|
+
from the expected standard deviation of the number of photoelectrons.
|
|
149
|
+
"""
|
|
150
|
+
# Calculate the median number of photoelectrons
|
|
151
|
+
npe_median = np.nanmedian(column, axis=2)
|
|
152
|
+
# Calculate the basic variance
|
|
153
|
+
basic_variance = (
|
|
154
|
+
npe_median / self.n_events + (self.relative_qe_dispersion * npe_median) ** 2
|
|
155
|
+
)
|
|
156
|
+
# Calculate the linear noise term
|
|
157
|
+
linear_term = (
|
|
158
|
+
self.linear_noise_coeff / (np.sqrt(self.n_events))
|
|
159
|
+
+ self.linear_noise_offset
|
|
160
|
+
)
|
|
161
|
+
# Calculate the quadratic noise term
|
|
162
|
+
quadratic_term = (
|
|
163
|
+
self.quadratic_noise_coeff / (np.sqrt(self.n_events))
|
|
164
|
+
+ self.quadratic_noise_offset
|
|
165
|
+
)
|
|
166
|
+
# Calculate the added variance
|
|
167
|
+
added_variance = (linear_term * npe_median) ** 2 + (
|
|
168
|
+
quadratic_term * npe_median
|
|
169
|
+
) ** 2
|
|
170
|
+
# Calculate the total standard deviation of the number of photoelectrons
|
|
171
|
+
npe_std = np.sqrt(basic_variance + added_variance)
|
|
172
|
+
# Detect outliers based on the deviation of the standard deviation distribution
|
|
173
|
+
deviation = column - npe_median[:, :, np.newaxis]
|
|
174
|
+
outliers = np.logical_or(
|
|
175
|
+
deviation < self.std_range_factors[0] * npe_std[:, :, np.newaxis],
|
|
176
|
+
deviation > self.std_range_factors[1] * npe_std[:, :, np.newaxis],
|
|
177
|
+
)
|
|
178
|
+
return outliers
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class CameraCalibratorTool(Tool):
|
|
182
|
+
"""Calculate camera calibration coefficients using the FFactor method."""
|
|
183
|
+
|
|
184
|
+
name = "calibpipe-calculate-camcalib-coefficients"
|
|
185
|
+
description = "Calculate camera calibration coefficients using the FFactor method"
|
|
186
|
+
|
|
187
|
+
examples = """
|
|
188
|
+
To calculate camera calibration coefficients using the FFactor method, run:
|
|
189
|
+
|
|
190
|
+
> calibpipe-calculate-camcalib-coefficients --input_url monitoring.h5 --overwrite
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
input_url = Path(
|
|
194
|
+
help="CTAO HDF5 files for DL1 calibration monitoring",
|
|
195
|
+
allow_none=False,
|
|
196
|
+
exists=True,
|
|
197
|
+
directory_ok=False,
|
|
198
|
+
file_ok=True,
|
|
199
|
+
).tag(config=True)
|
|
200
|
+
|
|
201
|
+
allowed_tels = Set(
|
|
202
|
+
trait=CInt(),
|
|
203
|
+
default_value=None,
|
|
204
|
+
allow_none=True,
|
|
205
|
+
help=(
|
|
206
|
+
"List of allowed telescope IDs, others will be ignored. If None, all "
|
|
207
|
+
"telescopes in the input stream will be included. Requires the "
|
|
208
|
+
"telescope IDs to match between the groups of the monitoring file."
|
|
209
|
+
),
|
|
210
|
+
).tag(config=True)
|
|
211
|
+
|
|
212
|
+
timestamp_tolerance = AstroQuantity(
|
|
213
|
+
default_value=u.Quantity(1.0, u.second),
|
|
214
|
+
physical_type=u.physical.time,
|
|
215
|
+
help="Time difference in seconds to consider two timestamps equal.",
|
|
216
|
+
).tag(config=True)
|
|
217
|
+
|
|
218
|
+
faulty_pixels_fraction = Float(
|
|
219
|
+
default_value=0.1,
|
|
220
|
+
allow_none=True,
|
|
221
|
+
help="Minimum fraction of faulty camera pixels to identify regions of trouble.",
|
|
222
|
+
).tag(config=True)
|
|
223
|
+
|
|
224
|
+
# TODO These parameters are temporary and should be read from the metadata
|
|
225
|
+
systematic_correction_path = Path(
|
|
226
|
+
default_value=None,
|
|
227
|
+
allow_none=True,
|
|
228
|
+
exists=True,
|
|
229
|
+
directory_ok=False,
|
|
230
|
+
help=(
|
|
231
|
+
"Temp Fix: Path to systematic correction file "
|
|
232
|
+
"for additional noise component that is proportional to the signal amplitude "
|
|
233
|
+
),
|
|
234
|
+
).tag(config=True)
|
|
235
|
+
|
|
236
|
+
# TODO These parameters are temporary and should be read from the metadata
|
|
237
|
+
squared_excess_noise_factor = Float(
|
|
238
|
+
1.222, help="Temp Fix: Excess noise factor squared: 1+ Var(gain)/Mean(Gain)**2"
|
|
239
|
+
).tag(config=True)
|
|
240
|
+
|
|
241
|
+
# TODO These parameters are temporary and should be read from the metadata
|
|
242
|
+
window_width = Int(
|
|
243
|
+
12,
|
|
244
|
+
help="Temp Fix: Width of the window used for the image extraction",
|
|
245
|
+
).tag(config=True)
|
|
246
|
+
|
|
247
|
+
overwrite = Bool(help="Overwrite output file if it exists").tag(config=True)
|
|
248
|
+
|
|
249
|
+
aliases = {
|
|
250
|
+
("i", "input_url"): "CameraCalibratorTool.input_url",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
flags = {
|
|
254
|
+
"overwrite": (
|
|
255
|
+
{"CameraCalibratorTool": {"overwrite": True}},
|
|
256
|
+
"Overwrite existing files",
|
|
257
|
+
),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
classes = classes_with_traits(NpeStdOutlierDetector)
|
|
261
|
+
|
|
262
|
+
# Define the group in the monitoring file
|
|
263
|
+
MONITORING_TEL_GROUP = "/dl1/monitoring/telescope/"
|
|
264
|
+
|
|
265
|
+
def setup(self):
|
|
266
|
+
"""Set up the tool.
|
|
267
|
+
|
|
268
|
+
- Set up the subarray.
|
|
269
|
+
- Load the systematic correction term B.
|
|
270
|
+
- Configure the outlier detector for the expected standard deviation of the number of photoelectrons.
|
|
271
|
+
"""
|
|
272
|
+
# Load the subarray description from the input file
|
|
273
|
+
subarray = SubarrayDescription.from_hdf(self.input_url)
|
|
274
|
+
# Select a new subarray if the allowed_tels configuration is used
|
|
275
|
+
self.subarray = (
|
|
276
|
+
subarray
|
|
277
|
+
if self.allowed_tels is None
|
|
278
|
+
else subarray.select_subarray(self.allowed_tels)
|
|
279
|
+
)
|
|
280
|
+
# Load systematic correction term B
|
|
281
|
+
self.quadratic_term = 0
|
|
282
|
+
if self.systematic_correction_path is not None:
|
|
283
|
+
with h5py.File(self.systematic_correction_path, "r") as hf:
|
|
284
|
+
self.quadratic_term = np.array(hf["B_term"])
|
|
285
|
+
# Load the outlier detector for the expected standard deviation of the number of photoelectrons
|
|
286
|
+
if "NpeStdOutlierDetector" in self.config:
|
|
287
|
+
self.log.info(
|
|
288
|
+
"Applying outlier detection 'NpeStdOutlierDetector' "
|
|
289
|
+
"based on the deviation from the expected standard "
|
|
290
|
+
"deviation of the number of photoelectrons."
|
|
291
|
+
)
|
|
292
|
+
self.outlier_detector = NpeStdOutlierDetector(
|
|
293
|
+
parent=self, subarray=self.subarray
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
self.log.info(
|
|
297
|
+
"No outlier detection applied. 'NpeStdOutlierDetector' not in config."
|
|
298
|
+
)
|
|
299
|
+
self.outlier_detector = None
|
|
300
|
+
|
|
301
|
+
# Instantiate the chunk interpolators for each table
|
|
302
|
+
self.pedestal_image_interpolator = PedestalImageInterpolator()
|
|
303
|
+
self.flatfield_image_interpolator = FlatfieldImageInterpolator()
|
|
304
|
+
self.flatfield_peak_time_interpolator = FlatfieldPeakTimeInterpolator()
|
|
305
|
+
|
|
306
|
+
def start(self):
|
|
307
|
+
"""Iterate over the telescope IDs and calculate the camera calibration coefficients."""
|
|
308
|
+
self.camcalib_table = {}
|
|
309
|
+
# Iterate over the telescope IDs and calculate the camera calibration coefficients
|
|
310
|
+
for tel_id in self.subarray.tel_ids:
|
|
311
|
+
# Read the tables from the monitoring file requiring all tables to be present
|
|
312
|
+
calibration_tables = {
|
|
313
|
+
name: read_table(
|
|
314
|
+
self.input_url,
|
|
315
|
+
f"{self.MONITORING_TEL_GROUP}{name}/tel_{tel_id:03d}",
|
|
316
|
+
)
|
|
317
|
+
for name in ("pedestal_image", "flatfield_image", "flatfield_peak_time")
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# Check if there is a single chunk for all the tables
|
|
321
|
+
if all(len(table) == 1 for table in calibration_tables.values()):
|
|
322
|
+
# If there is only a single chunk, set the unique timestamps to the start time
|
|
323
|
+
unique_timestamps = [
|
|
324
|
+
min(
|
|
325
|
+
calibration_tables["pedestal_image"]["time_start"][0],
|
|
326
|
+
calibration_tables["flatfield_image"]["time_start"][0],
|
|
327
|
+
)
|
|
328
|
+
]
|
|
329
|
+
else:
|
|
330
|
+
# Get the unique timestamps from the tables
|
|
331
|
+
unique_timestamps = self._get_unique_timestamps(
|
|
332
|
+
*calibration_tables.values()
|
|
333
|
+
)
|
|
334
|
+
# Process the tables and interpolate the data
|
|
335
|
+
for name, interpolator in (
|
|
336
|
+
("pedestal_image", self.pedestal_image_interpolator),
|
|
337
|
+
("flatfield_image", self.flatfield_image_interpolator),
|
|
338
|
+
("flatfield_peak_time", self.flatfield_peak_time_interpolator),
|
|
339
|
+
):
|
|
340
|
+
calibration_tables[name] = self._process_table(
|
|
341
|
+
tel_id,
|
|
342
|
+
calibration_tables[name],
|
|
343
|
+
interpolator,
|
|
344
|
+
unique_timestamps,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Concatenate the outlier masks
|
|
348
|
+
outlier_mask = np.logical_or.reduce(
|
|
349
|
+
[
|
|
350
|
+
np.isnan(table["median"].data)
|
|
351
|
+
for table in calibration_tables.values()
|
|
352
|
+
]
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Extract calibration coefficients with F-factor method
|
|
356
|
+
# Calculate the signal
|
|
357
|
+
signal = (
|
|
358
|
+
calibration_tables["flatfield_image"]["median"].data
|
|
359
|
+
- calibration_tables["pedestal_image"]["median"].data
|
|
360
|
+
)
|
|
361
|
+
# Calculate the gain with the excess noise factor must be known from elsewhere
|
|
362
|
+
gain = (
|
|
363
|
+
np.divide(
|
|
364
|
+
calibration_tables["flatfield_image"]["std"].data ** 2
|
|
365
|
+
- calibration_tables["pedestal_image"]["std"].data ** 2,
|
|
366
|
+
self.squared_excess_noise_factor * signal,
|
|
367
|
+
)
|
|
368
|
+
- self.quadratic_term**2 * signal / self.squared_excess_noise_factor
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Calculate the number of photoelectrons
|
|
372
|
+
n_pe = np.divide(signal, gain)
|
|
373
|
+
# Absolute gain calibration
|
|
374
|
+
npe_median = np.nanmedian(n_pe, axis=2)
|
|
375
|
+
|
|
376
|
+
data, units = {}, {}
|
|
377
|
+
# Set the time column to the unique timestamps
|
|
378
|
+
data["time"] = unique_timestamps
|
|
379
|
+
data["factor"] = np.divide(npe_median[:, :, np.newaxis], signal)
|
|
380
|
+
# Pedestal offset
|
|
381
|
+
# TODO: read window_width from metadata
|
|
382
|
+
data["pedestal_offset"] = (
|
|
383
|
+
calibration_tables["pedestal_image"]["median"].data / self.window_width
|
|
384
|
+
)
|
|
385
|
+
units["pedestal_offset"] = calibration_tables["pedestal_image"][
|
|
386
|
+
"median"
|
|
387
|
+
].unit
|
|
388
|
+
# Relative time calibration
|
|
389
|
+
median_arrival_time = np.nanmedian(
|
|
390
|
+
calibration_tables["flatfield_peak_time"]["median"].data, axis=2
|
|
391
|
+
)
|
|
392
|
+
data["time_shift"] = (
|
|
393
|
+
calibration_tables["flatfield_peak_time"]["median"].data
|
|
394
|
+
- median_arrival_time[:, :, np.newaxis]
|
|
395
|
+
)
|
|
396
|
+
units["time_shift"] = calibration_tables["flatfield_peak_time"][
|
|
397
|
+
"median"
|
|
398
|
+
].unit
|
|
399
|
+
|
|
400
|
+
# Apply outlier detection if selected
|
|
401
|
+
if self.outlier_detector is not None:
|
|
402
|
+
# Create npe outlier mask
|
|
403
|
+
npe_outliers = self.outlier_detector(Column(data=n_pe, name="n_pe"))
|
|
404
|
+
# Stack the outlier masks with the npe outlier mask
|
|
405
|
+
outlier_mask = np.logical_or(
|
|
406
|
+
outlier_mask,
|
|
407
|
+
npe_outliers,
|
|
408
|
+
)
|
|
409
|
+
# Append the column of the new outlier mask
|
|
410
|
+
data["outlier_mask"] = outlier_mask
|
|
411
|
+
# Check if the camera has two gain channels
|
|
412
|
+
if outlier_mask.shape[1] == 2:
|
|
413
|
+
# Combine the outlier mask of both gain channels
|
|
414
|
+
outlier_mask = np.logical_or.reduce(outlier_mask, axis=1)
|
|
415
|
+
# Calculate the fraction of faulty pixels over the camera
|
|
416
|
+
faulty_pixels = (
|
|
417
|
+
np.count_nonzero(outlier_mask, axis=-1) / np.shape(outlier_mask)[-1]
|
|
418
|
+
)
|
|
419
|
+
# Check for valid chunks if the predefined threshold ``faulty_pixels_fraction``
|
|
420
|
+
# is not exceeded and append the is_valid column
|
|
421
|
+
data["is_valid"] = faulty_pixels < self.faulty_pixels_fraction
|
|
422
|
+
|
|
423
|
+
# Create the table for the camera calibration coefficients
|
|
424
|
+
self.camcalib_table[tel_id] = Table(data, units=units)
|
|
425
|
+
|
|
426
|
+
def finish(self):
|
|
427
|
+
"""Write the camera calibration coefficients to the output file."""
|
|
428
|
+
# Overwrite the subarray description in the file if overwrite is selected
|
|
429
|
+
self.subarray.to_hdf(self.input_url, overwrite=self.overwrite)
|
|
430
|
+
self.log.info(
|
|
431
|
+
"Subarray description was overwritten in '%s'",
|
|
432
|
+
self.input_url,
|
|
433
|
+
)
|
|
434
|
+
# Write the camera calibration coefficients and their outlier mask
|
|
435
|
+
# to the output file for each selected telescope
|
|
436
|
+
for tel_id in self.subarray.tel_ids:
|
|
437
|
+
write_table(
|
|
438
|
+
self.camcalib_table[tel_id],
|
|
439
|
+
self.input_url,
|
|
440
|
+
f"{self.MONITORING_TEL_GROUP}camera_calibration/tel_{tel_id:03d}",
|
|
441
|
+
overwrite=self.overwrite,
|
|
442
|
+
)
|
|
443
|
+
self.log.info(
|
|
444
|
+
"DL1 monitoring data was stored in '%s' under '%s'",
|
|
445
|
+
self.input_url,
|
|
446
|
+
f"{self.MONITORING_TEL_GROUP}camera_calibration/tel_{tel_id:03d}",
|
|
447
|
+
)
|
|
448
|
+
self.log.info("Tool is shutting down")
|
|
449
|
+
|
|
450
|
+
def _get_unique_timestamps(
|
|
451
|
+
self, pedestal_image_table, flatfield_image_table, flatfield_peak_time_table
|
|
452
|
+
):
|
|
453
|
+
"""
|
|
454
|
+
Extract unique timestamps from the given tables.
|
|
455
|
+
|
|
456
|
+
This method collects the start and end timestamps from the provided
|
|
457
|
+
chunks in the pedestal_image, flatfield_image, and flatfield_peak_time
|
|
458
|
+
tables. It then sorts the timestamps and filters them based on the
|
|
459
|
+
specified timestamp tolerance.
|
|
460
|
+
|
|
461
|
+
Parameters
|
|
462
|
+
----------
|
|
463
|
+
pedestal_image_table : astropy.table.Table
|
|
464
|
+
Table containing pedestal image data.
|
|
465
|
+
flatfield_image_table : astropy.table.Table
|
|
466
|
+
Table containing flatfield image data.
|
|
467
|
+
flatfield_peak_time_table : astropy.table.Table
|
|
468
|
+
Table containing flatfield peak time data.
|
|
469
|
+
|
|
470
|
+
Returns
|
|
471
|
+
-------
|
|
472
|
+
unique_timestamps : astropy.time.Time
|
|
473
|
+
Unique timestamps sorted and filtered based on the timestamp tolerance.
|
|
474
|
+
"""
|
|
475
|
+
# Collect all start and end times in MJD (days)
|
|
476
|
+
timestamps = []
|
|
477
|
+
for mon_table in (
|
|
478
|
+
pedestal_image_table,
|
|
479
|
+
flatfield_image_table,
|
|
480
|
+
flatfield_peak_time_table,
|
|
481
|
+
):
|
|
482
|
+
# Append timestamps from the start and end of chunks
|
|
483
|
+
timestamps.append(mon_table["time_start"])
|
|
484
|
+
timestamps.append(mon_table["time_end"])
|
|
485
|
+
# Sort the timestamps
|
|
486
|
+
timestamps = np.concatenate(timestamps)
|
|
487
|
+
timestamps.sort()
|
|
488
|
+
# Filter the timestamps based on the timestamp tolerance
|
|
489
|
+
unique_timestamps = [timestamps[-1]]
|
|
490
|
+
for t in reversed(timestamps[:-1]):
|
|
491
|
+
if (unique_timestamps[-1] - t) > self.timestamp_tolerance:
|
|
492
|
+
unique_timestamps.append(t)
|
|
493
|
+
unique_timestamps.reverse()
|
|
494
|
+
return unique_timestamps
|
|
495
|
+
|
|
496
|
+
def _process_table(self, tel_id, table, interpolator, unique_timestamps):
|
|
497
|
+
"""
|
|
498
|
+
Process the input table.
|
|
499
|
+
|
|
500
|
+
This method processes the input table by renaming columns,
|
|
501
|
+
adjusting the first timestamp, setting outliers to NaNs,
|
|
502
|
+
and applying chunk interpolation.
|
|
503
|
+
|
|
504
|
+
Parameters
|
|
505
|
+
----------
|
|
506
|
+
tel_id : int
|
|
507
|
+
Telescope ID.
|
|
508
|
+
table : astropy.table.Table
|
|
509
|
+
Table containing calibration data (pedestal, flatfield, or time correction).
|
|
510
|
+
interpolator : callable
|
|
511
|
+
Interpolation function to use.
|
|
512
|
+
unique_timestamps : list
|
|
513
|
+
List of unique timestamps for interpolation.
|
|
514
|
+
|
|
515
|
+
Returns
|
|
516
|
+
-------
|
|
517
|
+
Table
|
|
518
|
+
Processed table with interpolated data.
|
|
519
|
+
"""
|
|
520
|
+
# TODO Rename columns in the tables. Do it in the interpolator.
|
|
521
|
+
table.rename_column("time_start", "start_time")
|
|
522
|
+
table.rename_column("time_end", "end_time")
|
|
523
|
+
# Check if first timestamp is within the timestamp tolerance
|
|
524
|
+
if (table["start_time"][0] - unique_timestamps[0]) < self.timestamp_tolerance:
|
|
525
|
+
# Set the first timestamp to the first unique timestamp
|
|
526
|
+
table["start_time"][0] = unique_timestamps[0]
|
|
527
|
+
# Set outliers to NaNs
|
|
528
|
+
for col in ["mean", "median", "std"]:
|
|
529
|
+
table[col][table["outlier_mask"].data] = np.nan
|
|
530
|
+
# Register the table with the interpolator
|
|
531
|
+
interpolator.add_table(tel_id, table)
|
|
532
|
+
# Interpolate data at the unique timestamps
|
|
533
|
+
data = defaultdict(list)
|
|
534
|
+
for time in unique_timestamps:
|
|
535
|
+
data_interpolated = interpolator(tel_id, time)
|
|
536
|
+
for col in ("mean", "median", "std"):
|
|
537
|
+
# Interpolator returns np.nan if the requested time is outside the range.
|
|
538
|
+
# In this case, we set the data to NaN for all channels and pixels
|
|
539
|
+
# to avoid the stack operation below to fail.
|
|
540
|
+
if np.isscalar(data_interpolated[col]) and np.isnan(
|
|
541
|
+
data_interpolated[col]
|
|
542
|
+
):
|
|
543
|
+
data_interpolated[col] = np.full(table[col][0].shape, np.nan)
|
|
544
|
+
data[col].append(data_interpolated[col])
|
|
545
|
+
# Stack the data to a numpy array
|
|
546
|
+
for col in ("mean", "median", "std"):
|
|
547
|
+
data[col] = np.stack(data[col], axis=0)
|
|
548
|
+
return Table(data)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def main():
|
|
552
|
+
# Run the tool
|
|
553
|
+
tool = CameraCalibratorTool()
|
|
554
|
+
tool.run()
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
if __name__ == "main":
|
|
558
|
+
main()
|