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.
Files changed (105) hide show
  1. calibpipe/__init__.py +5 -0
  2. calibpipe/_dev_version/__init__.py +9 -0
  3. calibpipe/_version.py +34 -0
  4. calibpipe/atmosphere/__init__.py +1 -0
  5. calibpipe/atmosphere/atmosphere_containers.py +109 -0
  6. calibpipe/atmosphere/meteo_data_handlers.py +485 -0
  7. calibpipe/atmosphere/models/README.md +14 -0
  8. calibpipe/atmosphere/models/__init__.py +1 -0
  9. calibpipe/atmosphere/models/macobac.ecsv +23 -0
  10. calibpipe/atmosphere/models/reference_MDPs/__init__.py +1 -0
  11. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_intermediate.ecsv +8 -0
  12. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_summer.ecsv +8 -0
  13. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_winter.ecsv +8 -0
  14. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_summer.ecsv +8 -0
  15. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_winter.ecsv +8 -0
  16. calibpipe/atmosphere/models/reference_atmospheres/__init__.py +1 -0
  17. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_intermediate.ecsv +73 -0
  18. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_summer.ecsv +73 -0
  19. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_winter.ecsv +73 -0
  20. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_summer.ecsv +73 -0
  21. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_winter.ecsv +73 -0
  22. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/__init__.py +1 -0
  23. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_intermediate.ecsv +857 -0
  24. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_summer.ecsv +857 -0
  25. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_winter.ecsv +857 -0
  26. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_summer.ecsv +857 -0
  27. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_winter.ecsv +857 -0
  28. calibpipe/atmosphere/templates/request_templates/__init__.py +1 -0
  29. calibpipe/atmosphere/templates/request_templates/copernicus.json +11 -0
  30. calibpipe/atmosphere/templates/request_templates/gdas.json +12 -0
  31. calibpipe/core/__init__.py +39 -0
  32. calibpipe/core/common_metadata_containers.py +198 -0
  33. calibpipe/core/exceptions.py +87 -0
  34. calibpipe/database/__init__.py +24 -0
  35. calibpipe/database/adapter/__init__.py +23 -0
  36. calibpipe/database/adapter/adapter.py +80 -0
  37. calibpipe/database/adapter/database_containers/__init__.py +63 -0
  38. calibpipe/database/adapter/database_containers/atmosphere.py +199 -0
  39. calibpipe/database/adapter/database_containers/common_metadata.py +150 -0
  40. calibpipe/database/adapter/database_containers/container_map.py +59 -0
  41. calibpipe/database/adapter/database_containers/observatory.py +61 -0
  42. calibpipe/database/adapter/database_containers/table_version_manager.py +39 -0
  43. calibpipe/database/adapter/database_containers/throughput.py +30 -0
  44. calibpipe/database/adapter/database_containers/version_control.py +17 -0
  45. calibpipe/database/connections/__init__.py +28 -0
  46. calibpipe/database/connections/calibpipe_database.py +60 -0
  47. calibpipe/database/connections/postgres_utils.py +97 -0
  48. calibpipe/database/connections/sql_connection.py +103 -0
  49. calibpipe/database/connections/user_confirmation.py +19 -0
  50. calibpipe/database/interfaces/__init__.py +71 -0
  51. calibpipe/database/interfaces/hashable_row_data.py +54 -0
  52. calibpipe/database/interfaces/queries.py +180 -0
  53. calibpipe/database/interfaces/sql_column_info.py +67 -0
  54. calibpipe/database/interfaces/sql_metadata.py +6 -0
  55. calibpipe/database/interfaces/sql_table_info.py +131 -0
  56. calibpipe/database/interfaces/table_handler.py +333 -0
  57. calibpipe/database/interfaces/types.py +96 -0
  58. calibpipe/telescope/throughput/containers.py +66 -0
  59. calibpipe/tests/conftest.py +274 -0
  60. calibpipe/tests/data/atmosphere/molecular_atmosphere/__init__.py +0 -0
  61. calibpipe/tests/data/atmosphere/molecular_atmosphere/contemporary_MDP.ecsv +34 -0
  62. calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.csv +852 -0
  63. calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.ecsv +23 -0
  64. calibpipe/tests/data/atmosphere/molecular_atmosphere/merged_file.ecsv +1082 -0
  65. calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_copernicus.ecsv +1082 -0
  66. calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_gdas.ecsv +66 -0
  67. calibpipe/tests/data/atmosphere/molecular_atmosphere/observatory_configurations.json +71 -0
  68. calibpipe/tests/data/utils/__init__.py +0 -0
  69. calibpipe/tests/data/utils/meteo_data_winter_and_summer.ecsv +12992 -0
  70. calibpipe/tests/test_conftest_data.py +200 -0
  71. calibpipe/tests/unittests/array/test_cross_calibration.py +412 -0
  72. calibpipe/tests/unittests/atmosphere/astral_testing.py +107 -0
  73. calibpipe/tests/unittests/atmosphere/test_meteo_data_handler.py +775 -0
  74. calibpipe/tests/unittests/atmosphere/test_molecular_atmosphere.py +327 -0
  75. calibpipe/tests/unittests/database/test_table_handler.py +163 -0
  76. calibpipe/tests/unittests/database/test_types.py +38 -0
  77. calibpipe/tests/unittests/telescope/camera/test_calculate_camcalib_coefficients.py +456 -0
  78. calibpipe/tests/unittests/telescope/camera/test_produce_camcalib_test_data.py +37 -0
  79. calibpipe/tests/unittests/telescope/throughput/test_muon_throughput_calibrator.py +693 -0
  80. calibpipe/tests/unittests/test_bootstrap_db.py +79 -0
  81. calibpipe/tests/unittests/utils/test_observatory.py +309 -0
  82. calibpipe/tools/atmospheric_base_tool.py +78 -0
  83. calibpipe/tools/atmospheric_model_db_loader.py +181 -0
  84. calibpipe/tools/basic_tool_with_db.py +38 -0
  85. calibpipe/tools/camcalib_test_data.py +374 -0
  86. calibpipe/tools/camera_calibrator.py +462 -0
  87. calibpipe/tools/contemporary_mdp_producer.py +87 -0
  88. calibpipe/tools/init_db.py +37 -0
  89. calibpipe/tools/macobac_calculator.py +82 -0
  90. calibpipe/tools/molecular_atmospheric_model_producer.py +197 -0
  91. calibpipe/tools/muon_throughput_calculator.py +219 -0
  92. calibpipe/tools/observatory_data_db_loader.py +71 -0
  93. calibpipe/tools/reference_atmospheric_model_selector.py +201 -0
  94. calibpipe/tools/telescope_cross_calibration_calculator.py +721 -0
  95. calibpipe/utils/__init__.py +10 -0
  96. calibpipe/utils/observatory.py +486 -0
  97. calibpipe/utils/observatory_containers.py +26 -0
  98. calibpipe/version.py +24 -0
  99. ctao_calibpipe-0.3.0rc2.dist-info/METADATA +92 -0
  100. ctao_calibpipe-0.3.0rc2.dist-info/RECORD +105 -0
  101. ctao_calibpipe-0.3.0rc2.dist-info/WHEEL +5 -0
  102. ctao_calibpipe-0.3.0rc2.dist-info/entry_points.txt +12 -0
  103. ctao_calibpipe-0.3.0rc2.dist-info/licenses/AUTHORS.md +13 -0
  104. ctao_calibpipe-0.3.0rc2.dist-info/licenses/LICENSE +21 -0
  105. ctao_calibpipe-0.3.0rc2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,456 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test calibpipe-calculate-camcalib-coefficients tool
4
+ """
5
+
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import pytest
11
+ import tables
12
+ import yaml
13
+ from astropy import units as u
14
+ from astropy.time import Time
15
+ from calibpipe.tools.camera_calibrator import CameraCalibratorTool
16
+ from ctapipe.core import ToolConfigurationError, run_tool
17
+ from ctapipe.instrument import SubarrayDescription
18
+ from ctapipe.io import read_table, write_table
19
+ from ctapipe.io.hdf5dataformat import (
20
+ DL1_CAMERA_COEFFICIENTS_GROUP,
21
+ DL1_PIXEL_STATISTICS_GROUP,
22
+ DL1_TEL_IMAGES_GROUP,
23
+ SIMULATION_GROUP,
24
+ )
25
+ from ctapipe.tools.calculate_pixel_stats import PixelStatisticsCalculatorTool
26
+ from ctapipe.tools.merge import MergeTool
27
+ from ctapipe.tools.process import ProcessorTool
28
+ from traitlets.config.loader import Config
29
+
30
+ # Get the path to the configuration files
31
+ CONFIG_PATH = Path(__file__).parent.joinpath(
32
+ "../../../../../../docs/source/user_guide/telescope/camera/configuration/"
33
+ )
34
+
35
+
36
+ @pytest.fixture(scope="session")
37
+ def camera_test_data_dir(
38
+ flatfield_file, pedestal_file, calibpipe_test_data_dir, tmp_path_factory
39
+ ):
40
+ """
41
+ Fixture providing camera test data files with proper naming.
42
+
43
+ This fixture creates a temporary directory with the test files
44
+ named as expected by the camera calibration tests.
45
+ """
46
+ temp_dir = tmp_path_factory.mktemp("camera_test_data")
47
+
48
+ # Copy files with the expected names
49
+ shutil.copy2(flatfield_file, temp_dir / "flatfield_LST_dark.simtel.gz")
50
+ shutil.copy2(pedestal_file, temp_dir / "pedestal_LST_dark.simtel.gz")
51
+
52
+ # Copy the pre-computed stats files for high_stats tests
53
+ for mode in ["single_chunk", "same_chunks", "different_chunks"]:
54
+ stats_file = (
55
+ calibpipe_test_data_dir
56
+ / "telescope"
57
+ / "camera"
58
+ / f"high_statsagg_v1_{mode}.dl1.h5"
59
+ )
60
+ if stats_file.exists():
61
+ shutil.copy2(stats_file, temp_dir / f"high_statsagg_v1_{mode}.dl1.h5")
62
+
63
+ return temp_dir
64
+
65
+
66
+ # The telescope ID to be used in the tests
67
+ TEL_ID = 1
68
+ # The monitoring groups to be used in the tests
69
+ CAMERA_MONITORING_GROUPS = [
70
+ "sky_pedestal_image",
71
+ "flatfield_image",
72
+ "flatfield_peak_time",
73
+ ]
74
+ # Define reference time and trigger rate for the tests. These values
75
+ # are used to create realistic timestamps for the aggregated chunks.
76
+ REFERENCE_TIME = Time.now()
77
+ REFERENCE_TRIGGER_RATE = 1000.0 * u.Hz
78
+ # Simulated values for the tests for the different gain channels
79
+ # HG: High Gain, LG: Low Gain
80
+ EXPECTED_ADC_OFFSET = {"HG": 400, "LG": 400}
81
+ EXPECTED_DC2PE = {"HG": 0.015, "LG": 0.25}
82
+ EXPECTED_TIME_SHIFT = {"HG": 0.0, "LG": 0.0}
83
+ # Set different file prefixes and tolerances for the two statistic modes
84
+ # - 'low_stats': 100 events per calibration type processed from simtel files to the final camcalib
85
+ # coefficients to ensure that the camcalib coefficients calculation works from DL0.
86
+ # Due to the low number of events, the tolerance for the coefficients is relatively high,
87
+ # since the statistics are not sufficient to calculate the coefficients with high precision.
88
+ # - 'high_stats': 25000 events per calibration type already aggregated from simtel files via
89
+ # the 'CamCalibTestDataTool' and retrieved from MinIO. The final camcalib coefficients
90
+ # are calculated from the aggregated statistics files to test the correctness
91
+ # of the camcalib coefficients calculation within a restrictive tolerance.
92
+ FILE_PREFIX = {"low_stats": "low_statsagg_", "high_stats": "high_statsagg_v1_"}
93
+ DC2PE_TOLERANCE = {
94
+ "low_stats": {"rtol": 0.25, "atol": 0.0},
95
+ "high_stats": {"rtol": 0.02, "atol": 0.0},
96
+ }
97
+ ADC_OFFSET_TOLERANCE = {
98
+ "low_stats": {"rtol": 0.0, "atol": 10.0},
99
+ "high_stats": {"rtol": 0.0, "atol": 2.0},
100
+ }
101
+ TIME_SHIFT_TOLERANCE = {
102
+ "low_stats": {"rtol": 0.0, "atol": 0.25},
103
+ "high_stats": {"rtol": 0.0, "atol": 0.25},
104
+ }
105
+
106
+
107
+ @pytest.mark.order(1)
108
+ def test_produce_dl1_image_file(camera_test_data_dir):
109
+ """
110
+ Produce DL1A file containing the images of the calibration events.
111
+ """
112
+ # Set the path to the simtel calibration events
113
+ for calibration_type in ["pedestal", "flatfield"]:
114
+ simtel_file = camera_test_data_dir.joinpath(
115
+ f"{calibration_type}_LST_dark.simtel.gz"
116
+ )
117
+ # Set the output file path for pedestal images
118
+ image_file = camera_test_data_dir.joinpath(f"{calibration_type}_events.dl1.h5")
119
+ with open(
120
+ CONFIG_PATH.joinpath(f"ctapipe_process_{calibration_type}.yaml")
121
+ ) as yaml_file:
122
+ config = yaml.safe_load(yaml_file)
123
+ # Run the ProcessorTool to create pedestal images
124
+ assert (
125
+ run_tool(
126
+ ProcessorTool(config=Config(config)),
127
+ argv=[
128
+ f"--input={simtel_file}",
129
+ f"--output={image_file}",
130
+ "--overwrite",
131
+ ],
132
+ cwd=camera_test_data_dir,
133
+ )
134
+ == 0
135
+ )
136
+
137
+
138
+ @pytest.mark.order(2)
139
+ @pytest.mark.verifies_usecase("UC-120-2.21")
140
+ @pytest.mark.parametrize(
141
+ "aggregation_mode",
142
+ ["single_chunk", "same_chunks", "different_chunks"],
143
+ )
144
+ def test_stats_aggregation(aggregation_mode, camera_test_data_dir):
145
+ """
146
+ DL1 camera monitoring file containing the statistics aggregation for a given chunk mode.
147
+ """
148
+ # Loop over the monitoring groups and calculate pixel statistics
149
+ output_files = []
150
+ for mon_group in CAMERA_MONITORING_GROUPS:
151
+ # Set the output file path for the statistics aggregation
152
+ output_file = camera_test_data_dir.joinpath(
153
+ f"low_statsagg_{mon_group}_{aggregation_mode}.dl1.h5"
154
+ )
155
+ # Save the output file path to the list to be used later in the merge tool
156
+ output_files.append(str(output_file))
157
+ # Set the input file path for the PixelStatisticsCalculator
158
+ dl1_image_file = (
159
+ camera_test_data_dir.joinpath("pedestal_events.dl1.h5")
160
+ if mon_group == "sky_pedestal_image"
161
+ else camera_test_data_dir.joinpath("flatfield_events.dl1.h5")
162
+ )
163
+ # Get the standard configuration for the PixelStatisticsCalculator
164
+ with open(
165
+ CONFIG_PATH.joinpath(f"ctapipe_calculate_pixel_stats_{mon_group}.yaml")
166
+ ) as yaml_file:
167
+ pix_stats_config = yaml.safe_load(yaml_file)
168
+ # Set some additional parameters using cli arguments
169
+ cli_argv = [
170
+ f"--input_url={dl1_image_file}",
171
+ f"--output_path={output_file}",
172
+ ]
173
+ n_events = len(
174
+ read_table(
175
+ dl1_image_file,
176
+ path=f"{DL1_TEL_IMAGES_GROUP}/tel_{TEL_ID:03d}",
177
+ )
178
+ )
179
+ # Modify the configuration for the specific chunk mode
180
+ if aggregation_mode == "single_chunk":
181
+ chunk_duration = 1000.0 * u.s
182
+ # Use a single chunk size for all monitoring groups
183
+ # Overwrite the chunk size for the specific aggregator
184
+ cli_argv.append(f"--SizeChunking.chunk_size={n_events}")
185
+ elif aggregation_mode == "same_chunks":
186
+ chunk_duration = 100.0 * u.s
187
+ # Overwrite the chunk size for the specific aggregators to have ten chunks
188
+ cli_argv.append(f"--SizeChunking.chunk_size={n_events//10}")
189
+ elif aggregation_mode == "different_chunks":
190
+ # Use different chunk sizes for each monitoring group
191
+ if mon_group == "sky_pedestal_image":
192
+ chunk_duration = 200.0 * u.s
193
+ cli_argv.append(f"--SizeChunking.chunk_size={2 * (n_events//10)}")
194
+ elif mon_group == "flatfield_image":
195
+ chunk_duration = 100.0 * u.s
196
+ cli_argv.append(f"--SizeChunking.chunk_size={n_events//10}")
197
+ elif mon_group == "flatfield_peak_time":
198
+ chunk_duration = 500.0 * u.s
199
+ cli_argv.append(f"--SizeChunking.chunk_size={5 * (n_events//10)}")
200
+
201
+ # Run the PixelStatisticsCalculatorTool to calculate pixel statistics
202
+ assert (
203
+ run_tool(
204
+ PixelStatisticsCalculatorTool(config=Config(pix_stats_config)),
205
+ argv=cli_argv,
206
+ cwd=camera_test_data_dir,
207
+ raises=True,
208
+ )
209
+ == 0
210
+ )
211
+ # Overwrite timestamps in the output file to make them realistic
212
+ # Read the created statsagg table for the specific monitoring group
213
+ stats_aggregation_tab = read_table(
214
+ output_file,
215
+ path=f"{DL1_PIXEL_STATISTICS_GROUP}/{mon_group}/tel_{TEL_ID:03d}",
216
+ )
217
+ # Loop over the chunks and set the new timestamps
218
+ for chunk_nr in range(len(stats_aggregation_tab)):
219
+ stats_aggregation_tab["time_start"][chunk_nr] = (
220
+ REFERENCE_TIME
221
+ + (1 / REFERENCE_TRIGGER_RATE).to(u.s)
222
+ + chunk_nr * chunk_duration
223
+ )
224
+ stats_aggregation_tab["time_end"][chunk_nr] = (
225
+ REFERENCE_TIME + (chunk_nr + 1) * chunk_duration
226
+ )
227
+ # Set a different starting time (outside the default 1 second tolerance)
228
+ # for the pedestal group if the mode is 'diffent_chunks'. This is to ensure
229
+ # that the we can later test when the chunk interpolator is returning NaN values
230
+ # for the first and last unique timestamps.
231
+ if aggregation_mode == "different_chunks":
232
+ if mon_group == "sky_pedestal_image":
233
+ stats_aggregation_tab["time_start"][0] -= 2 * u.s
234
+ stats_aggregation_tab["time_end"][-1] += 2 * u.s
235
+ # Overwrite the table in the output file
236
+ write_table(
237
+ stats_aggregation_tab,
238
+ output_file,
239
+ f"{DL1_PIXEL_STATISTICS_GROUP}/{mon_group}/tel_{TEL_ID:03d}",
240
+ overwrite=True,
241
+ )
242
+ # Run the merge tool to combine the statistics
243
+ # from the three files into a single monitoring file
244
+ monitoring_sims_file = camera_test_data_dir.joinpath(
245
+ f"low_statsagg_{aggregation_mode}.dl1.h5"
246
+ )
247
+ run_tool(
248
+ MergeTool(),
249
+ argv=output_files
250
+ + [
251
+ f"--output={monitoring_sims_file}",
252
+ "--monitoring",
253
+ "--single-ob",
254
+ ],
255
+ cwd=camera_test_data_dir,
256
+ raises=True,
257
+ )
258
+ # Also create a monitoring file mimicking a real observation file
259
+ monitoring_obs_file = camera_test_data_dir.joinpath(
260
+ f"low_statsagg_{aggregation_mode}_obs.dl1.h5"
261
+ )
262
+ shutil.copy(monitoring_sims_file, monitoring_obs_file)
263
+ # Remove the simulation to mimic a real observation file
264
+ with tables.open_file(monitoring_obs_file, "r+") as f:
265
+ f.remove_node(SIMULATION_GROUP, recursive=True)
266
+ for monitoring_file in [monitoring_sims_file, monitoring_obs_file]:
267
+ # Check that the output file has been created
268
+ assert monitoring_file.exists()
269
+ for mon_group in CAMERA_MONITORING_GROUPS:
270
+ # Read the monitoring table for the specific monitoring group
271
+ stats_aggregation_tab = read_table(
272
+ monitoring_file,
273
+ path=f"{DL1_PIXEL_STATISTICS_GROUP}/{mon_group}/tel_{TEL_ID:03d}",
274
+ )
275
+ # Check that the timestamps are set correctly
276
+ assert len(stats_aggregation_tab) > 0
277
+ assert np.all(
278
+ stats_aggregation_tab["time_start"] >= REFERENCE_TIME - 2 * u.s
279
+ )
280
+ assert np.all(stats_aggregation_tab["time_end"] >= REFERENCE_TIME)
281
+ # Check that the timestamps are in ascending order
282
+ assert np.all(
283
+ stats_aggregation_tab["time_start"][1:]
284
+ >= stats_aggregation_tab["time_start"][:-1]
285
+ )
286
+
287
+
288
+ # We are ignoring the warning about NaN slices, since we expect all values to be
289
+ # NaN for the first and last timestamps in the 'different_chunks' mode.
290
+ @pytest.mark.order(3)
291
+ @pytest.mark.verifies_usecase("UC-120-2.20")
292
+ @pytest.mark.parametrize(
293
+ "statistic_mode",
294
+ ["low_stats", "high_stats"],
295
+ )
296
+ @pytest.mark.filterwarnings("ignore:All-NaN slice encountered")
297
+ def test_calculate_camcalib_coeffs_tool(statistic_mode, camera_test_data_dir):
298
+ """check camcalib coefficients calculation from dl1 camera monitoring data files"""
299
+ # There are three different aggregation modes:
300
+ # - single_chunk: all monitoring groups are aggregated in a single chunk
301
+ # - same_chunks: all monitoring groups are aggregated in the same chunks
302
+ # - different_chunks: each monitoring group is aggregated in different chunks
303
+ for aggregation_mode in ["single_chunk", "same_chunks", "different_chunks"]:
304
+ # Set the path to the simtel pedestal events
305
+ stats_aggregation_sims_file = camera_test_data_dir.joinpath(
306
+ f"{FILE_PREFIX[statistic_mode]}{aggregation_mode}.dl1.h5"
307
+ )
308
+ # Create a monitoring file mimicking a real observation file
309
+ stats_aggregation_obs_file = camera_test_data_dir.joinpath(
310
+ f"{FILE_PREFIX[statistic_mode]}{aggregation_mode}_obs.dl1.h5"
311
+ )
312
+ shutil.copy(stats_aggregation_sims_file, stats_aggregation_obs_file)
313
+ # Remove the simulation to mimic a real observation file
314
+ with tables.open_file(stats_aggregation_obs_file, "r+") as f:
315
+ f.remove_node(SIMULATION_GROUP, recursive=True)
316
+ for stats_aggregation_file in [
317
+ stats_aggregation_sims_file,
318
+ stats_aggregation_obs_file,
319
+ ]:
320
+ # Run the tool with the configuration and the input file
321
+ assert (
322
+ run_tool(
323
+ CameraCalibratorTool(),
324
+ argv=[
325
+ f"--input_url={stats_aggregation_file}",
326
+ "--overwrite",
327
+ ],
328
+ cwd=camera_test_data_dir,
329
+ raises=True,
330
+ )
331
+ == 0
332
+ )
333
+ # Read subarray description from the created monitoring file
334
+ subarray = SubarrayDescription.from_hdf(stats_aggregation_file)
335
+ # Check for the selected telescope
336
+ assert subarray.tel_ids[0] == TEL_ID
337
+ # Read the camera calibration coefficients from the created monitoring file
338
+ # and check that the calculated values are as expected.
339
+ camcalib_coeffs = read_table(
340
+ stats_aggregation_file,
341
+ path=f"{DL1_CAMERA_COEFFICIENTS_GROUP}/tel_{TEL_ID:03d}",
342
+ )
343
+ for i in range(len(camcalib_coeffs)):
344
+ if (
345
+ aggregation_mode == "different_chunks"
346
+ and stats_aggregation_file == stats_aggregation_obs_file
347
+ ):
348
+ # For the 'different_chunks' mode, we expect the first factor and pedestal
349
+ # to be NaN, since the first timestamp is not valid for the pedestal group.
350
+ if i == 0 or i == len(camcalib_coeffs) - 1:
351
+ # Check that the factor and time shift are NaN for the first and last timestamps
352
+ assert np.isnan(camcalib_coeffs["factor"][i]).all()
353
+ assert np.isnan(camcalib_coeffs["time_shift"][i]).all()
354
+ # Check that the outlier mask is all True for the first and last timestamps
355
+ assert camcalib_coeffs["outlier_mask"][i].all()
356
+ # Check that the is_valid flag is False for the first and last timestamps
357
+ assert not camcalib_coeffs["is_valid"][i]
358
+ # Check that the pedestal offsets are not NaN since the first and last timestamps
359
+ # are valid for the pedestal group.
360
+ for g, gain_channel in enumerate(["HG", "LG"]):
361
+ np.testing.assert_allclose(
362
+ np.nanmedian(camcalib_coeffs["pedestal_offset"][i][g]),
363
+ EXPECTED_ADC_OFFSET[gain_channel],
364
+ rtol=ADC_OFFSET_TOLERANCE[statistic_mode]["rtol"],
365
+ atol=ADC_OFFSET_TOLERANCE[statistic_mode]["atol"],
366
+ err_msg=(
367
+ f"Pedestal per sample values do not match expected values within "
368
+ f"a tolerance of {int(ADC_OFFSET_TOLERANCE[statistic_mode]['atol'])} ADC counts"
369
+ ),
370
+ )
371
+ continue
372
+ # Check that the median of the calculated factor is close to the
373
+ # simtel_dc2pe values for the corresponding gain channel.
374
+ for g, gain_channel in enumerate(["HG", "LG"]):
375
+ np.testing.assert_allclose(
376
+ np.nanmedian(camcalib_coeffs["factor"][i][g]),
377
+ EXPECTED_DC2PE[gain_channel],
378
+ rtol=DC2PE_TOLERANCE[statistic_mode]["rtol"],
379
+ atol=DC2PE_TOLERANCE[statistic_mode]["atol"],
380
+ err_msg=(
381
+ f"Factor coefficients do not match expected values within "
382
+ f"a tolerance of {int(DC2PE_TOLERANCE[statistic_mode]['rtol']*100)}%"
383
+ ),
384
+ )
385
+ # Check that the median of the calculated pedestal offset is close to the
386
+ # simtel_pedestal_per_sample values for the corresponding gain channel.
387
+ np.testing.assert_allclose(
388
+ np.nanmedian(camcalib_coeffs["pedestal_offset"][i][g]),
389
+ EXPECTED_ADC_OFFSET[gain_channel],
390
+ rtol=ADC_OFFSET_TOLERANCE[statistic_mode]["rtol"],
391
+ atol=ADC_OFFSET_TOLERANCE[statistic_mode]["atol"],
392
+ err_msg=(
393
+ f"Pedestal per sample values do not match expected values within "
394
+ f"a tolerance of {int(ADC_OFFSET_TOLERANCE[statistic_mode]['atol'])} ADC counts"
395
+ ),
396
+ )
397
+ # Check that the median of the calculated time shift is close to the
398
+ # simtel_time_shift values for the corresponding gain channel.
399
+ np.testing.assert_allclose(
400
+ np.nanmedian(camcalib_coeffs["time_shift"][i][g]),
401
+ EXPECTED_TIME_SHIFT[gain_channel],
402
+ rtol=TIME_SHIFT_TOLERANCE[statistic_mode]["rtol"],
403
+ atol=TIME_SHIFT_TOLERANCE[statistic_mode]["atol"],
404
+ err_msg=(
405
+ "Time shift values do not match expected values "
406
+ "within a tolerance of a quarter of a waveform sample"
407
+ ),
408
+ )
409
+ # Check that the is_valid flag is True for all timestamps
410
+ assert camcalib_coeffs["is_valid"][i]
411
+
412
+
413
+ def test_npe_std_outlier_detector(camera_test_data_dir):
414
+ """check camcalib coefficients calculation with the NpeStdOutlierDetector"""
415
+ # Only consider the single_chunk aggregation mode for this test
416
+ stats_aggregation_file = camera_test_data_dir.joinpath(
417
+ "high_statsagg_v1_single_chunk.dl1.h5"
418
+ )
419
+ # Read the NpeStdOutlierDetector configuration from the YAML file
420
+ with open(CONFIG_PATH.joinpath("npe_std_outlier_detector.yaml")) as yaml_file:
421
+ npe_std_outlier_detector_config = yaml.safe_load(yaml_file)
422
+ # Run the CameraCalibratorTool with the NpeStdOutlierDetector configuration
423
+ assert (
424
+ run_tool(
425
+ CameraCalibratorTool(config=Config(npe_std_outlier_detector_config)),
426
+ argv=[
427
+ f"--input_url={stats_aggregation_file}",
428
+ "--overwrite",
429
+ ],
430
+ cwd=camera_test_data_dir,
431
+ raises=True,
432
+ )
433
+ == 0
434
+ )
435
+
436
+
437
+ def test_exceptions_camcalibtool(camera_test_data_dir):
438
+ """check exceptions of the CameraCalibratorTool"""
439
+ # Only consider the single_chunk aggregation mode for this test
440
+ stats_aggregation_file = camera_test_data_dir.joinpath(
441
+ "high_statsagg_v1_same_chunks.dl1.h5"
442
+ )
443
+
444
+ with pytest.raises(
445
+ ToolConfigurationError,
446
+ match="CameraCalibratorTool requires exactly one input file.",
447
+ ):
448
+ run_tool(
449
+ CameraCalibratorTool(),
450
+ argv=[
451
+ f"--input_url={stats_aggregation_file}",
452
+ f"--input_url={stats_aggregation_file}",
453
+ ],
454
+ cwd=camera_test_data_dir,
455
+ raises=True,
456
+ )
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test calibpipe-produce-camcalib-test-data tool
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ from calibpipe.tools.camcalib_test_data import CamCalibTestDataTool
9
+ from ctapipe.core import run_tool
10
+
11
+ # Get the path to the configuration files
12
+ CONFIG_PATH = Path(__file__).parent.joinpath(
13
+ "../../../../../../docs/source/user_guide/telescope/camera/configuration/"
14
+ )
15
+
16
+
17
+ def test_produce_camcalib_test_data(pedestal_file, flatfield_file, tmp_path):
18
+ """Test the calibpipe-produce-camcalib-test-data tool"""
19
+ # Run the tool with the configuration and the input files
20
+ assert (
21
+ run_tool(
22
+ CamCalibTestDataTool(),
23
+ argv=[
24
+ f"--CamCalibTestDataTool.pedestal_input_url={pedestal_file}",
25
+ f"--CamCalibTestDataTool.flatfield_input_url={flatfield_file}",
26
+ f"--CamCalibTestDataTool.output_dir={tmp_path}",
27
+ f"--CamCalibTestDataTool.process_pedestal_config={CONFIG_PATH.joinpath('ctapipe_process_pedestal.yaml')}",
28
+ f"--CamCalibTestDataTool.process_flatfield_config={CONFIG_PATH.joinpath('ctapipe_process_flatfield.yaml')}",
29
+ f"--CamCalibTestDataTool.agg_stats_sky_pedestal_image_config={CONFIG_PATH.joinpath('ctapipe_calculate_pixel_stats_sky_pedestal_image.yaml')}",
30
+ f"--CamCalibTestDataTool.agg_stats_flatfield_image_config={CONFIG_PATH.joinpath('ctapipe_calculate_pixel_stats_flatfield_image.yaml')}",
31
+ f"--CamCalibTestDataTool.agg_stats_flatfield_peak_time_config={CONFIG_PATH.joinpath('ctapipe_calculate_pixel_stats_flatfield_peak_time.yaml')}",
32
+ ],
33
+ cwd=tmp_path,
34
+ raises=True,
35
+ )
36
+ == 0
37
+ )