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.

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.0rc1.dist-info}/METADATA +3 -2
  19. {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/RECORD +24 -14
  20. {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/WHEEL +1 -1
  21. {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/entry_points.txt +4 -0
  22. {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/licenses/AUTHORS.md +0 -0
  23. {ctao_calibpipe-0.1.0rc8.dist-info → ctao_calibpipe-0.2.0rc1.dist-info}/licenses/LICENSE +0 -0
  24. {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()