gammasimtools 0.9.0__py3-none-any.whl → 0.11.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.
Files changed (135) hide show
  1. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/METADATA +4 -2
  2. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/RECORD +133 -117
  3. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/entry_points.txt +6 -1
  5. simtools/_version.py +9 -4
  6. simtools/applications/calculate_trigger_rate.py +15 -38
  7. simtools/applications/convert_all_model_parameters_from_simtel.py +9 -29
  8. simtools/applications/convert_geo_coordinates_of_array_elements.py +47 -45
  9. simtools/applications/convert_model_parameter_from_simtel.py +2 -3
  10. simtools/applications/db_add_file_to_db.py +1 -3
  11. simtools/applications/db_add_simulation_model_from_repository_to_db.py +110 -0
  12. simtools/applications/db_add_value_from_json_to_db.py +1 -2
  13. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +6 -6
  14. simtools/applications/db_get_file_from_db.py +11 -12
  15. simtools/applications/db_get_parameter_from_db.py +26 -35
  16. simtools/applications/derive_mirror_rnda.py +1 -2
  17. simtools/applications/derive_photon_electron_spectrum.py +99 -0
  18. simtools/applications/derive_psf_parameters.py +1 -0
  19. simtools/applications/docs_produce_array_element_report.py +71 -0
  20. simtools/applications/docs_produce_model_parameter_reports.py +63 -0
  21. simtools/applications/generate_array_config.py +17 -17
  22. simtools/applications/generate_corsika_histograms.py +2 -2
  23. simtools/applications/generate_regular_arrays.py +19 -17
  24. simtools/applications/generate_simtel_array_histograms.py +11 -48
  25. simtools/applications/production_derive_limits.py +95 -0
  26. simtools/applications/production_generate_simulation_config.py +37 -33
  27. simtools/applications/production_scale_events.py +4 -9
  28. simtools/applications/run_application.py +165 -0
  29. simtools/applications/simulate_light_emission.py +0 -4
  30. simtools/applications/simulate_prod.py +1 -1
  31. simtools/applications/simulate_prod_htcondor_generator.py +26 -26
  32. simtools/applications/submit_data_from_external.py +12 -4
  33. simtools/applications/submit_model_parameter_from_external.py +18 -11
  34. simtools/applications/validate_camera_efficiency.py +2 -2
  35. simtools/applications/validate_file_using_schema.py +26 -22
  36. simtools/camera/single_photon_electron_spectrum.py +168 -0
  37. simtools/configuration/commandline_parser.py +37 -1
  38. simtools/configuration/configurator.py +8 -10
  39. simtools/constants.py +10 -3
  40. simtools/corsika/corsika_config.py +19 -17
  41. simtools/corsika/corsika_histograms.py +5 -7
  42. simtools/corsika/corsika_histograms_visualize.py +2 -4
  43. simtools/data_model/data_reader.py +0 -3
  44. simtools/data_model/metadata_collector.py +20 -12
  45. simtools/data_model/metadata_model.py +8 -124
  46. simtools/data_model/model_data_writer.py +81 -75
  47. simtools/data_model/schema.py +220 -0
  48. simtools/data_model/validate_data.py +79 -68
  49. simtools/db/db_handler.py +350 -492
  50. simtools/db/db_model_upload.py +139 -0
  51. simtools/dependencies.py +112 -0
  52. simtools/io_operations/hdf5_handler.py +54 -24
  53. simtools/layout/array_layout.py +38 -32
  54. simtools/model/array_model.py +13 -7
  55. simtools/model/model_parameter.py +55 -54
  56. simtools/model/site_model.py +2 -2
  57. simtools/production_configuration/calculate_statistical_errors_grid_point.py +119 -145
  58. simtools/production_configuration/event_scaler.py +9 -35
  59. simtools/production_configuration/generate_simulation_config.py +9 -44
  60. simtools/production_configuration/interpolation_handler.py +9 -15
  61. simtools/production_configuration/limits_calculation.py +202 -0
  62. simtools/reporting/docs_read_parameters.py +310 -0
  63. simtools/runners/corsika_simtel_runner.py +4 -4
  64. simtools/schemas/{integration_tests_config.metaschema.yml → application_workflow.metaschema.yml} +61 -27
  65. simtools/schemas/array_elements.yml +8 -0
  66. simtools/schemas/input/MST_mirror_2f_measurements.schema.yml +39 -0
  67. simtools/schemas/input/single_pe_spectrum.schema.yml +38 -0
  68. simtools/schemas/model_parameter.metaschema.yml +103 -2
  69. simtools/schemas/model_parameter_and_data_schema.metaschema.yml +4 -1
  70. simtools/schemas/model_parameters/array_element_position_utm.schema.yml +1 -1
  71. simtools/schemas/model_parameters/array_window.schema.yml +37 -0
  72. simtools/schemas/model_parameters/asum_clipping.schema.yml +0 -4
  73. simtools/schemas/model_parameters/channels_per_chip.schema.yml +1 -1
  74. simtools/schemas/model_parameters/correct_nsb_spectrum_to_telescope_altitude.schema.yml +1 -1
  75. simtools/schemas/model_parameters/corsika_cherenkov_photon_bunch_size.schema.yml +2 -0
  76. simtools/schemas/model_parameters/corsika_cherenkov_photon_wavelength_range.schema.yml +2 -0
  77. simtools/schemas/model_parameters/corsika_first_interaction_height.schema.yml +2 -0
  78. simtools/schemas/model_parameters/corsika_iact_io_buffer.schema.yml +4 -2
  79. simtools/schemas/model_parameters/corsika_iact_max_bunches.schema.yml +2 -0
  80. simtools/schemas/model_parameters/corsika_iact_split_auto.schema.yml +2 -0
  81. simtools/schemas/model_parameters/corsika_longitudinal_shower_development.schema.yml +2 -0
  82. simtools/schemas/model_parameters/corsika_particle_kinetic_energy_cutoff.schema.yml +2 -0
  83. simtools/schemas/model_parameters/corsika_starting_grammage.schema.yml +2 -0
  84. simtools/schemas/model_parameters/dsum_clipping.schema.yml +0 -2
  85. simtools/schemas/model_parameters/dsum_ignore_below.schema.yml +0 -2
  86. simtools/schemas/model_parameters/dsum_offset.schema.yml +0 -2
  87. simtools/schemas/model_parameters/dsum_pedsub.schema.yml +0 -2
  88. simtools/schemas/model_parameters/dsum_pre_clipping.schema.yml +0 -2
  89. simtools/schemas/model_parameters/dsum_prescale.schema.yml +0 -2
  90. simtools/schemas/model_parameters/dsum_presum_max.schema.yml +0 -2
  91. simtools/schemas/model_parameters/dsum_presum_shift.schema.yml +0 -2
  92. simtools/schemas/model_parameters/dsum_shaping.schema.yml +0 -2
  93. simtools/schemas/model_parameters/dsum_shaping_renormalize.schema.yml +0 -2
  94. simtools/schemas/model_parameters/dsum_threshold.schema.yml +0 -2
  95. simtools/schemas/model_parameters/dsum_zero_clip.schema.yml +0 -2
  96. simtools/schemas/model_parameters/fadc_compensate_pedestal.schema.yml +1 -1
  97. simtools/schemas/model_parameters/fadc_lg_compensate_pedestal.schema.yml +1 -1
  98. simtools/schemas/model_parameters/fadc_noise.schema.yml +3 -3
  99. simtools/schemas/model_parameters/fake_mirror_list.schema.yml +33 -0
  100. simtools/schemas/model_parameters/iobuf_maximum.schema.yml +1 -1
  101. simtools/schemas/model_parameters/iobuf_output_maximum.schema.yml +1 -1
  102. simtools/schemas/model_parameters/laser_photons.schema.yml +2 -2
  103. simtools/schemas/model_parameters/lightguide_efficiency_vs_incidence_angle.schema.yml +1 -1
  104. simtools/schemas/model_parameters/lightguide_efficiency_vs_wavelength.schema.yml +1 -1
  105. simtools/schemas/model_parameters/min_photoelectrons.schema.yml +1 -1
  106. simtools/schemas/model_parameters/min_photons.schema.yml +1 -1
  107. simtools/schemas/model_parameters/random_generator.schema.yml +1 -1
  108. simtools/schemas/model_parameters/sampled_output.schema.yml +1 -1
  109. simtools/schemas/model_parameters/save_pe_with_amplitude.schema.yml +1 -1
  110. simtools/schemas/model_parameters/secondary_mirror_degraded_reflection.schema.yml +1 -1
  111. simtools/schemas/model_parameters/store_photoelectrons.schema.yml +1 -1
  112. simtools/schemas/model_parameters/tailcut_scale.schema.yml +1 -1
  113. simtools/schemas/production_configuration_metrics.schema.yml +68 -0
  114. simtools/schemas/production_tables.schema.yml +41 -0
  115. simtools/simtel/simtel_config_reader.py +1 -2
  116. simtools/simtel/simtel_config_writer.py +6 -8
  117. simtools/simtel/simtel_io_histogram.py +32 -68
  118. simtools/simtel/simtel_io_histograms.py +17 -34
  119. simtools/simtel/simulator_array.py +2 -1
  120. simtools/simtel/simulator_camera_efficiency.py +6 -3
  121. simtools/simtel/simulator_light_emission.py +5 -6
  122. simtools/simtel/simulator_ray_tracing.py +3 -4
  123. simtools/testing/configuration.py +2 -1
  124. simtools/testing/helpers.py +6 -13
  125. simtools/testing/validate_output.py +141 -47
  126. simtools/utils/general.py +114 -14
  127. simtools/utils/names.py +299 -157
  128. simtools/utils/value_conversion.py +17 -13
  129. simtools/version.py +2 -2
  130. simtools/visualization/legend_handlers.py +2 -0
  131. simtools/applications/db_add_model_parameters_from_repository_to_db.py +0 -176
  132. simtools/db/db_array_elements.py +0 -130
  133. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/LICENSE +0 -0
  134. {gammasimtools-0.9.0.dist-info → gammasimtools-0.11.0.dist-info}/top_level.txt +0 -0
  135. /simtools/{camera_efficiency.py → camera/camera_efficiency.py} +0 -0
@@ -67,7 +67,6 @@ class ModelParameter:
67
67
 
68
68
  self._parameters = {}
69
69
  self._simulation_config_parameters = {"corsika": {}, "simtel": {}}
70
- self._derived = None
71
70
  self.collection = collection
72
71
  self.label = label
73
72
  self.model_version = model_version
@@ -77,6 +76,9 @@ class ModelParameter:
77
76
  if array_element_name is not None
78
77
  else None
79
78
  )
79
+ self.design_model = self.db.get_design_model(
80
+ self.model_version, self.name, collection="telescopes"
81
+ )
80
82
  self._config_file_directory = None
81
83
  self._config_file_path = None
82
84
  self._load_parameters_from_db()
@@ -111,12 +113,8 @@ class ModelParameter:
111
113
  """
112
114
  try:
113
115
  return self._parameters[par_name]
114
- except KeyError:
115
- pass
116
- try:
117
- return self.derived[par_name]
118
116
  except (KeyError, ValueError) as e:
119
- msg = f"Parameter {par_name} was not found in the model"
117
+ msg = f"Parameter {par_name} was not found in the model {self.name}, {self.site}."
120
118
  self._logger.error(msg)
121
119
  raise InvalidModelParameterError(msg) from e
122
120
 
@@ -180,7 +178,10 @@ class ModelParameter:
180
178
  _value = self.get_parameter_value(par_name, _parameter)
181
179
 
182
180
  try:
183
- _unit = [item.strip() for item in _parameter.get("unit").split(",")]
181
+ if isinstance(_parameter.get("unit"), str):
182
+ _unit = [item.strip() for item in _parameter.get("unit").split(",")]
183
+ else:
184
+ _unit = _parameter.get("unit")
184
185
 
185
186
  # if there is only one value or the values share one unit
186
187
  if (isinstance(_value, (int | float))) or (len(_value) > len(_unit)):
@@ -241,32 +242,21 @@ class ModelParameter:
241
242
  self._logger.debug(f"Parameter {par_name} does not have a file associated with it.")
242
243
  return False
243
244
 
244
- @property
245
- def derived(self):
246
- """Load the derived values and export them if the class instance hasn't done it yet."""
247
- if self._derived is None:
248
- self._load_derived_values()
249
- self._export_derived_files()
250
- return self._derived
251
-
252
- def _load_derived_values(self):
253
- """Load derived values from the DB."""
254
- self._logger.debug("Reading derived values from DB")
255
- self._derived = self.db.get_derived_values(
256
- self.site,
257
- self.name,
258
- self.model_version,
259
- )
245
+ def get_parameter_version(self, par_name):
246
+ """
247
+ Get version for a given parameter used in the model.
260
248
 
261
- def _export_derived_files(self):
262
- """Write to disk a file from the derived values DB."""
263
- for par_now in self.derived.values():
264
- if par_now.get("File") or par_now.get("file"):
265
- self.db.export_file_db(
266
- db_name=self.db.DB_DERIVED_VALUES,
267
- dest=self.config_file_directory,
268
- file_name=(par_now.get("value") or par_now.get("Value")),
269
- )
249
+ Parameters
250
+ ----------
251
+ par_name: str
252
+ Name of the parameter.
253
+
254
+ Returns
255
+ -------
256
+ str
257
+ parameter version used in the model (eg. '1.0.0')
258
+ """
259
+ return self._get_parameter_dict(par_name)["parameter_version"]
270
260
 
271
261
  def print_parameters(self):
272
262
  """Print parameters and their values for debugging purposes."""
@@ -310,6 +300,21 @@ class ModelParameter:
310
300
  """
311
301
  return self._simulation_config_parameters.get(simulation_software)
312
302
 
303
+ def has_parameter(self, par_name):
304
+ """Check if a parameter exists in the model.
305
+
306
+ Parameters
307
+ ----------
308
+ par_name : str
309
+ Name of the parameter.
310
+
311
+ Returns
312
+ -------
313
+ bool
314
+ True if parameter exists in the model.
315
+ """
316
+ return par_name in self._parameters
317
+
313
318
  def _load_simulation_software_parameter(self):
314
319
  """Read simulation software parameters from DB."""
315
320
  for simulation_software in self._simulation_config_parameters:
@@ -322,10 +327,11 @@ class ModelParameter:
322
327
  simulation_software=simulation_software,
323
328
  )
324
329
  )
325
- except ValueError:
330
+ except ValueError as exc:
326
331
  self._logger.warning(
327
332
  f"No {simulation_software} parameters found for "
328
- f"{self.site}, {self.name} (model version {self.model_version})."
333
+ f"{self.site}, {self.name} (model version {self.model_version}). "
334
+ f" (Query {exc})"
329
335
  )
330
336
 
331
337
  def _load_parameters_from_db(self):
@@ -335,15 +341,18 @@ class ModelParameter:
335
341
 
336
342
  if self.name is not None:
337
343
  self._parameters = self.db.get_model_parameters(
338
- self.site, self.name, self.model_version, self.collection, only_applicable=True
344
+ self.site, self.name, self.collection, self.model_version
339
345
  )
340
346
 
341
347
  if self.site is not None:
342
- _site_pars = self.db.get_site_parameters(
343
- self.site, self.model_version, only_applicable=True
348
+ self._parameters.update(
349
+ self.db.get_model_parameters(
350
+ self.site,
351
+ None,
352
+ "sites",
353
+ self.model_version,
354
+ )
344
355
  )
345
- self._parameters.update(_site_pars)
346
-
347
356
  self._load_simulation_software_parameter()
348
357
 
349
358
  def set_extra_label(self, extra_label):
@@ -369,7 +378,7 @@ class ModelParameter:
369
378
  """Return the extra label if defined, if not return ''."""
370
379
  return self._extra_label if self._extra_label is not None else ""
371
380
 
372
- def get_simtel_parameters(self, parameters=None, telescope_model=True, site_model=True):
381
+ def get_simtel_parameters(self, parameters=None):
373
382
  """
374
383
  Get simtel parameters as name and value pairs.
375
384
 
@@ -377,10 +386,6 @@ class ModelParameter:
377
386
  ----------
378
387
  parameters: dict
379
388
  Parameters (simtools) to be renamed (if necessary)
380
- telescope_model: bool
381
- If True, telescope model parameters are included.
382
- site_model: bool
383
- If True, site model parameters are included.
384
389
 
385
390
  Returns
386
391
  -------
@@ -394,10 +399,7 @@ class ModelParameter:
394
399
  _simtel_parameter_value = {}
395
400
  for key in parameters:
396
401
  _par_name = names.get_simulation_software_name_from_parameter_name(
397
- key,
398
- simulation_software="sim_telarray",
399
- search_telescope_parameters=telescope_model,
400
- search_site_parameters=site_model,
402
+ key, simulation_software="sim_telarray"
401
403
  )
402
404
  if _par_name is not None:
403
405
  _simtel_parameter_value[_par_name] = parameters[key].get("value")
@@ -441,8 +443,7 @@ class ModelParameter:
441
443
  )
442
444
 
443
445
  self._logger.debug(
444
- f"Changing parameter {par_name} "
445
- f"from {self.get_parameter_value(par_name)} to {value}"
446
+ f"Changing parameter {par_name} from {self.get_parameter_value(par_name)} to {value}"
446
447
  )
447
448
  self._parameters[par_name]["value"] = value
448
449
 
@@ -513,7 +514,7 @@ class ModelParameter:
513
514
  for par in self._added_parameter_files:
514
515
  pars_from_db.pop(par)
515
516
 
516
- self.db.export_model_files(pars_from_db, self.config_file_directory)
517
+ self.db.export_model_files(parameters=pars_from_db, dest=self.config_file_directory)
517
518
  self._is_exported_model_files_up_to_date = True
518
519
 
519
520
  def get_model_file_as_table(self, par_name):
@@ -535,7 +536,7 @@ class ModelParameter:
535
536
  _par_entry[par_name] = self._parameters[par_name]
536
537
  except KeyError as exc:
537
538
  raise ValueError(f"Parameter {par_name} not found in the model.") from exc
538
- self.db.export_model_files(_par_entry, self.config_file_directory)
539
+ self.db.export_model_files(parameters=_par_entry, dest=self.config_file_directory)
539
540
  if _par_entry[par_name]["value"].endswith("ecsv"):
540
541
  return Table.read(
541
542
  self.config_file_directory.joinpath(_par_entry[par_name]["value"]),
@@ -620,7 +621,7 @@ class ModelParameter:
620
621
  Model directory to export the file to.
621
622
  """
622
623
  self.db.export_model_files(
623
- {
624
+ parameters={
624
625
  "nsb_spectrum_at_2200m": {
625
626
  "value": self._simulation_config_parameters["simtel"][
626
627
  "correct_nsb_spectrum_to_telescope_altitude"
@@ -628,5 +629,5 @@ class ModelParameter:
628
629
  "file": True,
629
630
  }
630
631
  },
631
- model_directory,
632
+ dest=model_directory,
632
633
  )
@@ -151,11 +151,11 @@ class SiteModel(ModelParameter):
151
151
  Model directory to export the file to.
152
152
  """
153
153
  self.db.export_model_files(
154
- {
154
+ parameters={
155
155
  "atmospheric_transmission_file": {
156
156
  "value": self.get_parameter_value("atmospheric_profile"),
157
157
  "file": True,
158
158
  }
159
159
  },
160
- model_directory,
160
+ dest=model_directory,
161
161
  )
@@ -1,13 +1,4 @@
1
- """
2
- Provides functionality to evaluate statistical uncertainties from DL2 MC event files.
3
-
4
- Classes
5
- -------
6
- StatisticalErrorEvaluator
7
- Handles error calculation for given DL2 MC event files and specified metrics.
8
-
9
-
10
- """
1
+ """Evaluate statistical uncertainties from DL2 MC event files."""
11
2
 
12
3
  import logging
13
4
 
@@ -15,7 +6,7 @@ import numpy as np
15
6
  from astropy import units as u
16
7
  from astropy.io import fits
17
8
 
18
- _logger = logging.getLogger(__name__)
9
+ __all__ = ["StatisticalErrorEvaluator"]
19
10
 
20
11
 
21
12
  class StatisticalErrorEvaluator:
@@ -41,26 +32,13 @@ class StatisticalErrorEvaluator:
41
32
  metrics: dict[str, float],
42
33
  grid_point: tuple[float, float, float, float, float] | None = None,
43
34
  ):
44
- """
45
- Init the evaluator with a DL2 MC event file, its type, and metrics to calculate.
46
-
47
- Parameters
48
- ----------
49
- file_path : str
50
- The path to the DL2 MC event file.
51
- file_type : str
52
- The type of the file ('point-like' or 'cone').
53
- metrics : dict, optional
54
- Dictionary specifying which metrics to compute and their reference values.
55
- grid_point : tuple, optional
56
- Tuple specifying the grid point (energy, azimuth, zenith, NSB, offset).
57
- """
58
- self.file_path = file_path
35
+ """Init the evaluator with a DL2 MC event file, its type, and metrics to calculate."""
36
+ self._logger = logging.getLogger(__name__)
59
37
  self.file_type = file_type
60
38
  self.metrics = metrics
61
39
  self.grid_point = grid_point
62
40
 
63
- self.data = self.load_data_from_file()
41
+ self.data = self.load_data_from_file(file_path)
64
42
 
65
43
  self.uncertainty_effective_area = None
66
44
  self.energy_estimate = None
@@ -70,7 +48,58 @@ class StatisticalErrorEvaluator:
70
48
  self.metric_results = None
71
49
  self.energy_threshold = None
72
50
 
73
- def load_data_from_file(self):
51
+ def _load_event_data(self, hdul, data_type):
52
+ """
53
+ Load data and units for the event and simulated data data.
54
+
55
+ Parameters
56
+ ----------
57
+ hdul : HDUList
58
+ The HDUList object.
59
+ data_type: str
60
+ The type of data to load ('EVENTS' or 'SIMULATED EVENTS').
61
+
62
+ Returns
63
+ -------
64
+ dict
65
+ Dictionary containing units for the event data.
66
+ """
67
+ _data = hdul[data_type].data # pylint: disable=E1101
68
+ _header = hdul[data_type].header # pylint: disable=E1101
69
+ _units = {}
70
+ for idx, col_name in enumerate(_data.columns.names, start=1):
71
+ unit_key = f"TUNIT{idx}"
72
+ if unit_key in _header:
73
+ _units[col_name] = u.Unit(_header[unit_key])
74
+ else:
75
+ _units[col_name] = None
76
+ return _data, _units
77
+
78
+ def _set_grid_point(self, events_data):
79
+ """Set azimuth/zenith angle of grid point."""
80
+ unique_azimuths = np.unique(events_data["PNT_AZ"]) * u.deg
81
+ unique_zeniths = 90 * u.deg - np.unique(events_data["PNT_ALT"]) * u.deg
82
+ if len(unique_azimuths) > 1 or len(unique_zeniths) > 1:
83
+ msg = (
84
+ f"Multiple values found for azimuth ({unique_azimuths}) zenith ({unique_zeniths})."
85
+ )
86
+ self._logger.error(msg)
87
+ raise ValueError(msg)
88
+ if self.grid_point is not None:
89
+ self._logger.warning(
90
+ f"Grid point already set to: {self.grid_point}. "
91
+ "Overwriting with new values from file."
92
+ )
93
+ self.grid_point = (
94
+ 1 * u.TeV,
95
+ unique_azimuths[0],
96
+ unique_zeniths[0],
97
+ 0,
98
+ 0 * u.deg,
99
+ )
100
+ self._logger.info(f"Grid point values: {self.grid_point}")
101
+
102
+ def load_data_from_file(self, file_path):
74
103
  """
75
104
  Load data from the DL2 MC event file and return dictionaries with units.
76
105
 
@@ -81,91 +110,24 @@ class StatisticalErrorEvaluator:
81
110
  """
82
111
  data = {}
83
112
  try:
84
- with fits.open(self.file_path) as hdul:
85
- events_data = hdul["EVENTS"].data # pylint: disable=E1101
86
- sim_events_data = hdul["SIMULATED EVENTS"].data # pylint: disable=E1101
87
- event_units = {}
88
- for idx, col_name in enumerate(events_data.columns.names, start=1):
89
- unit_key = f"TUNIT{idx}"
90
- if unit_key in hdul["EVENTS"].header: # pylint: disable=E1101
91
- event_units[col_name] = u.Unit(
92
- hdul["EVENTS"].header[unit_key] # pylint: disable=E1101
93
- )
94
- else:
95
- event_units[col_name] = None
96
-
97
- sim_units = {}
98
- for idx, col_name in enumerate(sim_events_data.columns.names, start=1):
99
- unit_key = f"TUNIT{idx}"
100
- if unit_key in hdul["SIMULATED EVENTS"].header: # pylint: disable=E1101
101
- sim_units[col_name] = u.Unit(
102
- hdul["SIMULATED EVENTS"].header[unit_key] # pylint: disable=E1101
103
- )
104
- else:
105
- sim_units[col_name] = None
106
- # dl2 files are required to have units for these entries
107
- event_energies_reco = events_data["ENERGY"] * event_units["ENERGY"]
108
-
109
- event_energies_mc = events_data["MC_ENERGY"] * event_units["MC_ENERGY"]
110
-
111
- bin_edges_low = sim_events_data["MC_ENERG_LO"] * sim_units["MC_ENERG_LO"]
112
-
113
- bin_edges_high = sim_events_data["MC_ENERG_HI"] * sim_units["MC_ENERG_HI"]
114
-
115
- simulated_event_histogram = sim_events_data["EVENTS"] * u.count
116
-
117
- viewcone = hdul[3].data["viewcone"][0][1] # pylint: disable=E1101
118
- core_range = hdul[3].data["core_range"][0][1] # pylint: disable=E1101
113
+ with fits.open(file_path) as hdul:
114
+ events_data, event_units = self._load_event_data(hdul, "EVENTS")
115
+ sim_events_data, sim_units = self._load_event_data(hdul, "SIMULATED EVENTS")
119
116
 
120
117
  data = {
121
- "event_energies_reco": event_energies_reco,
122
- "event_energies_mc": event_energies_mc,
123
- "bin_edges_low": bin_edges_low,
124
- "bin_edges_high": bin_edges_high,
125
- "simulated_event_histogram": simulated_event_histogram,
126
- "viewcone": viewcone,
127
- "core_range": core_range,
118
+ "event_energies_reco": events_data["ENERGY"] * event_units["ENERGY"],
119
+ "event_energies_mc": events_data["MC_ENERGY"] * event_units["MC_ENERGY"],
120
+ "bin_edges_low": sim_events_data["MC_ENERG_LO"] * sim_units["MC_ENERG_LO"],
121
+ "bin_edges_high": sim_events_data["MC_ENERG_HI"] * sim_units["MC_ENERG_HI"],
122
+ "simulated_event_histogram": sim_events_data["EVENTS"] * u.count,
123
+ "viewcone": hdul[3].data["viewcone"][0][1], # pylint: disable=E1101
124
+ "core_range": hdul[3].data["core_range"][0][1], # pylint: disable=E1101
128
125
  }
129
- unique_azimuths = np.unique(events_data["PNT_AZ"]) * u.deg
130
- unique_zeniths = 90 * u.deg - np.unique(events_data["PNT_ALT"]) * u.deg
131
- if self.grid_point is None:
132
- _logger.info(f"Unique azimuths: {unique_azimuths}")
133
- _logger.info(f"Unique zeniths: {unique_zeniths}")
134
-
135
- if len(unique_azimuths) == 1 and len(unique_zeniths) == 1:
136
- _logger.info(
137
- f"Setting initial grid point with azimuth: {unique_azimuths[0]}"
138
- f" zenith: {unique_zeniths[0]}"
139
- )
140
- self.grid_point = (
141
- 1 * u.TeV,
142
- unique_azimuths[0],
143
- unique_zeniths[0],
144
- 0,
145
- 0 * u.deg,
146
- ) # Initialize grid point with azimuth and zenith
147
- else:
148
- msg = "Multiple unique values found for azimuth or zenith."
149
- _logger.error(msg)
150
- raise ValueError(msg)
151
- else:
152
- _logger.warning(
153
- f"Grid point already set to: {self.grid_point}. "
154
- "Overwriting with new values from file."
155
- )
156
-
157
- self.grid_point = (
158
- 1 * u.TeV,
159
- unique_azimuths[0],
160
- unique_zeniths[0],
161
- 0,
162
- 0 * u.deg,
163
- )
164
- _logger.info(f"New grid point values: {self.grid_point}")
126
+ self._set_grid_point(events_data)
165
127
 
166
128
  except FileNotFoundError as e:
167
- error_message = f"Error loading file {self.file_path}: {e}"
168
- _logger.error(error_message)
129
+ error_message = f"Error loading file {file_path}: {e}"
130
+ self._logger.error(error_message)
169
131
  raise FileNotFoundError(error_message) from e
170
132
  return data
171
133
 
@@ -183,9 +145,9 @@ class StatisticalErrorEvaluator:
183
145
  bin_edges = np.concatenate([bin_edges_low, [bin_edges_high[-1]]])
184
146
  return np.unique(bin_edges)
185
147
 
186
- def compute_triggered_event_histogram(self, event_energies_reco, bin_edges):
148
+ def compute_reconstructed_event_histogram(self, event_energies_reco, bin_edges):
187
149
  """
188
- Compute histogram for triggered events.
150
+ Compute histogram of events as function of reconstructed energy.
189
151
 
190
152
  Parameters
191
153
  ----------
@@ -196,24 +158,26 @@ class StatisticalErrorEvaluator:
196
158
 
197
159
  Returns
198
160
  -------
199
- triggered_event_histogram : array
200
- Histogram of triggered events.
161
+ reconstructed_event_histogram : array
162
+ Histogram of reconstructed events.
201
163
  """
202
164
  event_energies_reco = event_energies_reco.to(bin_edges.unit)
203
165
 
204
- triggered_event_histogram, _ = np.histogram(event_energies_reco.value, bins=bin_edges.value)
205
- return triggered_event_histogram * u.count
166
+ reconstructed_event_histogram, _ = np.histogram(
167
+ event_energies_reco.value, bins=bin_edges.value
168
+ )
169
+ return reconstructed_event_histogram * u.count
206
170
 
207
- def compute_efficiency_and_errors(self, triggered_event_counts, simulated_event_counts):
171
+ def compute_efficiency_and_errors(self, reconstructed_event_counts, simulated_event_counts):
208
172
  """
209
- Compute trigger efficiency and its statistical error using the binomial distribution.
173
+ Compute reconstructed event efficiency and its uncertainty assuming binomial distribution.
210
174
 
211
175
  Parameters
212
176
  ----------
213
- triggered_event_counts : array with units
214
- Histogram counts of the triggered events.
177
+ reconstructed_event_counts : array with units
178
+ Histogram counts of reconstructed events.
215
179
  simulated_event_counts : array with units
216
- Histogram counts of the simulated events.
180
+ Histogram counts of simulated events.
217
181
 
218
182
  Returns
219
183
  -------
@@ -223,32 +187,39 @@ class StatisticalErrorEvaluator:
223
187
  Array of relative uncertainties.
224
188
  """
225
189
  # Ensure the inputs have compatible units
226
- triggered_event_counts = triggered_event_counts.to(u.ct)
227
- simulated_event_counts = simulated_event_counts.to(u.ct)
190
+ reconstructed_event_counts = (
191
+ reconstructed_event_counts.to(u.ct)
192
+ if isinstance(reconstructed_event_counts, u.Quantity)
193
+ else reconstructed_event_counts * u.ct
194
+ )
195
+ simulated_event_counts = (
196
+ simulated_event_counts.to(u.ct)
197
+ if isinstance(simulated_event_counts, u.Quantity)
198
+ else simulated_event_counts * u.ct
199
+ )
200
+
201
+ if np.any(reconstructed_event_counts > simulated_event_counts):
202
+ raise ValueError("Reconstructed event counts exceed simulated event counts.")
228
203
 
229
204
  # Compute efficiencies, ensuring the output is dimensionless
230
205
  efficiencies = np.divide(
231
- triggered_event_counts,
206
+ reconstructed_event_counts,
232
207
  simulated_event_counts,
233
- out=np.zeros_like(triggered_event_counts),
208
+ out=np.zeros_like(reconstructed_event_counts),
234
209
  where=simulated_event_counts > 0,
235
210
  ).to(u.dimensionless_unscaled)
236
211
 
237
212
  # Set up a mask for valid data with a unit-consistent threshold
238
- if np.any(triggered_event_counts > simulated_event_counts):
239
- raise ValueError(
240
- "Triggered event counts exceed simulated event counts. Please check input data."
241
- )
242
- valid = (simulated_event_counts > 0 * u.ct) & (triggered_event_counts > 0 * u.ct)
213
+ valid = (simulated_event_counts > 0) & (reconstructed_event_counts > 0)
243
214
 
244
- uncertainties = np.zeros_like(triggered_event_counts.value) * u.dimensionless_unscaled
215
+ uncertainties = np.zeros_like(reconstructed_event_counts.value) * u.dimensionless_unscaled
245
216
 
246
217
  if np.any(valid):
247
218
  uncertainties[valid] = np.sqrt(
248
219
  np.maximum(
249
220
  simulated_event_counts[valid]
250
- / triggered_event_counts[valid]
251
- * (1 - triggered_event_counts[valid] / simulated_event_counts[valid]),
221
+ / reconstructed_event_counts[valid]
222
+ * (1 - reconstructed_event_counts[valid] / simulated_event_counts[valid]),
252
223
  0,
253
224
  )
254
225
  )
@@ -273,13 +244,13 @@ class StatisticalErrorEvaluator:
273
244
  Energy threshold value.
274
245
  """
275
246
  bin_edges = self.create_bin_edges()
276
- triggered_event_histogram = self.compute_triggered_event_histogram(
247
+ reconstructed_event_histogram = self.compute_reconstructed_event_histogram(
277
248
  self.data["event_energies_mc"], bin_edges
278
249
  )
279
250
  simulated_event_histogram = self.data["simulated_event_histogram"]
280
251
 
281
252
  efficiencies, _ = self.compute_efficiency_and_errors(
282
- triggered_event_histogram, simulated_event_histogram
253
+ reconstructed_event_histogram, simulated_event_histogram
283
254
  )
284
255
 
285
256
  # Determine the effective area threshold (10% of max effective area)
@@ -302,12 +273,12 @@ class StatisticalErrorEvaluator:
302
273
  Dictionary with uncertainties for the file.
303
274
  """
304
275
  bin_edges = self.create_bin_edges()
305
- triggered_event_histogram = self.compute_triggered_event_histogram(
276
+ reconstructed_event_histogram = self.compute_reconstructed_event_histogram(
306
277
  self.data["event_energies_mc"], bin_edges
307
278
  )
308
279
  simulated_event_histogram = self.data["simulated_event_histogram"]
309
280
  _, relative_errors = self.compute_efficiency_and_errors(
310
- triggered_event_histogram, simulated_event_histogram
281
+ reconstructed_event_histogram, simulated_event_histogram
311
282
  )
312
283
  return {"relative_errors": relative_errors}
313
284
 
@@ -326,7 +297,10 @@ class StatisticalErrorEvaluator:
326
297
  event_energies_mc = self.data["event_energies_mc"]
327
298
 
328
299
  if len(event_energies_reco) != len(event_energies_mc):
329
- raise ValueError(f"Mismatch in the number of energies for file {self.file_path}")
300
+ raise ValueError(
301
+ f"Mismatch in the number of energies: {len(event_energies_reco)} vs "
302
+ f"{len(event_energies_mc)}"
303
+ )
330
304
 
331
305
  energy_deviation = (event_energies_reco - event_energies_mc) / event_energies_mc
332
306
 
@@ -351,15 +325,15 @@ class StatisticalErrorEvaluator:
351
325
  def calculate_metrics(self):
352
326
  """Calculate all defined metrics as specified in self.metrics and store results."""
353
327
  if "uncertainty_effective_area" in self.metrics:
354
-
355
328
  self.uncertainty_effective_area = self.calculate_uncertainty_effective_area()
356
329
  if self.uncertainty_effective_area:
357
- validity_range = self.metrics.get("uncertainty_effective_area", {}).get(
358
- "valid_range"
330
+ energy_range = self.metrics.get("uncertainty_effective_area", {}).get(
331
+ "energy_range"
332
+ )
333
+ min_energy, max_energy = (
334
+ energy_range["value"][0] * u.Unit(energy_range["unit"]),
335
+ energy_range["value"][1] * u.Unit(energy_range["unit"]),
359
336
  )
360
- min_energy, max_energy = validity_range["value"][0] * u.Unit(
361
- validity_range["unit"]
362
- ), validity_range["value"][1] * u.Unit(validity_range["unit"])
363
337
 
364
338
  valid_errors = [
365
339
  error
@@ -375,7 +349,7 @@ class StatisticalErrorEvaluator:
375
349
  ref_value = self.metrics.get("uncertainty_effective_area", {}).get("target_error")[
376
350
  "value"
377
351
  ]
378
- _logger.info(
352
+ self._logger.info(
379
353
  f"Effective Area Error (max in validity range): "
380
354
  f"{self.uncertainty_effective_area['max_error'].value:.6f}, "
381
355
  f"Reference: {ref_value:.3f}"
@@ -386,7 +360,7 @@ class StatisticalErrorEvaluator:
386
360
  self.calculate_energy_estimate()
387
361
  )
388
362
  ref_value = self.metrics.get("energy_estimate", {}).get("target_error")["value"]
389
- _logger.info(
363
+ self._logger.info(
390
364
  f"Energy Estimate Error: {self.energy_estimate:.3f}, Reference: {ref_value:.3f}"
391
365
  )
392
366
  else:
@@ -442,7 +416,7 @@ class StatisticalErrorEvaluator:
442
416
  overall_max_errors[metric_name] = result
443
417
  else:
444
418
  raise ValueError(f"Unsupported result type for {metric_name}: {type(result)}")
445
- _logger.info(f"overall_max_errors {overall_max_errors}")
419
+ self._logger.info(f"overall_max_errors {overall_max_errors}")
446
420
  all_max_errors = list(overall_max_errors.values())
447
421
  if metric == "average":
448
422
  overall_metric = np.mean(all_max_errors)