gammasimtools 0.25.0__py3-none-any.whl → 0.27.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 (138) hide show
  1. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/METADATA +6 -1
  2. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/RECORD +135 -130
  3. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/WHEEL +1 -1
  4. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/entry_points.txt +3 -2
  5. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/licenses/LICENSE +1 -1
  6. simtools/_version.py +2 -2
  7. simtools/application_control.py +35 -7
  8. simtools/applications/convert_geo_coordinates_of_array_elements.py +3 -3
  9. simtools/applications/db_add_file_to_db.py +1 -1
  10. simtools/applications/db_add_simulation_model_from_repository_to_db.py +1 -1
  11. simtools/applications/db_add_value_from_json_to_db.py +1 -1
  12. simtools/applications/db_generate_compound_indexes.py +1 -1
  13. simtools/applications/db_get_array_layouts_from_db.py +3 -7
  14. simtools/applications/db_get_file_from_db.py +1 -1
  15. simtools/applications/db_get_parameter_from_db.py +1 -1
  16. simtools/applications/db_inspect_databases.py +1 -1
  17. simtools/applications/db_upload_model_repository.py +1 -1
  18. simtools/applications/derive_ctao_array_layouts.py +1 -2
  19. simtools/applications/{calculate_incident_angles.py → derive_incident_angle.py} +16 -18
  20. simtools/applications/derive_mirror_rnda.py +112 -180
  21. simtools/applications/derive_psf_parameters.py +0 -1
  22. simtools/applications/derive_pulse_shape_parameters.py +0 -1
  23. simtools/applications/derive_trigger_rates.py +1 -1
  24. simtools/applications/docs_produce_array_element_report.py +2 -8
  25. simtools/applications/docs_produce_calibration_reports.py +1 -3
  26. simtools/applications/docs_produce_model_parameter_reports.py +0 -2
  27. simtools/applications/docs_produce_simulation_configuration_report.py +1 -3
  28. simtools/applications/generate_array_config.py +0 -1
  29. simtools/applications/generate_corsika_histograms.py +79 -229
  30. simtools/applications/generate_regular_arrays.py +76 -69
  31. simtools/applications/generate_simtel_event_data.py +2 -2
  32. simtools/applications/maintain_simulation_model_add_production.py +2 -2
  33. simtools/applications/maintain_simulation_model_write_array_element_positions.py +87 -0
  34. simtools/applications/plot_array_layout.py +5 -111
  35. simtools/applications/plot_simulated_event_distributions.py +57 -0
  36. simtools/applications/plot_tabular_data.py +0 -1
  37. simtools/applications/plot_tabular_data_for_model_parameter.py +1 -6
  38. simtools/applications/production_derive_corsika_limits.py +1 -1
  39. simtools/applications/production_generate_grid.py +0 -1
  40. simtools/applications/run_application.py +1 -1
  41. simtools/applications/simulate_flasher.py +3 -15
  42. simtools/applications/simulate_illuminator.py +2 -11
  43. simtools/applications/simulate_pedestals.py +1 -5
  44. simtools/applications/simulate_prod.py +8 -11
  45. simtools/applications/simulate_prod_htcondor_generator.py +1 -1
  46. simtools/applications/submit_array_layouts.py +2 -4
  47. simtools/applications/submit_data_from_external.py +2 -1
  48. simtools/applications/submit_model_parameter_from_external.py +1 -3
  49. simtools/applications/validate_camera_efficiency.py +28 -28
  50. simtools/applications/validate_camera_fov.py +0 -1
  51. simtools/applications/validate_cumulative_psf.py +1 -5
  52. simtools/applications/validate_optics.py +2 -14
  53. simtools/atmosphere.py +83 -0
  54. simtools/camera/camera_efficiency.py +171 -53
  55. simtools/camera/single_photon_electron_spectrum.py +8 -7
  56. simtools/configuration/commandline_parser.py +82 -11
  57. simtools/configuration/configurator.py +6 -11
  58. simtools/constants.py +5 -0
  59. simtools/corsika/corsika_config.py +100 -202
  60. simtools/corsika/corsika_histograms.py +561 -1708
  61. simtools/corsika/primary_particle.py +1 -1
  62. simtools/data_model/metadata_collector.py +5 -2
  63. simtools/data_model/metadata_model.py +0 -4
  64. simtools/data_model/model_data_writer.py +59 -64
  65. simtools/data_model/schema.py +2 -0
  66. simtools/data_model/validate_data.py +1 -3
  67. simtools/db/db_handler.py +23 -10
  68. simtools/db/mongo_db.py +2 -2
  69. simtools/dependencies.py +81 -38
  70. simtools/io/ascii_handler.py +55 -5
  71. simtools/io/io_handler.py +23 -12
  72. simtools/io/table_handler.py +1 -1
  73. simtools/job_execution/job_manager.py +154 -79
  74. simtools/job_execution/process_pool.py +137 -0
  75. simtools/layout/array_layout.py +4 -13
  76. simtools/layout/array_layout_utils.py +348 -57
  77. simtools/model/array_model.py +23 -63
  78. simtools/model/calibration_model.py +4 -8
  79. simtools/model/legacy_model_parameter.py +134 -0
  80. simtools/model/model_parameter.py +147 -86
  81. simtools/model/model_utils.py +40 -6
  82. simtools/model/site_model.py +4 -8
  83. simtools/model/telescope_model.py +10 -16
  84. simtools/production_configuration/derive_corsika_limits.py +6 -11
  85. simtools/production_configuration/interpolation_handler.py +16 -16
  86. simtools/ray_tracing/incident_angles.py +92 -17
  87. simtools/ray_tracing/mirror_panel_psf.py +338 -222
  88. simtools/ray_tracing/psf_analysis.py +62 -48
  89. simtools/ray_tracing/psf_parameter_optimisation.py +3 -3
  90. simtools/ray_tracing/ray_tracing.py +43 -25
  91. simtools/reporting/docs_auto_report_generator.py +8 -13
  92. simtools/reporting/docs_read_parameters.py +2 -8
  93. simtools/runners/corsika_runner.py +52 -195
  94. simtools/runners/corsika_simtel_runner.py +77 -108
  95. simtools/runners/runner_services.py +214 -213
  96. simtools/runners/simtel_runner.py +27 -160
  97. simtools/runners/simtools_runner.py +11 -73
  98. simtools/schemas/application_workflow.metaschema.yml +8 -0
  99. simtools/settings.py +173 -0
  100. simtools/{io/eventio_handler.py → sim_events/file_info.py} +3 -3
  101. simtools/{simtel/simtel_io_event_histograms.py → sim_events/histograms.py} +25 -15
  102. simtools/{simtel/simtel_io_event_reader.py → sim_events/reader.py} +20 -17
  103. simtools/{simtel/simtel_io_event_writer.py → sim_events/writer.py} +84 -25
  104. simtools/simtel/pulse_shapes.py +7 -2
  105. simtools/simtel/simtel_config_writer.py +79 -91
  106. simtools/simtel/simtel_seeds.py +184 -0
  107. simtools/simtel/simtel_table_reader.py +6 -4
  108. simtools/simtel/simulator_array.py +114 -109
  109. simtools/simtel/simulator_camera_efficiency.py +68 -46
  110. simtools/simtel/simulator_light_emission.py +164 -132
  111. simtools/simtel/simulator_ray_tracing.py +80 -71
  112. simtools/simulator.py +137 -355
  113. simtools/telescope_trigger_rates.py +3 -4
  114. simtools/testing/assertions.py +84 -33
  115. simtools/testing/configuration.py +1 -2
  116. simtools/testing/helpers.py +2 -3
  117. simtools/testing/log_inspector.py +1 -0
  118. simtools/testing/sim_telarray_metadata.py +14 -12
  119. simtools/testing/validate_output.py +121 -42
  120. simtools/utils/general.py +43 -17
  121. simtools/utils/geometry.py +0 -77
  122. simtools/utils/names.py +5 -5
  123. simtools/utils/random.py +36 -0
  124. simtools/visualization/legend_handlers.py +7 -6
  125. simtools/visualization/plot_array_layout.py +91 -16
  126. simtools/visualization/plot_corsika_histograms.py +145 -605
  127. simtools/visualization/plot_incident_angles.py +48 -1
  128. simtools/visualization/plot_mirrors.py +1 -4
  129. simtools/visualization/plot_pixels.py +2 -4
  130. simtools/visualization/plot_psf.py +160 -19
  131. simtools/visualization/plot_simtel_event_histograms.py +4 -4
  132. simtools/visualization/plot_simtel_events.py +6 -11
  133. simtools/visualization/plot_tables.py +8 -19
  134. simtools/visualization/visualize.py +22 -2
  135. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +0 -160
  136. simtools/applications/print_version.py +0 -53
  137. simtools/io/hdf5_handler.py +0 -139
  138. {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/top_level.txt +0 -0
@@ -1,52 +1,27 @@
1
- """Extract Cherenkov photons information from a CORSIKA IACT file."""
1
+ """Extract Cherenkov photons from a CORSIKA IACT file and fill histograms."""
2
2
 
3
- import functools
4
3
  import logging
5
- import operator
6
- import re
7
- import time
8
4
  from pathlib import Path
9
5
 
10
6
  import boost_histogram as bh
11
7
  import numpy as np
12
8
  from astropy import units as u
13
- from astropy.io.misc import yaml
14
- from astropy.units import cds
15
- from corsikaio.subblocks import event_header, get_units_from_fields, run_header
16
- from ctapipe.io import write_table
17
9
  from eventio import IACTFile
18
10
 
19
- from simtools import version
20
- from simtools.io import io_handler
21
- from simtools.io.ascii_handler import collect_data_from_file
22
- from simtools.io.hdf5_handler import fill_hdf5_table
23
- from simtools.utils.geometry import convert_2d_to_radial_distr, rotate
24
- from simtools.utils.names import sanitize_name
25
- from simtools.visualization import plot_corsika_histograms as visualize
26
-
27
- X_AXIS_STRING = "x axis"
28
- Y_AXIS_STRING = "y axis"
29
- Z_AXIS_STRING = "z axis"
30
-
31
-
32
- class HistogramNotCreatedError(Exception):
33
- """Exception for histogram not created."""
11
+ from simtools.utils.geometry import rotate
34
12
 
35
13
 
36
14
  class CorsikaHistograms:
37
15
  """
38
- Extracts the Cherenkov photons information from a CORSIKA IACT file.
16
+ Extract Cherenkov photons from a CORSIKA IACT file and fill histograms.
39
17
 
40
18
  Parameters
41
19
  ----------
42
20
  input_file: str or Path
43
- CORSIKA IACT file provided by the CORSIKA simulation.
44
- label: str
45
- Instance label.
46
- output_path: str
47
- Path where to save the output of the class methods.
48
- hdf5_file_name: str
49
- HDF5 file name for histogram storage.
21
+ CORSIKA IACT file.
22
+ axis_distance: astropy.units.Quantity or float
23
+ Distance from the axis to consider when calculating the lateral density profiles
24
+ along x and y axes. If a float is given, it is assumed to be in meters.
50
25
 
51
26
  Raises
52
27
  ------
@@ -54,569 +29,99 @@ class CorsikaHistograms:
54
29
  if the input file given does not exist.
55
30
  """
56
31
 
57
- def __init__(self, input_file, label=None, output_path=None, hdf5_file_name=None):
58
- self.label = label
32
+ def __init__(self, input_file, normalization_method="per-telescope", axis_distance=1000 * u.m):
59
33
  self._logger = logging.getLogger(__name__)
60
- self._logger.debug("Init CorsikaHistograms")
61
- self.input_file = input_file
62
-
63
- self.input_file = Path(self.input_file)
34
+ self.input_file = Path(input_file)
64
35
  if not self.input_file.exists():
65
- msg = f"file {self.input_file} does not exist."
66
- self._logger.error(msg)
67
- raise FileNotFoundError
68
-
69
- self.io_handler = io_handler.IOHandler()
70
- _default_output_path = self.io_handler.get_output_directory("corsika")
71
-
72
- if output_path is None:
73
- self.output_path = _default_output_path
74
- else:
75
- self.output_path = Path(output_path)
76
-
77
- if hdf5_file_name is None:
78
- self.hdf5_file_name = re.split(r"\.", self.input_file.name)[0] + ".hdf5"
79
- else:
80
- self.hdf5_file_name = hdf5_file_name
81
-
82
- self._telescope_indices = None
83
- self._telescope_positions = None
84
- self.num_events = None
85
- self.num_of_hist = None
86
- self.num_telescopes = None
87
- self._num_photons_per_event_per_telescope = None
88
- self._num_photons_per_event = None
89
- self._num_photons_per_telescope = None
90
- self.__meta_dict = None
91
- self._dict_2d_distributions = None
92
- self._dict_1d_distributions = None
93
- self._event_azimuth_angles = None
94
- self._event_zenith_angles = None
95
- self._hist_config = None
96
- self._total_num_photons = None
97
- self._magnetic_field_x = None
98
- self._magnetic_field_z = None
99
- self._event_total_energies = None
100
- self._event_first_interaction_heights = None
101
- self._corsika_version = None
102
- self.event_information = None
103
- self._individual_telescopes = None
104
- self._allowed_histograms = {"hist_position", "hist_direction", "hist_time_altitude"}
105
- self._allowed_1d_labels = {"wavelength", "time", "altitude"}
106
- self._allowed_2d_labels = {"counts", "density", "direction", "time_altitude"}
107
- self._header = None
108
- self.hist_position = None
109
- self.hist_direction = None
110
- self.hist_time_altitude = None
111
-
112
- self.read_event_information()
113
- self._initialize_header()
114
-
115
- def parse_telescope_indices(self, indices_arg):
116
- """Return telescope indices as ndarray[int] or None.
117
-
118
- Accepts None, a sequence of strings/ints. Raises ValueError on invalid input.
119
- """
120
- if indices_arg is None:
121
- return None
122
- try:
123
- return np.array(indices_arg).astype(int)
124
- except ValueError as exc:
125
- msg = (
126
- f"{indices_arg} not a valid input. Please use integer numbers for telescope_indices"
127
- )
128
- self._logger.error(msg)
129
- raise ValueError(msg) from exc
130
-
131
- def should_overwrite(
132
- self, write_hdf5: bool, event1d: list | None, event2d: list | None
133
- ) -> bool:
134
- """Return True if output HDF5 exists and any writing flag is requested."""
135
- exists = Path(self.hdf5_file_name).exists()
136
- if exists and (write_hdf5 or bool(event1d) or bool(event2d)):
137
- self._logger.warning(
138
- f"Output hdf5 file {self.hdf5_file_name} already exists. Overwriting it."
139
- )
140
- return True
141
- return False
36
+ raise FileNotFoundError(f"File {self.input_file} does not exist.")
142
37
 
143
- def run_export_pipeline(
144
- self,
145
- *,
146
- individual_telescopes: bool,
147
- hist_config,
148
- indices_arg,
149
- write_pdf: bool,
150
- write_hdf5: bool,
151
- event1d: list | None,
152
- event2d: list | None,
153
- test: bool = False,
154
- ) -> dict:
155
- """Run the full histogram export pipeline and return output artifact paths.
156
-
157
- Returns a dict with optional keys: pdf_photons, pdf_event1d, pdf_event2d.
158
- """
159
- outputs: dict[str, Path | None] = {
160
- "pdf_photons": None,
161
- "pdf_event_1d": None,
162
- "pdf_event_2d": None,
163
- }
164
-
165
- indices = self.parse_telescope_indices(indices_arg)
166
- overwrite = self.should_overwrite(write_hdf5, event1d, event2d)
167
-
168
- self.set_histograms(
169
- telescope_indices=indices,
170
- individual_telescopes=individual_telescopes,
171
- hist_config=hist_config,
38
+ self.axis_distance = (
39
+ axis_distance.to(u.m).value if isinstance(axis_distance, u.Quantity) else axis_distance
172
40
  )
41
+ self.events = None
42
+ self.hist = self._set_2d_distributions()
43
+ self.hist.update(self._set_1d_distributions())
44
+ self._density_samples = []
45
+ self.normalization_method = normalization_method
173
46
 
174
- if write_pdf:
175
- pdf_path = visualize.export_all_photon_figures_pdf(self, test=test)
176
- outputs["pdf_photons"] = pdf_path
177
- if write_hdf5:
178
- self.export_histograms(overwrite=overwrite)
179
-
180
- if event1d is not None:
181
- outputs["pdf_event_1d"] = visualize.derive_event_1d_histograms(
182
- self, event1d, pdf=write_pdf, hdf5=write_hdf5, overwrite=not write_hdf5
183
- )
184
- if event2d is not None:
185
- outputs["pdf_event_2d"] = visualize.derive_event_2d_histograms(
186
- self,
187
- event2d,
188
- pdf=write_pdf,
189
- hdf5=write_hdf5,
190
- overwrite=not (write_hdf5 or bool(event1d)),
191
- )
192
-
193
- return outputs
194
-
195
- @property
196
- def hdf5_file_name(self):
197
- """
198
- Property for the hdf5 file name.
199
-
200
- The idea of this property is to allow setting (or changing) the name of the hdf5 file
201
- even after creating the class instance.
202
- """
203
- return self._hdf5_file_name
204
-
205
- @hdf5_file_name.setter
206
- def hdf5_file_name(self, hdf5_file_name):
207
- """
208
- Set the hdf5_file_name to the argument passed.
209
-
210
- Parameters
211
- ----------
212
- hdf5_file_name: str
213
- The name of hdf5 file to be set.
214
- """
215
- self._hdf5_file_name = Path(self.output_path).joinpath(hdf5_file_name).absolute().as_posix()
216
-
217
- @property
218
- def corsika_version(self):
219
- """
220
- Get the version of the CORSIKA IACT file.
221
-
222
- Returns
223
- -------
224
- float:
225
- The version of CORSIKA used to produce the CORSIKA IACT file given by self.input_file.
226
- """
227
- if self._corsika_version is None:
228
- all_corsika_versions = list(run_header.run_header_types.keys())
229
- header = list(self.iact_file.header)
230
-
231
- for i_version in reversed(all_corsika_versions):
232
- # Get the event header for this software version being tested.
233
- single_run_header = run_header.run_header_types[i_version]
234
- # Get the position in the dictionary, where the version is.
235
- version_index_position = np.argwhere(
236
- np.array(list(single_run_header.names)) == "version"
237
- )[0]
238
-
239
- # Check if version tested is the same as the version written in the file header.
240
- if i_version == np.trunc(float(header[version_index_position[0]]) * 10) / 10:
241
- # If the version found is the same as the initial guess, leave the loop,
242
- # otherwise, iterate until we find the correct version.
243
- self._corsika_version = np.around(float(header[version_index_position[0]]), 3)
244
- break
245
- return self._corsika_version
246
-
247
- def _initialize_header(self):
248
- """Initialize the header."""
249
- self.all_run_keys = list(
250
- run_header.run_header_types[np.around(self.corsika_version, 1)].names
251
- )
252
- self._header = {}
47
+ def fill(self):
48
+ """Fill Cherenkov photons histograms."""
49
+ self._read_event_headers()
253
50
 
254
- # Get units of the header
255
- all_run_units = get_units_from_fields(
256
- run_header.run_header_fields[np.trunc(self.corsika_version * 10) / 10]
51
+ with IACTFile(self.input_file) as f:
52
+ telescope_positions = np.array(f.telescope_positions)
53
+ for event_counter, event in enumerate(f):
54
+ if hasattr(event, "photon_bunches"):
55
+ photons = list(event.photon_bunches.values())
56
+ self._fill_histograms(photons, event_counter, telescope_positions, False)
57
+
58
+ self._update_distributions()
59
+
60
+ def _read_event_headers(self):
61
+ """Read event information from headers."""
62
+ event_dtype = np.dtype(
63
+ [
64
+ ("particle_id", "i4"),
65
+ ("total_energy", "f8"),
66
+ ("azimuth_deg", "f8"),
67
+ ("zenith_deg", "f8"),
68
+ ("num_photons", "f8"),
69
+ ]
257
70
  )
258
- all_header_astropy_units = self._get_header_astropy_units(self.all_run_keys, all_run_units)
259
-
260
- # Fill the header dictionary
261
- for i_key, key in enumerate(self.all_run_keys[1:]): # starting at the second
262
- # element to avoid the non-numeric key.
263
- self._header[key] = self.iact_file.header[i_key + 1] * all_header_astropy_units[key]
264
-
265
- @property
266
- def header(self):
267
- """
268
- Get the run header.
269
71
 
270
- Returns
271
- -------
272
- dict:
273
- The run header.
274
- """
275
- return self._header
276
-
277
- def read_event_information(self):
278
- """
279
- Read the information about the events from their headers and save as a class instance.
280
-
281
- The main information can also be fetched individually through the functions below.
282
- For the remaining information (such as px, py, pz), use this function.
283
-
284
- """
285
- if self.event_information is None:
286
- with IACTFile(self.input_file) as self.iact_file:
287
- self.telescope_positions = np.array(self.iact_file.telescope_positions)
288
- self.num_telescopes = np.size(self.telescope_positions, axis=0)
289
- self.all_event_keys = list(
290
- event_header.event_header_types[np.trunc(self.corsika_version * 10) / 10].names
291
- )
292
- all_event_units = get_units_from_fields(
293
- event_header.event_header_fields[np.trunc(self.corsika_version * 10) / 10]
72
+ with IACTFile(self.input_file) as iact_file:
73
+ records = [
74
+ (
75
+ event.header["particle_id"],
76
+ event.header["total_energy"],
77
+ np.rad2deg(event.header["azimuth"]),
78
+ np.rad2deg(event.header["zenith"]),
79
+ 0.0, # filled later when reading photon bunches
294
80
  )
81
+ for event in iact_file
82
+ ]
295
83
 
296
- self.event_information = {key: [] for key in self.all_event_keys}
297
-
298
- self.num_events = 0
299
- # Build a dictionary with the parameters for the events.
300
- for event in self.iact_file:
301
- for i_key, key in enumerate(self.all_event_keys[1:]):
302
- self.event_information[key].append(event.header[i_key + 1])
303
-
304
- self.num_events += 1
84
+ self.events = np.array(records, dtype=event_dtype)
305
85
 
306
- all_event_astropy_units = self._get_header_astropy_units(
307
- self.all_event_keys, all_event_units
308
- )
309
-
310
- # Add the units to dictionary with the parameters and turn it into
311
- # astropy.Quantities.
312
- for i_key, key in enumerate(self.all_event_keys[1:]): # starting at the second
313
- # element to avoid the non-numeric (e.g. 'EVTH') key.
314
- self.event_information[key] = (
315
- np.array(self.event_information[key]) * all_event_astropy_units[key]
316
- )
317
-
318
- def _get_header_astropy_units(self, parameters, non_astropy_units):
86
+ def _create_regular_axes(self, hist, axes):
319
87
  """
320
- Return the dictionary with astropy units from the given list of parameters.
88
+ Create regular axis for a single histogram.
321
89
 
322
90
  Parameters
323
91
  ----------
324
- parameters: list
325
- The list of parameters to extract the astropy units.
326
- non_astropy_units: dict
327
- A dictionary with the parameter units (in strings).
328
-
329
- Returns
330
- -------
331
- dict:
332
- A dictionary with the astropy units.
333
- """
334
- # Build a dictionary with astropy units for the unit of the event's (header's) parameters.
335
- all_event_astropy_units = {}
336
- for key in parameters[1:]: # starting at the second
337
- # element to avoid the non-numeric (e.g. 'EVTH') key.
338
-
339
- # We extract the astropy unit (dimensionless in case no unit is provided).
340
- if key in non_astropy_units:
341
- with cds.enable():
342
- unit = u.Unit(non_astropy_units[key])
343
- else:
344
- unit = u.dimensionless_unscaled
345
- all_event_astropy_units[key] = unit
346
- return all_event_astropy_units
347
-
348
- @property
349
- def telescope_indices(self):
350
- """
351
- The telescope index (or indices), which are considered for the production of the histograms.
92
+ hist: dict
93
+ Histogram dictionary.
94
+ axes: list
95
+ List of axis names (e.g. ["x_bins", "y_bins"]).
352
96
 
353
97
  Returns
354
98
  -------
355
99
  list:
356
- The indices of the telescopes of interest.
357
- """
358
- return self._telescope_indices
359
-
360
- @telescope_indices.setter
361
- def telescope_indices(self, telescope_new_indices):
362
- """
363
- Set the telescope index (or indices).
364
-
365
- If self.individual_telescopes is True, the indices of the telescopes passed are analyzed
366
- individually (different histograms for each telescope) even if all telescopes are listed.
367
-
368
- Parameters
369
- ----------
370
- telescope_new_indices: int or list of int or np.array of int
371
- The indices of the specific telescopes to be inspected. If not specified, all telescopes
372
- are treated together in one histogram and the value of self._telescope_indices is a list
373
- of all telescope indices.
374
-
375
- Raises
376
- ------
377
- TypeError:
378
- if the indices passed through telescope_index are not of type int.
379
- """
380
- if telescope_new_indices is None:
381
- self._telescope_indices = np.arange(self.num_telescopes)
382
- else:
383
- if not isinstance(telescope_new_indices, list | np.ndarray):
384
- telescope_new_indices = np.array([telescope_new_indices])
385
- for i_telescope in telescope_new_indices:
386
- if not isinstance(i_telescope, int | np.integer):
387
- msg = "The index or indices given are not of type int."
388
- self._logger.error(msg)
389
- raise TypeError
390
- self._telescope_indices = np.sort(telescope_new_indices)
391
-
392
- @property
393
- def hist_config(self):
394
- """
395
- The configuration of the histograms.
396
-
397
- Returns
398
- -------
399
- dict:
400
- the dictionary with the histogram configuration.
401
- """
402
- if self._hist_config is None:
403
- msg = (
404
- "No histogram configuration was defined before. The default config is being "
405
- "created now."
406
- )
407
- self._logger.warning(msg)
408
- self._hist_config = self._create_histogram_default_config()
409
- return self._hist_config
410
-
411
- @hist_config.setter
412
- def hist_config(self, input_config):
413
- """
414
- Set the configuration for the histograms (e.g., bin size, min and max values, etc).
415
-
416
- The input is allowed either through a yaml file or a dictionary. If nothing is given,
417
- the dictionary is created with default values.
418
-
419
- Parameters
420
- ----------
421
- input_config: str, Path, dict or NoneType
422
- yaml file with the configuration parameters to create the histograms. For the correct
423
- format, please look at the docstring at _create_histogram_default_config.
424
- Alternatively, it can be a dictionary with the configuration parameters to create
425
- the histograms.
426
- """
427
- if isinstance(input_config, dict):
428
- self._hist_config = input_config
429
- else:
430
- self._hist_config = collect_data_from_file(input_config) if input_config else None
431
-
432
- def hist_config_to_yaml(self, file_name=None):
433
- """
434
- Save the histogram configuration dictionary to a yaml file.
435
-
436
- Parameters
437
- ----------
438
- file_name: str
439
- Name of the output file, in which to save the histogram configuration.
440
-
441
- """
442
- if file_name is None:
443
- file_name = "hist_config"
444
- file_name = Path(file_name).with_suffix(".yml")
445
- output_config_file_name = Path(self.output_path).joinpath(file_name)
446
- with open(output_config_file_name, "w", encoding="utf-8") as file:
447
- yaml.dump(self.hist_config, file)
448
-
449
- def _create_histogram_default_config(self):
450
- """
451
- Create a dictionary with the configuration necessary to create the histograms.
452
-
453
- It is used only in case the configuration is not provided in a yaml file or dict.
454
-
455
- Three histograms are created: hist_position with 3 dimensions (x, y positions and the
456
- wavelength), hist_direction with 2 dimensions (direction cosines in x and y directions),
457
- hist_time_altitude with 2 dimensions (time and altitude of emission).
458
-
459
- Four arguments are passed to each dimension in the dictionary:
460
-
461
- "bins": the number of bins,
462
- "start": the first element of the histogram,
463
- "stop": the last element of the histogram, and
464
- "scale" to define the scale of the bins which can be "linear" or "log". If "log", the
465
- common logarithm (log10) is applied to the axes. The start and stop values have to be
466
- valid, i.e., >0.
467
-
468
- Returns
469
- -------
470
- dict:
471
- Dictionary with the configuration parameters to create the histograms.
472
- """
473
- if self.individual_telescopes is False:
474
- xy_maximum = 1000 * u.m
475
- xy_bin = 100
476
-
477
- else:
478
- xy_maximum = 16 * u.m
479
- xy_bin = 64
480
- return {
481
- "hist_position": {
482
- X_AXIS_STRING: {
483
- "bins": xy_bin,
484
- "start": -xy_maximum,
485
- "stop": xy_maximum,
486
- "scale": "linear",
487
- },
488
- Y_AXIS_STRING: {
489
- "bins": xy_bin,
490
- "start": -xy_maximum,
491
- "stop": xy_maximum,
492
- "scale": "linear",
493
- },
494
- Z_AXIS_STRING: {
495
- "bins": 80,
496
- "start": 200 * u.nm,
497
- "stop": 1000 * u.nm,
498
- "scale": "linear",
499
- },
500
- },
501
- "hist_direction": {
502
- X_AXIS_STRING: {
503
- "bins": 100,
504
- "start": -1,
505
- "stop": 1,
506
- "scale": "linear",
507
- },
508
- Y_AXIS_STRING: {
509
- "bins": 100,
510
- "start": -1,
511
- "stop": 1,
512
- "scale": "linear",
513
- },
514
- },
515
- "hist_time_altitude": {
516
- X_AXIS_STRING: {
517
- "bins": 100,
518
- "start": -2000 * u.ns,
519
- "stop": 2000 * u.ns,
520
- "scale": "linear",
521
- },
522
- Y_AXIS_STRING: {
523
- "bins": 100,
524
- "start": 120 * u.km,
525
- "stop": 0 * u.km,
526
- "scale": "linear",
527
- },
528
- },
529
- }
530
-
531
- def _create_regular_axes(self, label):
532
- """
533
- Create regular axis for histograms.
534
-
535
- Parameters
536
- ----------
537
- label: str
538
- Label to identify to which histogram the new axis belongs.
539
-
540
- Raises
541
- ------
542
- ValueError:
543
- if label is not valid.
100
+ List of boost_histogram axis instances.
544
101
  """
545
102
  transform = {"log": bh.axis.transform.log, "linear": None}
546
103
 
547
- if label not in self._allowed_histograms:
548
- msg = f"allowed labels must be one of the following: {self._allowed_histograms}"
549
- self._logger.error(msg)
550
- raise ValueError(msg)
551
-
552
- all_axes = [X_AXIS_STRING, Y_AXIS_STRING]
553
- if label == "hist_position":
554
- all_axes.append(Z_AXIS_STRING)
555
-
556
104
  boost_axes = []
557
- for axis in all_axes:
558
- if isinstance(self.hist_config[label][axis]["start"], u.quantity.Quantity):
559
- start = self.hist_config[label][axis]["start"].value
560
- stop = self.hist_config[label][axis]["stop"].value
561
- else:
562
- start = self.hist_config[label][axis]["start"]
563
- stop = self.hist_config[label][axis]["stop"]
105
+ for axis in axes:
106
+ bins, start, stop = hist[axis][:3]
107
+ scale = hist[axis][3] if len(hist[axis]) > 3 else "linear"
108
+ if isinstance(start, u.quantity.Quantity):
109
+ start, stop = start.value, stop.value
564
110
  boost_axes.append(
565
111
  bh.axis.Regular(
566
- bins=self.hist_config[label][axis]["bins"],
112
+ bins=bins,
567
113
  start=start,
568
114
  stop=stop,
569
- transform=transform[self.hist_config[label][axis]["scale"]],
115
+ transform=transform[scale],
570
116
  )
571
117
  )
572
118
  return boost_axes
573
119
 
574
- def _create_histograms(self, individual_telescopes=False):
120
+ def _fill_histograms(self, photons, event_counter, telescope_positions, rotate_photons=True):
575
121
  """
576
- Create the histogram instances.
122
+ Fill Cherenkov photon histograms.
577
123
 
578
- Parameters
579
- ----------
580
- individual_telescopes: bool
581
- if False, the histograms are filled for all given telescopes together.
582
- if True, one histogram is set for each telescope separately.
583
- """
584
- self.individual_telescopes = individual_telescopes
585
- self.num_of_hist = len(self.telescope_indices) if self.individual_telescopes is True else 1
586
-
587
- self.hist_position, self.hist_direction, self.hist_time_altitude = [], [], []
588
-
589
- for _ in range(self.num_of_hist):
590
- boost_axes_position = self._create_regular_axes("hist_position")
591
- self.hist_position.append(
592
- bh.Histogram(
593
- boost_axes_position[0],
594
- boost_axes_position[1],
595
- boost_axes_position[2],
596
- )
597
- )
598
- boost_axes_direction = self._create_regular_axes("hist_direction")
599
- self.hist_direction.append(
600
- bh.Histogram(
601
- boost_axes_direction[0],
602
- boost_axes_direction[1],
603
- )
604
- )
605
-
606
- boost_axes_time_altitude = self._create_regular_axes("hist_time_altitude")
607
- self.hist_time_altitude.append(
608
- bh.Histogram(
609
- boost_axes_time_altitude[0],
610
- boost_axes_time_altitude[1],
611
- )
612
- )
613
-
614
- def _fill_histograms(self, photons, rotation_around_z_axis=None, rotation_around_y_axis=None):
615
- """
616
- Fill histograms with the information of the photons on the ground.
617
-
618
- if the azimuth and zenith angles are provided, the Cherenkov photon's coordinates are
619
- filled in the plane perpendicular to the incoming direction of the particle.
124
+ For rotate_photons, the Cherenkov photon's coordinates are filled in the shower plane.
620
125
 
621
126
  Parameters
622
127
  ----------
@@ -630,1240 +135,588 @@ class CorsikaHistograms:
630
135
  incoming direction and the x axis,
631
136
  cy: direction cosine in the y direction, i.e., the cosine of the angle between the
632
137
  incoming direction and the y axis,
633
- time: time of arrival of the photon in ns. The clock starts when the particle crosses
634
- the top of the atmosphere (CORSIKA-defined) if self.event_first_interaction_heights
635
- is positive or at first interaction if otherwise.
138
+ time: time of arrival of the photon in ns.
636
139
  zem: altitude where the photon was generated in cm,
637
140
  photons: number of photons associated to this bunch,
638
- wavelength: the wavelength of the photons in nm.
639
- rotation_around_z_axis: astropy.Quantity
640
- Angle to rotate the observational plane around the Z axis.
641
- It can be passed in radians or degrees.
642
- If not given, no rotation is performed.
643
- rotation_around_y_axis: astropy.Quantity
644
- Angle to rotate the observational plane around the Y axis.
645
- It can be passed in radians or degrees.
646
- If not given, no rotation is performed.
647
- rotation_around_z_axis and rotation_around_y_axis are used to align the observational
648
- plane normal to the incoming direction of the shower particles for the individual
649
- telescopes (useful for fixed targets).
650
-
651
- Raises
652
- ------
653
- IndexError:
654
- If the index or indices passed though telescope_index are out of range.
655
- """
656
- hist_num = 0
657
- for i_tel_info, photons_info in np.array(
658
- list(zip(self.telescope_positions, photons)), dtype=object
659
- )[self.telescope_indices]:
660
- if rotation_around_z_axis is None or rotation_around_y_axis is None:
661
- photon_x, photon_y = photons_info["x"], photons_info["y"]
662
- else:
663
- photon_x, photon_y = rotate(
664
- photons_info["x"],
665
- photons_info["y"],
666
- rotation_around_z_axis,
667
- rotation_around_y_axis,
141
+ event_counter: int
142
+ Event counter.
143
+ telescope_positions: numpy.array
144
+ Array with the telescope positions with shape (M, 2), where M is the number of
145
+ telescopes in the array. The two columns are the x and y positions of the telescopes
146
+ in the CORSIKA coordinate system.
147
+ rotate_photons: bool
148
+ If True, the photon's coordinates are rotated to the plane perpendicular to the
149
+ incoming direction of the primary particle.
150
+ """
151
+ hist_str = "histogram"
152
+ photons_per_telescope = np.zeros(len(telescope_positions))
153
+ zenith_rad = np.deg2rad(self.events["zenith_deg"][event_counter])
154
+
155
+ for tel_idx, (photon, telescope) in enumerate(zip(photons, telescope_positions)):
156
+ if rotate_photons:
157
+ px, py = rotate(
158
+ photon["x"],
159
+ photon["y"],
160
+ self.events["azimuth_deg"][event_counter],
161
+ self.events["zenith_deg"][event_counter],
668
162
  )
669
-
670
- if self.individual_telescopes is False:
671
- # Adding the position of the telescopes to the relative position of the photons
672
- # such that we have a common coordinate system.
673
- photon_x = -i_tel_info["x"] + photon_x
674
- photon_y = -i_tel_info["y"] + photon_y
675
-
676
- self.hist_position[hist_num].fill(
677
- (photon_x * u.cm).to(u.m),
678
- (photon_y * u.cm).to(u.m),
679
- np.abs(photons_info["wavelength"]) * u.nm,
163
+ else:
164
+ px, py = photon["x"], photon["y"]
165
+
166
+ px = px - telescope["x"]
167
+ py = py - telescope["y"]
168
+ w = photon["photons"]
169
+
170
+ pxm = px * u.cm.to(u.m)
171
+ pym = py * u.cm.to(u.m)
172
+ zem = (photon["zem"] * u.cm).to(u.km)
173
+ photons_per_telescope[tel_idx] += np.sum(w)
174
+
175
+ self.hist["counts_xy"][hist_str].fill(pxm, pym, weight=w)
176
+ self.hist["direction_xy"][hist_str].fill(photon["cx"], photon["cy"], weight=w)
177
+ self.hist["time_altitude"][hist_str].fill(photon["time"] * u.ns, zem, weight=w)
178
+ self.hist["wavelength_altitude"][hist_str].fill(
179
+ np.abs(photon["wavelength"]) * u.nm, zem, weight=w
680
180
  )
681
181
 
682
- self.hist_direction[hist_num].fill(photons_info["cx"], photons_info["cy"])
683
- self.hist_time_altitude[hist_num].fill(
684
- photons_info["time"] * u.ns, (photons_info["zem"] * u.cm).to(u.km)
182
+ r = np.hypot(px, py) * u.cm.to(u.m)
183
+ self.hist["counts_r"][hist_str].fill(r, weight=w)
184
+
185
+ self.events["num_photons"][event_counter] += np.sum(w)
186
+
187
+ for tel_idx, telescope in enumerate(telescope_positions):
188
+ area = np.pi * (telescope["r"] ** 2) / np.cos(zenith_rad) / 1.0e4 # in m^2
189
+ n_photons = photons_per_telescope[tel_idx]
190
+ density = n_photons / area if area > 0 else 0.0
191
+ density_error = np.sqrt(n_photons) / area if area > 0 else 0.0
192
+ self._density_samples.append(
193
+ {
194
+ "x": telescope["x"] * u.cm.to(u.m),
195
+ "y": telescope["y"] * u.cm.to(u.m),
196
+ "density": density,
197
+ "density_error": density_error,
198
+ }
685
199
  )
686
- if self.individual_telescopes is True:
687
- hist_num += 1
688
-
689
- def set_histograms(self, telescope_indices=None, individual_telescopes=None, hist_config=None):
690
- """
691
- Create and fill Cherenkov photons histograms using information from the CORSIKA IACT file.
692
-
693
- Parameters
694
- ----------
695
- telescope_indices: int or list of int
696
- The indices of the specific telescopes to be inspected.
697
- individual_telescopes: bool
698
- if False, the histograms are supposed to be filled for all telescopes. Default is False.
699
- if True, one histogram is set for each telescope separately.
700
- hist_config:
701
- yaml file with the configuration parameters to create the histograms. For the correct
702
- format, please look at the docstring of _create_histogram_default_config.
703
- Alternatively, it can be a dictionary with the configuration parameters to create
704
- the histograms.
705
-
706
- Returns
707
- -------
708
- list: list of boost_histogram.Histogram instances.
709
-
710
- Raises
711
- ------
712
- AttributeError:
713
- if event has not photon saved.
714
- """
715
- self.telescope_indices = telescope_indices
716
- self.individual_telescopes = individual_telescopes
717
- self.hist_config = hist_config
718
- self._create_histograms(individual_telescopes=self.individual_telescopes)
719
-
720
- num_photons_per_event_per_telescope_to_set = []
721
- start_time = time.time()
722
- self._logger.debug(f"Starting reading the file at {start_time}.")
723
- with IACTFile(self.input_file) as f:
724
- event_counter = 0
725
- for event in f:
726
- for i_telescope in self.telescope_indices:
727
- if hasattr(event, "photon_bunches"):
728
- photons = list(event.photon_bunches.values())
729
- else:
730
- msg = "The event has no associated photon bunches saved. "
731
- self._logger.error(msg)
732
- raise AttributeError
733
-
734
- # Count photons only from the telescopes given by self.telescope_indices.
735
- num_photons_per_event_per_telescope_to_set.append(event.n_photons[i_telescope])
736
- self._fill_histograms(
737
- photons,
738
- self.event_azimuth_angles[event_counter],
739
- self.event_zenith_angles[event_counter],
740
- )
741
- event_counter += 1
742
- self.num_photons_per_event_per_telescope = num_photons_per_event_per_telescope_to_set
743
- self._logger.debug(
744
- f"Finished reading the file and creating the histograms in {time.time() - start_time} "
745
- f"seconds"
746
- )
747
200
 
748
- @property
749
- def individual_telescopes(self):
750
- """Return the individual telescopes as property."""
751
- return self._individual_telescopes
752
-
753
- @individual_telescopes.setter
754
- def individual_telescopes(self, new_individual_telescopes: bool):
755
- """
756
- Define individual telescopes.
757
-
758
- Parameters
759
- ----------
760
- new_individual_telescopes: bool
761
- if False, the histograms are supposed to be filled for all telescopes.
762
- if True, one histogram is set for each telescope separately.
763
- """
764
- if new_individual_telescopes is None:
765
- self._individual_telescopes = False
766
- else:
767
- self._individual_telescopes = new_individual_telescopes
768
-
769
- def _raise_if_no_histogram(self):
770
- """
771
- Raise an error if the histograms were not created.
772
-
773
- Raises
774
- ------
775
- HistogramNotCreatedError:
776
- if the histogram was not previously created.
777
- """
778
- for histogram in self._allowed_histograms:
779
- if not hasattr(self, histogram) or getattr(self, histogram) is None:
780
- msg = (
781
- "The histograms were not created. Please, use create_histograms to create "
782
- "histograms from the CORSIKA output file."
783
- )
784
- self._logger.error(msg)
785
- raise HistogramNotCreatedError
786
-
787
- def _get_hist_2d_projection(self, label):
201
+ def get_hist_2d_projection(self, hist):
788
202
  """
789
203
  Get 2D distributions.
790
204
 
791
205
  Parameters
792
206
  ----------
793
- label: str
794
- Label to indicate which histogram.
207
+ hist: boost_histogram.Histogram
208
+ Histogram.
795
209
 
796
210
  Returns
797
211
  -------
798
212
  numpy.ndarray
799
- The counts of the histogram.
213
+ Histogram counts.
800
214
  numpy.array
801
- The x bin edges of the histograms.
215
+ Histogram x bin edges.
802
216
  numpy.array
803
- The y bin edges of the histograms.
804
-
805
- Raises
806
- ------
807
- ValueError:
808
- if label is not valid.
809
- """
810
- if label not in self._allowed_2d_labels:
811
- msg = f"label is not valid. Valid entries are {self._allowed_2d_labels}"
812
- self._logger.error(msg)
813
- raise ValueError(msg)
814
- self._raise_if_no_histogram()
815
-
816
- num_telescopes_to_fill = (
817
- len(self.telescope_indices) if self.individual_telescopes is True else 1
818
- )
819
-
820
- x_bin_edges, y_bin_edges, hist_values = [], [], []
821
- for i_telescope in range(num_telescopes_to_fill):
822
- mini_hist = None
823
- if label == "counts":
824
- mini_hist = self.hist_position[i_telescope][:, :, sum]
825
- hist_values.append(mini_hist.view().T)
826
- elif label == "density":
827
- mini_hist = self.hist_position[i_telescope][:, :, sum]
828
- areas = functools.reduce(operator.mul, mini_hist.axes.widths)
829
- hist_values.append(mini_hist.view().T / areas)
830
- elif label == "direction":
831
- mini_hist = self.hist_direction[i_telescope]
832
- hist_values.append(self.hist_direction[i_telescope].view().T)
833
- elif label == "time_altitude":
834
- mini_hist = self.hist_time_altitude[i_telescope]
835
- hist_values.append(self.hist_time_altitude[i_telescope].view().T)
836
- if mini_hist is not None:
837
- x_bin_edges.append(mini_hist.axes.edges[0].flatten())
838
- y_bin_edges.append(mini_hist.axes.edges[1].flatten())
839
-
840
- return np.array(hist_values), np.array(x_bin_edges), np.array(y_bin_edges)
841
-
842
- def get_2d_photon_position_distr(self):
843
- """
844
- Get 2D histograms of position of the Cherenkov photons on the ground.
845
-
846
- Returns
847
- -------
848
- numpy.ndarray
849
- The counts of the histogram.
850
- numpy.array
851
- The x bin edges of the count histograms in x, usually in meters.
852
- numpy.array
853
- The y bin edges of the count histograms in y, usually in meters.
854
- """
855
- return self._get_hist_2d_projection("counts")
856
-
857
- def get_2d_photon_density_distr(self):
858
- """
859
- Get 2D histograms of position of the Cherenkov photons on the ground.
860
-
861
- It returns the photon density per square meter.
862
-
863
- Returns
864
- -------
865
- numpy.ndarray
866
- The values of the histogram, usually in $m^{-2}$
867
- numpy.array
868
- The x bin edges of the density/count histograms in x, usually in meters.
869
- numpy.array
870
- The y bin edges of the density/count histograms in y, usually in meters.
871
- """
872
- return self._get_hist_2d_projection("density")
873
-
874
- def get_2d_photon_direction_distr(self):
875
- """
876
- Get 2D histograms of incoming direction of the Cherenkov photons on the ground.
877
-
878
- Returns
879
- -------
880
- numpy.ndarray
881
- The counts of the histogram.
882
- numpy.array
883
- The x bin edges of the direction histograms in cos(x).
884
- numpy.array
885
- The y bin edges of the direction histograms in cos(y)
886
- """
887
- return self._get_hist_2d_projection("direction")
888
-
889
- def get_2d_photon_time_altitude_distr(self):
890
- """
891
- Get 2D histograms of the time and altitude of the photon production.
892
-
893
- Returns
894
- -------
895
- numpy.ndarray
896
- The counts of the histogram.
897
- numpy.array
898
- The x bin edges of the time_altitude histograms, usually in ns.
899
- numpy.array
900
- The y bin edges of the time_altitude histograms, usually in km.
901
- """
902
- return self._get_hist_2d_projection("time_altitude")
903
-
904
- def get_2d_num_photons_distr(self):
905
- """
906
- Get the distribution of Cherenkov photons per event per telescope.
907
-
908
- It returns the 2D array accounting for the events from the telescopes given
909
- by self.telescope_indices.
910
-
911
- Returns
912
- -------
913
- numpy.ndarray
914
- The counts of the histogram.
915
- numpy.array
916
- An array that counts the telescopes in self.telescope_indices
917
- numpy.array
918
- Number of photons per event per telescope in self.telescope_indices.
919
- """
920
- num_events_array = np.arange(self.num_events + 1).reshape(1, self.num_events + 1)
921
- # It counts only the telescope indices given by self.telescope_indices.
922
- # The + 1 closes the last edge.
923
- telescope_counter = np.arange(len(self.telescope_indices) + 1).reshape(
924
- 1, len(self.telescope_indices) + 1
217
+ Histogram y bin edges.
218
+ numpy.ndarray or None
219
+ Histogram uncertainties (sqrt of variance) if available.
220
+ """
221
+ view = hist.view()
222
+ if self._check_for_all_attributes(view):
223
+ counts = np.asarray([view["value"].T])
224
+ uncertainties = np.asarray([np.sqrt(view["variance"].T)])
225
+ else:
226
+ counts = np.asarray([view.T])
227
+ uncertainties = None
228
+
229
+ x_edges = np.asarray([hist.axes.edges[0].flatten()])
230
+ y_edges = np.asarray([hist.axes.edges[1].flatten()])
231
+
232
+ return counts, x_edges, y_edges, uncertainties
233
+
234
+ def _get_hist_1d_from_numpy(self, label, hist):
235
+ """Get 1D histogram from numpy histogram."""
236
+ bins = hist["x_bins"][0]
237
+ start = hist["x_bins"][1] if hist["x_bins"][1] else np.min(self.events[label])
238
+ stop = hist["x_bins"][2] if hist["x_bins"][2] is not None else np.max(self.events[label])
239
+ scale = hist["x_bins"][3] if len(hist["x_bins"]) > 3 else "linear"
240
+ if scale == "log":
241
+ bin_edges = np.logspace(np.log10(start), np.log10(stop), bins + 1)
242
+ else:
243
+ bin_edges = np.linspace(start, stop, bins + 1)
244
+ histo_1d, _ = np.histogram(self.events[label], bins=bin_edges)
245
+ uncertainties = np.sqrt(histo_1d)
246
+ return (
247
+ histo_1d.reshape(1, bins),
248
+ bin_edges.reshape(1, bins + 1),
249
+ uncertainties.reshape(1, bins),
925
250
  )
926
- hist_2d = np.array(self.num_photons_per_event_per_telescope)
927
- hist_2d = hist_2d.reshape((1, len(self.telescope_indices), self.num_events))
928
- return (hist_2d, num_events_array, telescope_counter)
929
251
 
930
- def _get_hist_1d_projection(self, label):
252
+ def get_hist_1d_projection(self, label, hist):
931
253
  """
932
- Get 1D distributions.
254
+ Get 1D distributions from numpy or boost histograms (1D and 2D).
933
255
 
934
256
  Parameters
935
257
  ----------
936
258
  label: str
937
- Label to indicate which histogram.
259
+ Histogram label.
260
+ hist: dict
261
+ Histogram dictionary.
938
262
 
939
263
  Returns
940
264
  -------
941
265
  numpy.ndarray
942
- The counts of the histogram.
266
+ Histogram counts.
943
267
  numpy.array
944
- The bin edges of the histogram.
945
-
946
- Raises
947
- ------
948
- ValueError:
949
- if label is not valid.
950
- """
951
- if label not in self._allowed_1d_labels:
952
- msg = f"{label} is not valid. Valid entries are {self._allowed_1d_labels}"
953
- self._logger.error(msg)
954
- raise ValueError(msg)
955
- self._raise_if_no_histogram()
956
-
957
- x_bin_edges_list, hist_1d_list = [], []
958
- for i_hist, _ in enumerate(self.hist_position):
959
- mini_hist = None
960
- if label == "wavelength":
961
- mini_hist = self.hist_position[i_hist][sum, sum, :]
962
- elif label == "time":
963
- mini_hist = self.hist_time_altitude[i_hist][:, sum]
964
- elif label == "altitude":
965
- mini_hist = self.hist_time_altitude[i_hist][sum, :]
966
-
967
- x_bin_edges_list.append(mini_hist.axes.edges.T.flatten()[0])
968
- hist_1d_list.append(mini_hist.view().T)
969
- return np.array(hist_1d_list), np.array(x_bin_edges_list)
970
-
971
- def _get_bins_max_dist(self, bins=None, max_dist=None):
972
- """
973
- Get the number of bins and the max distance to generate the radial and density histograms.
974
-
975
- Parameters
976
- ----------
977
- bins: float
978
- Number of bins of the radial distribution.
979
- max_dist: float
980
- Maximum distance to consider in the 1D histogram (in meters).
981
- """
982
- hist_position = "hist_position"
983
- if max_dist is None:
984
- max_dist = np.amax(
985
- [
986
- self.hist_config[hist_position][X_AXIS_STRING]["start"].to(u.m).value,
987
- self.hist_config[hist_position][X_AXIS_STRING]["stop"].to(u.m).value,
988
- self.hist_config[hist_position][Y_AXIS_STRING]["start"].to(u.m).value,
989
- self.hist_config[hist_position][Y_AXIS_STRING]["stop"].to(u.m).value,
990
- ]
991
- )
992
- if bins is None:
993
- bins = (
994
- np.amax(
995
- [
996
- self.hist_config[hist_position][X_AXIS_STRING]["bins"],
997
- self.hist_config[hist_position][Y_AXIS_STRING]["bins"],
998
- ]
999
- )
1000
- // 2
1001
- ) # //2 because of the 2D array going into the negative and
1002
- # positive axis
1003
- return bins, max_dist
1004
-
1005
- def get_photon_radial_distr(self, bins=None, max_dist=None):
1006
- """
1007
- Get the phton radial distribution on the ground in relation to the center of the array.
1008
-
1009
- Parameters
1010
- ----------
1011
- bins: float
1012
- Number of bins of the radial distribution.
1013
- max_dist: float
1014
- Maximum distance to consider in the 1D histogram (in meters).
1015
-
1016
- Returns
1017
- -------
1018
- np.array
1019
- The counts of the 1D histogram with size = int(max_dist/bin_size).
1020
- np.array
1021
- The bin edges of the 1D histogram in meters with size = int(max_dist/bin_size) + 1,
1022
- usually in meter.
1023
- """
1024
- bins, max_dist = self._get_bins_max_dist(bins=bins, max_dist=max_dist)
1025
- bin_edges_1d_list, hist_1d_list = [], []
1026
-
1027
- hist_2d_values_list, x_position_list, y_position_list = self.get_2d_photon_position_distr()
1028
-
1029
- for i_hist, x_pos in enumerate(x_position_list):
1030
- hist_1d, bin_edges_1d = convert_2d_to_radial_distr(
1031
- hist_2d_values_list[i_hist],
1032
- x_pos,
1033
- y_position_list[i_hist],
1034
- bins=bins,
1035
- max_dist=max_dist,
1036
- )
1037
- bin_edges_1d_list.append(bin_edges_1d)
1038
- hist_1d_list.append(hist_1d)
1039
- return np.array(hist_1d_list), np.array(bin_edges_1d_list)
1040
-
1041
- def get_photon_density_distr(self, bins=None, max_dist=None):
1042
- """
1043
- Get the photon density distribution on the ground in relation to the center of the array.
1044
-
1045
- Parameters
1046
- ----------
1047
- bins: float
1048
- Number of bins of the radial distribution.
1049
- max_dist: float
1050
- Maximum distance to consider in the 1D histogram (in meters).
1051
-
1052
- Returns
1053
- -------
1054
- np.array
1055
- The density distribution of the 1D histogram with size = int(max_dist/bin_size),
1056
- usually in $m^{-2}$.
1057
- np.array
1058
- The bin edges of the 1D histogram in meters with size = int(max_dist/bin_size) + 1,
1059
- usually in meter.
1060
- """
1061
- bins, max_dist = self._get_bins_max_dist(bins=bins, max_dist=max_dist)
1062
- bin_edges_1d_list, hist_1d_list = [], []
1063
-
1064
- hist_2d_values_list, x_position_list, y_position_list = self.get_2d_photon_density_distr()
1065
-
1066
- for i_hist, _ in enumerate(x_position_list):
1067
- hist_1d, bin_edges_1d = convert_2d_to_radial_distr(
1068
- hist_2d_values_list[i_hist],
1069
- x_position_list[i_hist], # pylint: disable=unnecessary-list-index-lookup
1070
- y_position_list[i_hist],
1071
- bins=bins,
1072
- max_dist=max_dist,
1073
- )
1074
- bin_edges_1d_list.append(bin_edges_1d)
1075
- hist_1d_list.append(hist_1d)
1076
- return np.array(hist_1d_list), np.array(bin_edges_1d_list)
1077
-
1078
- def get_photon_wavelength_distr(self):
1079
- """
1080
- Get histograms with the wavelengths of the photon bunches.
1081
-
1082
- Returns
1083
- -------
1084
- np.array
1085
- The counts of the wavelength histogram.
1086
- np.array
1087
- The bin edges of the wavelength histogram in nanometers.
1088
-
1089
- """
1090
- return self._get_hist_1d_projection("wavelength")
1091
-
1092
- def get_photon_time_of_emission_distr(self):
1093
- """
1094
- Get the distribution of the emitted time of the Cherenkov photons.
1095
-
1096
- The clock starts when the particle crosses the top of the atmosphere (CORSIKA-defined) if
1097
- self.event_first_interaction_heights is positive or at first interaction if otherwise.
1098
-
1099
- Returns
1100
- -------
1101
- numpy.ndarray
1102
- The counts of the histogram.
1103
- numpy.array
1104
- The bin edges of the time histograms in ns.
1105
-
1106
- """
1107
- return self._get_hist_1d_projection("time")
1108
-
1109
- def get_photon_altitude_distr(self):
1110
- """
1111
- Get the emission altitude of the Cherenkov photons.
1112
-
1113
- Returns
1114
- -------
1115
- numpy.ndarray
1116
- The counts of the histogram.
1117
- numpy.array
1118
- The bin edges of the photon altitude histograms in km.
1119
-
1120
- """
1121
- return self._get_hist_1d_projection("altitude")
1122
-
1123
- @property
1124
- def num_photons_per_event_per_telescope(self):
1125
- """The number of photons per event per telescope."""
1126
- return self._num_photons_per_event_per_telescope
1127
-
1128
- @num_photons_per_event_per_telescope.setter
1129
- def num_photons_per_event_per_telescope(self, num_photons_per_event_per_telescope_to_set):
1130
- """Set the number of photons per event per telescope."""
1131
- self._num_photons_per_event_per_telescope = (
1132
- np.array(num_photons_per_event_per_telescope_to_set)
1133
- .reshape(self.num_events, len(self.telescope_indices))
1134
- .T
1135
- )
1136
-
1137
- @property
1138
- def num_photons_per_event(self):
1139
- """
1140
- Get the the number of photons per events.
1141
-
1142
- Includes the telescopes indicated by self.telescope_indices.
1143
-
1144
- Returns
1145
- -------
1146
- numpy.array
1147
- Number of photons per event.
1148
- """
1149
- self._num_photons_per_event = np.sum(self.num_photons_per_event_per_telescope, axis=0)
1150
- return self._num_photons_per_event
1151
-
1152
- def get_num_photons_per_event_distr(self, bins=50, hist_range=None):
1153
- """
1154
- Get the distribution of photons per event.
1155
-
1156
- Parameters
1157
- ----------
1158
- bins: float
1159
- Number of bins for the histogram.
1160
- hist_range: 2-tuple
1161
- Tuple to define the range of the histogram.
1162
-
1163
- Returns
1164
- -------
1165
- numpy.ndarray
1166
- The counts of the histogram.
1167
- numpy.array
1168
- Number of photons per event.
1169
- """
1170
- hist, bin_edges = np.histogram(self.num_photons_per_event, bins=bins, range=hist_range)
1171
- return hist.reshape(1, bins), bin_edges.reshape(1, bins + 1)
1172
-
1173
- def get_num_photons_per_telescope_distr(self, bins=50, hist_range=None):
1174
- """
1175
- Get the distribution of photons per telescope.
1176
-
1177
- Parameters
1178
- ----------
1179
- bins: float
1180
- Number of bins for the histogram.
1181
- hist_range: 2-tuple
1182
- Tuple to define the range of the histogram.
1183
-
1184
- Returns
1185
- -------
1186
- numpy.ndarray
1187
- The counts of the histogram.
1188
- numpy.array
1189
- Number of photons per telescope.
1190
- """
1191
- hist, bin_edges = np.histogram(self.num_photons_per_telescope, bins=bins, range=hist_range)
1192
- return hist.reshape(1, bins), bin_edges.reshape(1, bins + 1)
1193
-
1194
- def export_histograms(self, overwrite=False):
1195
- """
1196
- Export the histograms to hdf5 files.
1197
-
1198
- Parameters
1199
- ----------
1200
- overwrite: bool
1201
- If True overwrites the histograms already saved in the hdf5 file.
1202
- """
1203
- self._export_1d_histograms(overwrite=overwrite)
1204
- self._export_2d_histograms(overwrite=False)
1205
-
1206
- @property
1207
- def _meta_dict(self):
1208
- """
1209
- Define the meta dictionary for exporting the histograms.
1210
-
1211
- Returns
1212
- -------
1213
- dict
1214
- Meta dictionary for the hdf5 files with the histograms.
1215
- """
1216
- if self.__meta_dict is None:
1217
- self.__meta_dict = {
1218
- "corsika_version": self.corsika_version,
1219
- "simtools_version": version.__version__,
1220
- "iact_file": self.input_file.name,
1221
- "telescope_indices": list(self.telescope_indices),
1222
- "individual_telescopes": self.individual_telescopes,
1223
- "note": "Only lower bin edges are given.",
1224
- }
1225
- return self.__meta_dict
268
+ Histogram x bin edges.
269
+ numpy.ndarray or None
270
+ Histogram uncertainties (if available).
271
+ """
272
+ # plain numpy histogram
273
+ if (
274
+ hist.get("projection") is None
275
+ and hasattr(self, "events")
276
+ and label in self.events.dtype.names
277
+ ):
278
+ return self._get_hist_1d_from_numpy(label, hist)
279
+
280
+ # boost 1D histogram
281
+ if hist.get("projection") is None:
282
+ # Use histogram from hist dict if available, otherwise from self.hist
283
+ if "histogram" in hist:
284
+ histo_1d = hist["histogram"]
285
+ else:
286
+ # No histogram available, return None values
287
+ return None, None, None
288
+ edges = histo_1d.axes.edges.T.flatten()[0]
289
+ view = histo_1d.view()
290
+ if self._check_for_all_attributes(view):
291
+ counts = np.asarray([view["value"].T])
292
+ uncertainties = np.asarray([np.sqrt(view["variance"].T)])
293
+ else:
294
+ counts = np.asarray([view.T])
295
+ uncertainties = None
296
+ return counts, np.asarray([edges]), uncertainties
297
+
298
+ # boost 2D histogram projection
299
+ histo_2d = self.hist[hist["projection"][0]]["histogram"]
300
+ if hist["projection"][1] == "x":
301
+ h = histo_2d[:, sum]
302
+ else:
303
+ h = histo_2d[sum, :]
304
+ edges = h.axes.edges.T.flatten()[0]
305
+ view = h.view()
306
+ if self._check_for_all_attributes(view):
307
+ counts = np.asarray([view["value"].T])
308
+ uncertainties = np.asarray([np.sqrt(view["variance"].T)])
309
+ else:
310
+ counts = np.asarray([view.T])
311
+ uncertainties = None
312
+ return counts, np.asarray([edges]), uncertainties
1226
313
 
1227
- @property
1228
- def dict_1d_distributions(self):
314
+ def _set_1d_distributions(self, r_max=2000 * u.m, bins=100):
1229
315
  """
1230
- Dictionary to label the 1D distributions according to the class methods.
316
+ Define 1D histograms.
1231
317
 
1232
318
  Returns
1233
319
  -------
1234
320
  dict:
1235
- The dictionary with information about the 1D distributions.
321
+ Dictionary with 1D histogram information.
1236
322
  """
1237
- fn_key = "function"
1238
- file_name = "file name"
323
+ file_name = "file_name"
1239
324
  title = "title"
1240
- bin_edges = "bin edges"
1241
- axis_unit = "axis unit"
1242
- self._dict_1d_distributions = {
325
+ projection = "projection"
326
+ x_bins = "x_bins"
327
+ x_axis_unit = "x_axis_unit"
328
+ x_axis_title = "x_axis_title"
329
+ y_axis_unit = "y_axis_unit"
330
+ y_axis_title = "y_axis_title"
331
+ log_y = "log_y"
332
+ photon_density = "Photon density"
333
+ distance_to_center = "Distance to center"
334
+ hist_1d = {
1243
335
  "wavelength": {
1244
- fn_key: "get_photon_wavelength_distr",
1245
336
  file_name: "hist_1d_photon_wavelength_distr",
1246
337
  title: "Photon wavelength distribution",
1247
- bin_edges: "wavelength",
1248
- axis_unit: self.hist_config["hist_position"][Z_AXIS_STRING]["start"].unit,
338
+ projection: ["wavelength_altitude", "x"],
1249
339
  },
1250
- "counts": {
1251
- fn_key: "get_photon_radial_distr",
340
+ "counts_r": {
1252
341
  file_name: "hist_1d_photon_radial_distr",
1253
- title: "Radial photon distribution on the ground",
1254
- bin_edges: "Distance to center",
1255
- axis_unit: self.hist_config["hist_position"][X_AXIS_STRING]["start"].unit,
342
+ title: "Photon lateral distribution (ground level)",
343
+ x_bins: [bins, 0 * u.m, r_max, "linear"],
344
+ x_axis_title: distance_to_center,
345
+ x_axis_unit: u.m,
1256
346
  },
1257
- "density": {
1258
- fn_key: "get_photon_density_distr",
347
+ "density_r": {
1259
348
  file_name: "hist_1d_photon_density_distr",
1260
- title: "Photon density distribution on the ground",
1261
- bin_edges: "Distance to center",
1262
- axis_unit: self.hist_config["hist_position"][X_AXIS_STRING]["start"].unit,
349
+ title: "Photon lateral density distribution (ground level)",
350
+ x_bins: [bins, 0 * u.m, r_max, "linear"],
351
+ x_axis_title: distance_to_center,
352
+ x_axis_unit: u.m,
353
+ y_axis_title: photon_density,
354
+ y_axis_unit: u.m**-2,
355
+ },
356
+ "density_r_from_counts": {
357
+ file_name: "hist_1d_photon_density_from_counts_distr",
358
+ title: "Photon lateral density from counts distribution (ground level)",
359
+ x_bins: [bins, 0 * u.m, r_max, "linear"],
360
+ x_axis_title: distance_to_center,
361
+ x_axis_unit: u.m,
362
+ y_axis_title: photon_density,
363
+ y_axis_unit: u.m**-2,
364
+ },
365
+ "density_x": {
366
+ file_name: "hist_1d_photon_density_x_distr",
367
+ title: "Photon lateral density x distribution (ground level)",
368
+ projection: ["counts_xy", "x"], # projection requires counts_xy histogram
369
+ x_axis_title: distance_to_center,
370
+ x_axis_unit: u.m,
371
+ y_axis_title: photon_density,
372
+ y_axis_unit: u.m**-2,
373
+ },
374
+ "density_y": {
375
+ file_name: "hist_1d_photon_density_y_distr",
376
+ title: "Photon lateral density y distribution (ground level)",
377
+ projection: ["counts_xy", "y"], # projection requires counts_xy histogram
378
+ y_axis_title: photon_density,
379
+ y_axis_unit: u.m**-2,
1263
380
  },
1264
381
  "time": {
1265
- fn_key: "get_photon_time_of_emission_distr",
1266
382
  file_name: "hist_1d_photon_time_distr",
1267
- title: "Photon time of arrival distribution",
1268
- bin_edges: "Time of arrival",
1269
- axis_unit: self.hist_config["hist_time_altitude"][X_AXIS_STRING]["start"].unit,
383
+ title: "Photon arrival time distribution",
384
+ projection: ["time_altitude", "x"],
1270
385
  },
1271
386
  "altitude": {
1272
- fn_key: "get_photon_altitude_distr",
1273
387
  file_name: "hist_1d_photon_altitude_distr",
1274
- title: "Photon altitude of emission distribution",
1275
- bin_edges: "Altitude of emission",
1276
- axis_unit: self.hist_config["hist_time_altitude"][Y_AXIS_STRING]["start"].unit,
388
+ title: "Photon emission altitude distribution",
389
+ projection: ["time_altitude", "y"],
390
+ },
391
+ "direction_cosine_x": {
392
+ file_name: "hist_1d_photon_direction_cosine_x_distr",
393
+ title: "Photon direction cosine x distribution",
394
+ projection: ["direction_xy", "x"],
395
+ },
396
+ "direction_cosine_y": {
397
+ file_name: "hist_1d_photon_direction_cosine_y_distr",
398
+ title: "Photon direction cosine y distribution",
399
+ projection: ["direction_xy", "y"],
1277
400
  },
1278
- "num_photons_per_event": {
1279
- fn_key: "get_num_photons_per_event_distr",
401
+ "num_photons": {
1280
402
  file_name: "hist_1d_photon_per_event_distr",
1281
403
  title: "Photons per event distribution",
1282
- bin_edges: "Event counter",
1283
- axis_unit: u.dimensionless_unscaled,
1284
- },
1285
- "num_photons_per_telescope": {
1286
- fn_key: "get_num_photons_per_telescope_distr",
1287
- file_name: "hist_1d_photon_per_telescope_distr",
1288
- title: "Photons per telescope distribution",
1289
- bin_edges: "Telescope counter",
1290
- axis_unit: u.dimensionless_unscaled,
404
+ "event_type": True,
405
+ x_bins: [100, 0, None, "log"],
406
+ x_axis_title: "Cherenkov photons per event",
407
+ x_axis_unit: u.dimensionless_unscaled,
408
+ log_y: False,
1291
409
  },
1292
410
  }
1293
- return self._dict_1d_distributions
1294
-
1295
- def _export_1d_histograms(self, overwrite=False):
1296
- """
1297
- Auxiliary function to export only the 1D histograms.
1298
411
 
1299
- Parameters
1300
- ----------
1301
- overwrite: bool
1302
- If True overwrites the histograms already saved in the hdf5 file.
1303
- """
1304
- axis_unit = "axis unit"
1305
- for _, function_dict in self.dict_1d_distributions.items():
1306
- self._meta_dict["Title"] = sanitize_name(function_dict["title"])
1307
- histogram_function = getattr(self, function_dict["function"])
1308
- hist_1d_list, x_bin_edges_list = histogram_function()
1309
- x_bin_edges_list = x_bin_edges_list * function_dict[axis_unit]
1310
- if function_dict["function"] == "get_photon_density_distr":
1311
- histogram_value_unit = 1 / (function_dict[axis_unit] ** 2)
1312
- else:
1313
- histogram_value_unit = u.dimensionless_unscaled
1314
- hist_1d_list = hist_1d_list * histogram_value_unit
1315
- for i_histogram, _ in enumerate(x_bin_edges_list):
1316
- if self.individual_telescopes:
1317
- hdf5_table_name = (
1318
- f"/{function_dict['file name']}_"
1319
- f"tel_index_{self.telescope_indices[i_histogram]}"
1320
- )
1321
- else:
1322
- hdf5_table_name = f"/{function_dict['file name']}_all_tels"
1323
-
1324
- table = fill_hdf5_table(
1325
- hist=hist_1d_list[i_histogram],
1326
- x_bin_edges=x_bin_edges_list[i_histogram],
1327
- y_bin_edges=None,
1328
- x_label=function_dict["bin edges"],
1329
- y_label=None,
1330
- meta_data=self._meta_dict,
1331
- )
1332
- self._logger.info(
1333
- f"Writing 1D histogram with name {hdf5_table_name} to {self.hdf5_file_name}."
1334
- )
1335
- # overwrite takes precedence over append
1336
- if overwrite is True:
1337
- append = False
412
+ for value in hist_1d.values():
413
+ value["is_1d"] = True
414
+ value["log_y"] = value.get("log_y", True)
415
+ value[y_axis_title] = (
416
+ "Counts" if value.get(y_axis_title) is None else value[y_axis_title]
417
+ )
418
+ value[y_axis_unit] = (
419
+ u.dimensionless_unscaled if value.get(y_axis_unit) is None else value[y_axis_unit]
420
+ )
421
+ if value.get("projection") is not None:
422
+ hist_2d_name = value["projection"][0]
423
+ if value["projection"][1] == "x":
424
+ value[x_bins] = self.hist[hist_2d_name]["x_bins"]
425
+ value[x_axis_title] = self.hist[hist_2d_name]["x_axis_title"]
426
+ value[x_axis_unit] = self.hist[hist_2d_name]["x_axis_unit"]
1338
427
  else:
1339
- append = True
1340
- write_table(
1341
- table, self.hdf5_file_name, hdf5_table_name, append=append, overwrite=overwrite
1342
- )
428
+ value[x_bins] = self.hist[hist_2d_name]["y_bins"]
429
+ value[x_axis_title] = self.hist[hist_2d_name]["y_axis_title"]
430
+ value[x_axis_unit] = self.hist[hist_2d_name]["y_axis_unit"]
431
+ elif value.get("event_type", False) is False:
432
+ boost_axes = self._create_regular_axes(value, ["x_bins"])
433
+ value["histogram"] = bh.Histogram(boost_axes[0], storage=bh.storage.Weight())
434
+ return hist_1d
1343
435
 
1344
- @property
1345
- def dict_2d_distributions(self):
436
+ def _set_2d_distributions(self, xy_maximum=1000 * u.m, xy_bin=100):
1346
437
  """
1347
- Dictionary to label the 2D distributions according to the class methods.
438
+ Define 2D histograms.
1348
439
 
1349
440
  Returns
1350
441
  -------
1351
442
  dict:
1352
- The dictionary with information about the 2D distributions.
443
+ Dictionary with 2D histogram information.
1353
444
  """
1354
- fn_key = "function"
1355
- file_name = "file name"
445
+ file_name = "file_name"
1356
446
  title = "title"
1357
- x_bin_edges = "x bin edges"
1358
- x_axis_unit = "x axis unit"
1359
- y_bin_edges = "y bin edges"
1360
- y_axis_unit = "y axis unit"
1361
- if self._dict_2d_distributions is None:
1362
- self._dict_2d_distributions = {
1363
- "counts": {
1364
- fn_key: "get_2d_photon_position_distr",
1365
- file_name: "hist_2d_photon_count_distr",
1366
- title: "Photon count distribution on the ground",
1367
- x_bin_edges: "x position on the ground",
1368
- x_axis_unit: self.hist_config["hist_position"][X_AXIS_STRING]["start"].unit,
1369
- y_bin_edges: "y position on the ground",
1370
- y_axis_unit: self.hist_config["hist_position"][Y_AXIS_STRING]["start"].unit,
1371
- },
1372
- "density": {
1373
- fn_key: "get_2d_photon_density_distr",
1374
- file_name: "hist_2d_photon_density_distr",
1375
- title: "Photon density distribution on the ground",
1376
- x_bin_edges: "x position on the ground",
1377
- x_axis_unit: self.hist_config["hist_position"][X_AXIS_STRING]["start"].unit,
1378
- y_bin_edges: "y position on the ground",
1379
- y_axis_unit: self.hist_config["hist_position"][Y_AXIS_STRING]["start"].unit,
1380
- },
1381
- "direction": {
1382
- fn_key: "get_2d_photon_direction_distr",
1383
- file_name: "hist_2d_photon_direction_distr",
1384
- title: "Photon arrival direction",
1385
- x_bin_edges: "x direction cosine",
1386
- x_axis_unit: u.dimensionless_unscaled,
1387
- y_bin_edges: "y direction cosine",
1388
- y_axis_unit: u.dimensionless_unscaled,
1389
- },
1390
- "time_altitude": {
1391
- fn_key: "get_2d_photon_time_altitude_distr",
1392
- file_name: "hist_2d_photon_time_altitude_distr",
1393
- title: "Time of arrival vs altitude of emission",
1394
- x_bin_edges: "Time of arrival",
1395
- x_axis_unit: self.hist_config["hist_time_altitude"][X_AXIS_STRING][
1396
- "start"
1397
- ].unit,
1398
- y_bin_edges: "Altitude of emission",
1399
- y_axis_unit: self.hist_config["hist_time_altitude"][Y_AXIS_STRING][
1400
- "start"
1401
- ].unit,
1402
- },
1403
- "num_photons_per_telescope": {
1404
- fn_key: "get_2d_num_photons_distr",
1405
- file_name: "hist_2d_photon_telescope_event_distr",
1406
- title: "Number of photons per telescope and per event",
1407
- x_bin_edges: "Telescope counter",
1408
- x_axis_unit: u.dimensionless_unscaled,
1409
- y_bin_edges: "Event counter",
1410
- y_axis_unit: u.dimensionless_unscaled,
1411
- },
1412
- }
1413
- return self._dict_2d_distributions
1414
-
1415
- def _export_2d_histograms(self, overwrite):
1416
- """
1417
- Auxiliary function to export only the 2D histograms.
1418
-
1419
- Parameters
1420
- ----------
1421
- overwrite: bool
1422
- If True overwrites the histograms already saved in the hdf5 file.
1423
- """
1424
- x_axis_unit = "x axis unit"
1425
- y_axis_unit = "y axis unit"
1426
-
1427
- for property_name, function_dict in self.dict_2d_distributions.items():
1428
- self._meta_dict["Title"] = sanitize_name(function_dict["title"])
1429
- histogram_function = getattr(self, function_dict["function"])
1430
-
1431
- hist_2d_list, x_bin_edges_list, y_bin_edges_list = histogram_function()
1432
- if function_dict["function"] == "get_2d_photon_density_distr":
1433
- histogram_value_unit = 1 / (
1434
- self.dict_2d_distributions[property_name][x_axis_unit]
1435
- * self.dict_2d_distributions[property_name][y_axis_unit]
1436
- )
1437
- else:
1438
- histogram_value_unit = u.dimensionless_unscaled
447
+ x_bins, y_bins = "x_bins", "y_bins"
448
+ x_axis_title, x_axis_unit = "x_axis_title", "x_axis_unit"
449
+ y_axis_title, y_axis_unit = "y_axis_title", "y_axis_unit"
450
+ z_axis_title, z_axis_unit = "z_axis_title", "z_axis_unit"
451
+ photon_density = "Photon density"
452
+ x_pos = "x position on the ground"
453
+ y_pos = "y position on the ground"
454
+
455
+ hist_2d = {
456
+ "counts_xy": {
457
+ file_name: "hist_2d_photon_count_distr",
458
+ title: "Photon count distribution (ground level)",
459
+ x_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
460
+ y_bins: [xy_bin, -xy_maximum, xy_maximum],
461
+ x_axis_title: x_pos,
462
+ x_axis_unit: xy_maximum.unit,
463
+ y_axis_title: y_pos,
464
+ y_axis_unit: xy_maximum.unit,
465
+ },
466
+ "density_xy": {
467
+ file_name: "hist_2d_photon_density_distr",
468
+ title: "Photon density distribution (ground level)",
469
+ x_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
470
+ y_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
471
+ x_axis_title: x_pos,
472
+ x_axis_unit: xy_maximum.unit,
473
+ y_axis_title: y_pos,
474
+ y_axis_unit: xy_maximum.unit,
475
+ z_axis_title: photon_density,
476
+ z_axis_unit: u.m**-2,
477
+ },
478
+ "density_xy_from_counts": {
479
+ file_name: "hist_2d_photon_density_from_counts_distr",
480
+ title: "Photon density from counts distribution (ground level)",
481
+ x_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
482
+ y_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
483
+ x_axis_title: x_pos,
484
+ x_axis_unit: xy_maximum.unit,
485
+ y_axis_title: y_pos,
486
+ y_axis_unit: xy_maximum.unit,
487
+ z_axis_title: photon_density,
488
+ z_axis_unit: u.m**-2,
489
+ },
490
+ "direction_xy": {
491
+ file_name: "hist_2d_photon_direction_distr",
492
+ title: "Photon arrival direction",
493
+ x_bins: [100, -1, 1, "linear"],
494
+ y_bins: [100, -1, 1, "linear"],
495
+ x_axis_title: "x direction cosine",
496
+ x_axis_unit: u.dimensionless_unscaled,
497
+ y_axis_title: "y direction cosine",
498
+ y_axis_unit: u.dimensionless_unscaled,
499
+ },
500
+ "time_altitude": {
501
+ file_name: "hist_2d_photon_time_altitude_distr",
502
+ title: "Arrival time vs emission altitude",
503
+ x_bins: [100, -2000 * u.ns, 2000 * u.ns, "linear"],
504
+ y_bins: [100, 120 * u.km, 0 * u.km, "linear"],
505
+ x_axis_title: "Arrival time",
506
+ x_axis_unit: u.ns,
507
+ y_axis_title: "Emission altitude",
508
+ y_axis_unit: u.km,
509
+ },
510
+ "wavelength_altitude": {
511
+ file_name: "hist_2d_photon_wavelength_altitude_distr",
512
+ title: "Wavelength vs emission altitude ",
513
+ x_bins: [100, 100 * u.nm, 1000 * u.nm, "linear"],
514
+ y_bins: [100, 120 * u.km, 0 * u.km, "linear"],
515
+ x_axis_title: "Wavelength",
516
+ x_axis_unit: u.nm,
517
+ y_axis_title: "Emission altitude",
518
+ y_axis_unit: u.km,
519
+ },
520
+ }
1439
521
 
1440
- hist_2d_list, x_bin_edges_list, y_bin_edges_list = (
1441
- hist_2d_list * histogram_value_unit,
1442
- x_bin_edges_list * self.dict_2d_distributions[property_name][x_axis_unit],
1443
- y_bin_edges_list * self.dict_2d_distributions[property_name][y_axis_unit],
522
+ for value in hist_2d.values():
523
+ value["is_1d"] = False
524
+ value["log_z"] = True
525
+ value[z_axis_title] = (
526
+ "Counts" if value.get(z_axis_title) is None else value[z_axis_title]
527
+ )
528
+ value[z_axis_unit] = (
529
+ u.dimensionless_unscaled if value.get(z_axis_unit) is None else value[z_axis_unit]
530
+ )
531
+ boost_axes = self._create_regular_axes(value, ["x_bins", "y_bins"])
532
+ value["histogram"] = bh.Histogram(
533
+ boost_axes[0], boost_axes[1], storage=bh.storage.Weight()
1444
534
  )
1445
535
 
1446
- for i_histogram, _ in enumerate(x_bin_edges_list):
1447
- if self.individual_telescopes:
1448
- hdf5_table_name = (
1449
- f"/{self.dict_2d_distributions[property_name]['file name']}"
1450
- f"_tel_index_{self.telescope_indices[i_histogram]}"
1451
- )
536
+ return hist_2d
537
+
538
+ def _update_distributions(self):
539
+ """Update the distributions dictionary with the histogram values and bin edges."""
540
+ self._populate_density_from_probes()
541
+ self._populate_density_from_counts()
542
+ self._filter_density_histograms()
543
+
544
+ for key, value in self.hist.items():
545
+ value["input_file_name"] = str(self.input_file)
546
+ if "hist_values" not in value:
547
+ if value["is_1d"]:
548
+ (
549
+ value["hist_values"],
550
+ value["x_bin_edges"],
551
+ value["uncertainties"],
552
+ ) = self.get_hist_1d_projection(key, value)
1452
553
  else:
1453
- hdf5_table_name = (
1454
- f"/{self.dict_2d_distributions[property_name]['file name']}_all_tels"
1455
- )
1456
- table = fill_hdf5_table(
1457
- hist=hist_2d_list[i_histogram],
1458
- x_bin_edges=x_bin_edges_list[i_histogram],
1459
- y_bin_edges=y_bin_edges_list[i_histogram],
1460
- x_label=function_dict["x bin edges"],
1461
- y_label=function_dict["y bin edges"],
1462
- meta_data=self._meta_dict,
1463
- )
1464
-
1465
- self._logger.info(
1466
- f"Writing 2D histogram with name {hdf5_table_name} to {self.hdf5_file_name}."
1467
- )
1468
- # Always appending to table due to the file previously created
1469
- # by self._export_1d_histograms.
1470
- write_table(
1471
- table, self.hdf5_file_name, hdf5_table_name, append=True, overwrite=overwrite
1472
- )
1473
-
1474
- def export_event_header_1d_histogram(
1475
- self, event_header_element, bins=50, hist_range=None, overwrite=False
1476
- ):
1477
- """
1478
- Export 'event_header_element' from CORSIKA to hd5 for a 1D histogram.
1479
-
1480
- Parameters
1481
- ----------
1482
- event_header_element: str
1483
- The key to the CORSIKA event header element.
1484
- Possible choices are stored in 'self.all_event_keys'.
1485
- bins: float
1486
- Number of bins for the histogram.
1487
- hist_range: 2-tuple
1488
- Tuple to define the range of the histogram.
1489
- overwrite: bool
1490
- If True overwrites the histograms already saved in the hdf5 file.
1491
- """
1492
- hist, bin_edges = self.event_1d_histogram(
1493
- event_header_element, bins=bins, hist_range=hist_range
1494
- )
1495
- bin_edges *= self.event_information[event_header_element].unit
1496
- table = fill_hdf5_table(
1497
- hist=hist,
1498
- x_bin_edges=bin_edges,
1499
- y_bin_edges=None,
1500
- x_label=event_header_element,
1501
- y_label=None,
1502
- meta_data=self._meta_dict,
1503
- )
1504
- hdf5_table_name = f"/event_2d_histograms_{event_header_element}"
1505
-
1506
- self._logger.info(
1507
- f"Exporting histogram with name {hdf5_table_name} to {self.hdf5_file_name}."
1508
- )
1509
- # overwrite takes precedence over append
1510
- if overwrite is True:
1511
- append = False
554
+ (
555
+ value["hist_values"],
556
+ value["x_bin_edges"],
557
+ value["y_bin_edges"],
558
+ value["uncertainties"],
559
+ ) = self.get_hist_2d_projection(value["histogram"])
560
+
561
+ def _filter_density_histograms(self):
562
+ """Filter density histograms based on the normalization method."""
563
+ if self.normalization_method == "per-telescope":
564
+ keys_to_remove = ["density_xy_from_counts", "density_r_from_counts"]
565
+ elif self.normalization_method == "per-bin":
566
+ keys_to_remove = ["density_xy", "density_x", "density_y", "density_r"]
1512
567
  else:
1513
- append = True
1514
- write_table(table, self.hdf5_file_name, hdf5_table_name, append=append, overwrite=overwrite)
1515
-
1516
- def export_event_header_2d_histogram(
1517
- self,
1518
- event_header_element_1,
1519
- event_header_element_2,
1520
- bins=50,
1521
- hist_range=None,
1522
- overwrite=False,
1523
- ):
1524
- """
1525
- Export event_header of a 2D histogram to a hdf5 file.
1526
-
1527
- Searches the 2D histogram for the key 'event_header_element_1' and
1528
- 'event_header_element_2'from the CORSIKA event header.
1529
-
1530
- Parameters
1531
- ----------
1532
- event_header_element_1: str
1533
- The key to the CORSIKA event header element.
1534
- event_header_element_2: str
1535
- The key to the CORSIKA event header element.
1536
- Possible choices for 'event_header_element_1' and 'event_header_element_2' are stored
1537
- in 'self.all_event_keys'.
1538
- bins: float
1539
- Number of bins for the histogram.
1540
- hist_range: 2-tuple
1541
- Tuple to define the range of the histogram.
1542
- overwrite: bool
1543
- If True overwrites the histograms already saved in the hdf5 file.
568
+ raise ValueError(
569
+ f"Unknown normalization_method: {self.normalization_method}. "
570
+ "Must be 'per-telescope' or 'per-bin'."
571
+ )
1544
572
 
1545
- """
1546
- hist, x_bin_edges, y_bin_edges = self.event_2d_histogram(
1547
- event_header_element_1, event_header_element_2, bins=bins, hist_range=hist_range
1548
- )
1549
- x_bin_edges *= self.event_information[event_header_element_1].unit
1550
- y_bin_edges *= self.event_information[event_header_element_2].unit
1551
-
1552
- table = fill_hdf5_table(
1553
- hist=hist,
1554
- x_bin_edges=x_bin_edges,
1555
- y_bin_edges=y_bin_edges,
1556
- x_label=event_header_element_1,
1557
- y_label=event_header_element_2,
1558
- meta_data=self._meta_dict,
1559
- )
573
+ for key in keys_to_remove:
574
+ if key in self.hist:
575
+ del self.hist[key]
1560
576
 
1561
- hdf5_table_name = f"/event_2d_histograms_{event_header_element_1}_{event_header_element_2}"
577
+ def _fill_projected_density_values(self, value):
578
+ """Extract 1D density by using projection Counts and normalizing by area."""
579
+ histo_2d = value["projection"][0]
580
+ source_h = self.hist[histo_2d]["histogram"]
581
+ project_axis = value["projection"][1]
1562
582
 
1563
- self._logger.info(
1564
- f"Exporting histogram with name {hdf5_table_name} to {self.hdf5_file_name}."
1565
- )
1566
- # overwrite takes precedence over append
1567
- if overwrite is True:
1568
- append = False
583
+ if project_axis == "x":
584
+ h_1d = source_h[:, sum]
585
+ total_ortho_width = source_h.axes[1].edges[-1] - source_h.axes[1].edges[0]
1569
586
  else:
1570
- append = True
1571
- write_table(table, self.hdf5_file_name, hdf5_table_name, append=append, overwrite=overwrite)
587
+ h_1d = source_h[sum, :]
588
+ total_ortho_width = source_h.axes[0].edges[-1] - source_h.axes[0].edges[0]
1572
589
 
1573
- @property
1574
- def num_photons_per_telescope(self):
1575
- """
1576
- The number of photons per event, considering the telescopes given by self.telescope_indices.
590
+ areas_1d = h_1d.axes[0].widths * total_ortho_width
1577
591
 
1578
- Returns
1579
- -------
1580
- numpy.array
1581
- Number of photons per telescope.
1582
- """
1583
- self._num_photons_per_telescope = np.sum(
1584
- np.array(self.num_photons_per_event_per_telescope), axis=1
592
+ view = h_1d.view()
593
+ if self._check_for_all_attributes(view):
594
+ vals = view["value"] / areas_1d
595
+ uncs = np.sqrt(view["variance"]) / areas_1d
596
+ else:
597
+ vals = view / areas_1d
598
+ uncs = np.sqrt(vals) # Fallback if no weights
599
+
600
+ value["hist_values"] = np.asarray([vals.T])
601
+ value["x_bin_edges"] = np.asarray([h_1d.axes.edges[0]])
602
+ value["uncertainties"] = np.asarray([uncs.T])
603
+
604
+ def _populate_density_from_probes(self):
605
+ """Build density distributions from per-telescope sampling."""
606
+ if not self._density_samples:
607
+ return
608
+
609
+ s = self._density_samples
610
+ xs, ys = (np.array([p[k] for p in s]) for k in ("x", "y"))
611
+ dens = np.array([p["density"] for p in s])
612
+ errs = np.array([p["density_error"] for p in s])
613
+
614
+ hxy = self.hist["counts_xy"]["histogram"]
615
+ x_edges, y_edges = hxy.axes[0].edges, hxy.axes[1].edges
616
+ r_edges = self.hist["density_r"]["histogram"].axes[0].edges
617
+
618
+ def avg_unc_nd(coords, edges, values, errors):
619
+ num = np.histogramdd(coords, bins=edges, weights=values)[0]
620
+ den = np.histogramdd(coords, bins=edges)[0]
621
+ var = np.histogramdd(coords, bins=edges, weights=errors**2)[0]
622
+ with np.errstate(divide="ignore", invalid="ignore"):
623
+ avg = np.divide(num, den, out=np.zeros_like(num), where=den > 0)
624
+ unc = np.sqrt(np.divide(var, den**2, out=np.zeros_like(var), where=den > 0))
625
+ return avg, unc
626
+
627
+ # 2D
628
+ avg_xy, unc_xy = avg_unc_nd(np.column_stack((xs, ys)), (x_edges, y_edges), dens, errs)
629
+
630
+ self.hist["density_xy"].update(
631
+ {
632
+ "hist_values": np.asarray([avg_xy.T]),
633
+ "x_bin_edges": np.asarray([x_edges]),
634
+ "y_bin_edges": np.asarray([y_edges]),
635
+ "uncertainties": np.asarray([unc_xy.T]),
636
+ }
1585
637
  )
1586
- return self._num_photons_per_telescope
1587
638
 
1588
- @property
1589
- def total_num_photons(self):
1590
- """
1591
- The total number of photons.
639
+ # 1D helpers
640
+ def avg_unc_1d(x, e, v, err):
641
+ return avg_unc_nd(x[:, None], (e,), v, err)
1592
642
 
1593
- Returns
1594
- -------
1595
- float
1596
- Total number photons.
1597
- """
1598
- self._total_num_photons = np.sum(self.num_photons_per_event)
1599
- return self._total_num_photons
1600
-
1601
- @property
1602
- def telescope_positions(self):
1603
- """
1604
- The telescope positions found in the CORSIKA output file.
1605
-
1606
- It does not depend on the telescope_indices attribute.
643
+ ax = self.axis_distance
1607
644
 
1608
- Returns
1609
- -------
1610
- numpy.ndarray
1611
- x, y and z positions of the telescopes and their radius according to the CORSIKA
1612
- spherical representation of the telescopes.
1613
- """
1614
- return self._telescope_positions
1615
-
1616
- @telescope_positions.setter
1617
- def telescope_positions(self, new_positions):
1618
- """
1619
- Set the telescope positions.
1620
-
1621
- Parameters
1622
- ----------
1623
- numpy.ndarray
1624
- x, y and z positions of the telescopes and their radius according to the CORSIKA
1625
- spherical representation of the telescopes.
1626
- """
1627
- self._telescope_positions = new_positions
1628
-
1629
- # In the next five functions, we provide dedicated functions to retrieve specific information
1630
- # about the runs, i.e. zenith, azimuth, total energy, interaction height and Earth magnetic
1631
- # field defined for the run. For other information, please use the get_event_parameter_info
1632
- # function.
1633
- @property
1634
- def event_zenith_angles(self):
1635
- """
1636
- Get the zenith angles of the simulated events in astropy units of degrees.
1637
-
1638
- Returns
1639
- -------
1640
- astropy.Quantity
1641
- The zenith angles for each event.
1642
- """
1643
- if self._event_zenith_angles is None:
1644
- self._event_zenith_angles = np.around(
1645
- (self.event_information["zenith"]).to(u.deg),
1646
- 4,
1647
- )
1648
- return self._event_zenith_angles
1649
-
1650
- @property
1651
- def event_azimuth_angles(self):
1652
- """
1653
- Get the azimuth angles of the simulated events in astropy units of degrees.
1654
-
1655
- Returns
1656
- -------
1657
- astropy.Quantity
1658
- The azimuth angles for each event, usually in degrees.
1659
- """
1660
- if self._event_azimuth_angles is None:
1661
- self._event_azimuth_angles = np.around(
1662
- (self.event_information["azimuth"]).to(u.deg),
1663
- 4,
1664
- )
1665
- return self._event_azimuth_angles
1666
-
1667
- @property
1668
- def event_energies(self):
1669
- """
1670
- Get the energy of the simulated events in astropy units of TeV.
1671
-
1672
- Returns
1673
- -------
1674
- astropy.Quantity
1675
- The total energies of the incoming particles for each event, usually in TeV.
1676
- """
1677
- if self._event_total_energies is None:
1678
- self._event_total_energies = np.around(
1679
- (self.event_information["total_energy"]).to(u.TeV),
1680
- 4,
1681
- )
1682
- return self._event_total_energies
1683
-
1684
- @property
1685
- def event_first_interaction_heights(self):
1686
- """
1687
- Get the height of the first interaction in astropy units of km.
645
+ avg_x, unc_x = (
646
+ avg_unc_1d(xs[np.abs(ys) < ax], x_edges, dens[np.abs(ys) < ax], errs[np.abs(ys) < ax])
647
+ if np.any(np.abs(ys) < ax)
648
+ else (np.zeros(len(x_edges) - 1),) * 2
649
+ )
1688
650
 
1689
- If negative, tracking starts at margin of atmosphere,
1690
- see TSTART in the CORSIKA 7 user guide.
651
+ avg_y, unc_y = (
652
+ avg_unc_1d(ys[np.abs(xs) < ax], y_edges, dens[np.abs(xs) < ax], errs[np.abs(xs) < ax])
653
+ if np.any(np.abs(xs) < ax)
654
+ else (np.zeros(len(y_edges) - 1),) * 2
655
+ )
1691
656
 
1692
- Returns
1693
- -------
1694
- astropy.Quantity
1695
- The first interaction height for each event, usually in km.
1696
- """
1697
- if self._event_first_interaction_heights is None:
1698
- self._event_first_interaction_heights = np.around(
1699
- (self.event_information["first_interaction_height"]).to(u.km),
1700
- 4,
657
+ # Radial density
658
+ rs = np.hypot(xs, ys)
659
+ avg_r, unc_r = avg_unc_1d(rs, r_edges, dens, errs)
660
+
661
+ for k, avg, unc, edges in (
662
+ ("density_x", avg_x, unc_x, x_edges),
663
+ ("density_y", avg_y, unc_y, y_edges),
664
+ ("density_r", avg_r, unc_r, r_edges),
665
+ ):
666
+ self.hist[k].update(
667
+ {
668
+ "hist_values": np.asarray([avg]),
669
+ "x_bin_edges": np.asarray([edges]),
670
+ "uncertainties": np.asarray([unc]),
671
+ }
1701
672
  )
1702
- return self._event_first_interaction_heights
1703
-
1704
- @property
1705
- def magnetic_field(self):
1706
- """
1707
- Get the Earth magnetic field from the events header in astropy units of microT.
1708
-
1709
- A tuple with Earth's magnetic field in the x and z directions are returned.
1710
673
 
1711
- Returns
1712
- -------
1713
- astropy.Quantity
1714
- The Earth magnetic field in the x direction used for each event.
1715
- astropy.Quantity
1716
- The Earth magnetic field in the z direction used for each event.
1717
- """
1718
- if self._magnetic_field_x is None:
1719
- self._magnetic_field_x = (self.event_information["earth_magnetic_field_x"]).to(u.uT)
1720
- if self._magnetic_field_z is None:
1721
- self._magnetic_field_z = (self.event_information["earth_magnetic_field_z"]).to(u.uT)
1722
- return self._magnetic_field_x, self._magnetic_field_z
1723
-
1724
- def get_event_parameter_info(self, parameter):
1725
- """
1726
- Get specific information (i.e. any parameter) of the events.
1727
-
1728
- The parameter is passed through the key word parameter.
1729
- Available options are to be found under self.all_event_keys.
1730
- The unit of the parameter, if any, is given according to the CORSIKA version
1731
- (please see user guide in this case).
1732
-
1733
- Parameters
1734
- ----------
1735
- parameter: str
1736
- The parameter of interest. Available options are to be found under
1737
- self.all_event_keys.
1738
-
1739
- Returns
1740
- -------
1741
- astropy.Quantity
1742
- The array with the event information as required by the parameter.
1743
-
1744
- Raises
1745
- ------
1746
- KeyError:
1747
- If parameter is not valid.
1748
- """
1749
- if parameter not in self.all_event_keys:
1750
- msg = f"key is not valid. Valid entries are {self.all_event_keys}"
1751
- self._logger.error(msg)
1752
- raise KeyError
1753
- return self.event_information[parameter]
1754
-
1755
- def get_run_info(self, parameter):
1756
- """
1757
- Get specific information (i.e. any parameter) of the run.
1758
-
1759
- The parameter is passed through the key word parameter.
1760
- Available options are to be found under self.all_run_keys.
1761
- The unit of the parameter, if any, is given according to the CORSIKA version
1762
- (please see user guide in this case).
1763
-
1764
- Parameters
1765
- ----------
1766
- parameter: str
1767
- The parameter of interest. Available options are to be found under
1768
- self.all_run_keys.
1769
-
1770
- Raises
1771
- ------
1772
- KeyError:
1773
- If parameter is not valid.
1774
- """
1775
- if parameter not in self.all_run_keys:
1776
- msg = f"key is not valid. Valid entries are {self.all_run_keys}"
1777
- self._logger.error(msg)
1778
- raise KeyError
1779
- return self.header[parameter]
1780
-
1781
- def event_1d_histogram(self, key, bins=50, hist_range=None):
1782
- """
1783
- Create a histogram for the all events using key as parameter.
1784
-
1785
- Valid keys are stored in self.all_event_keys (CORSIKA defined).
1786
-
1787
- Parameters
1788
- ----------
1789
- key: str
1790
- The information from which to build the histogram, e.g. total_energy, zenith or
1791
- first_interaction_height.
1792
- bins: float
1793
- Number of bins for the histogram.
1794
- hist_range: 2-tuple
1795
- Tuple to define the range of the histogram.
1796
-
1797
- Returns
1798
- -------
1799
- numpy.ndarray
1800
- The counts of the histogram.
1801
- numpy.array
1802
- Edges of the histogram.
1803
-
1804
-
1805
- Raises
1806
- ------
1807
- KeyError:
1808
- If key is not valid.
1809
- """
1810
- if key not in self.all_event_keys:
1811
- msg = f"key is not valid. Valid entries are {self.all_event_keys}"
1812
- self._logger.error(msg)
1813
- raise KeyError
1814
- hist, bin_edges = np.histogram(
1815
- self.event_information[key].value,
1816
- bins=bins,
1817
- range=hist_range,
674
+ def _populate_density_from_counts(self):
675
+ """Build density distributions by dividing counts histograms by bin area."""
676
+ # --- 2D ---
677
+ hxy = self.hist["counts_xy"]["histogram"]
678
+ xw = np.diff(hxy.axes[0].edges)
679
+ yw = np.diff(hxy.axes[1].edges)
680
+ areas2d = np.outer(xw, yw)
681
+
682
+ dens_xy, unc_xy = self._density_and_unc(hxy.view(), areas2d)
683
+
684
+ self.hist["density_xy_from_counts"].update(
685
+ {
686
+ "hist_values": np.asarray([dens_xy.T]),
687
+ "x_bin_edges": np.asarray([hxy.axes[0].edges]),
688
+ "y_bin_edges": np.asarray([hxy.axes[1].edges]),
689
+ "uncertainties": np.asarray([unc_xy.T]),
690
+ }
1818
691
  )
1819
- return hist, bin_edges
1820
-
1821
- def event_2d_histogram(self, key_1, key_2, bins=50, hist_range=None):
1822
- """
1823
- Create a 2D histogram for the all events using key_1 and key_2 as parameters.
1824
692
 
1825
- Valid keys are stored in self.all_event_keys (CORSIKA defined).
693
+ # --- 1D ---
694
+ hr = self.hist["counts_r"]["histogram"]
695
+ r = hr.axes[0].edges
696
+ areas1d = np.pi * (r[1:] ** 2 - r[:-1] ** 2)
1826
697
 
1827
- Parameters
1828
- ----------
1829
- key_1: str
1830
- The information from which to build the histogram, e.g. total_energy, zenith or
1831
- first_interaction_height.
1832
- key_2: str
1833
- The information from which to build the histogram, e.g. total_energy, zenith or
1834
- first_interaction_height.
1835
- bins: float
1836
- Number of bins for the histogram.
1837
- hist_range: 2-tuple
1838
- Tuple to define the range of the histogram.
698
+ dens_r, unc_r = self._density_and_unc(hr.view(), areas1d)
1839
699
 
1840
- Returns
1841
- -------
1842
- numpy.ndarray
1843
- The counts of the histogram.
1844
- numpy.array
1845
- x Edges of the histogram.
1846
- numpy.array
1847
- y Edges of the histogram.
700
+ self.hist["density_r_from_counts"].update(
701
+ {
702
+ "hist_values": np.asarray([dens_r]),
703
+ "x_bin_edges": np.asarray([r]),
704
+ "uncertainties": np.asarray([unc_r]),
705
+ }
706
+ )
1848
707
 
708
+ def _density_and_unc(self, view, areas):
709
+ """Calculate density and uncertainty by dividing histogram values by areas."""
710
+ if self._check_for_all_attributes(view):
711
+ values = view["value"]
712
+ unc = np.sqrt(view["variance"])
713
+ else:
714
+ values = view
715
+ unc = np.sqrt(view)
716
+ return values / areas, unc / areas
1849
717
 
1850
- Raises
1851
- ------
1852
- KeyError:
1853
- If at least one of the keys is not valid.
1854
- """
1855
- for key in [key_1, key_2]:
1856
- if key not in self.all_event_keys:
1857
- msg = (
1858
- f"At least one of the keys given is not valid. Valid entries are "
1859
- f"{self.all_event_keys}"
1860
- )
1861
- self._logger.error(msg)
1862
- raise KeyError
1863
- hist, x_bin_edges, y_bin_edges = np.histogram2d(
1864
- self.event_information[key_1].value,
1865
- self.event_information[key_2].value,
1866
- bins=bins,
1867
- range=hist_range,
1868
- )
1869
- return hist, x_bin_edges, y_bin_edges
718
+ def _check_for_all_attributes(self, view):
719
+ """Check if view has dtype fields ('value', 'variance')."""
720
+ if hasattr(view, "dtype") and view.dtype.names == ("value", "variance"):
721
+ return True
722
+ return False